End Point

News

Welcome to End Point's blog

Ongoing observations by End Point people.

Custom validation with authlogic: Password can't be repeated.

I recently worked on a small security system enhancement for one of my projects: the user must not be able to repeat his or her password for at least ten cycles of change. Here is a little recipe for all the authlogic users out there.

We will store ten latest passwords in the users table.

def self.up
    change_table :users do |t|
      t.text    :old_passwords
    end
  end

The database value will be serialized and deserialized into Ruby array.

class User
  serialize :old_passwords, Array
end

If the crypted password field has changed, the current crypted password and its salt are added to the head of the array. The array is then sliced to hold only ten passwords.

def update_old_passwords
  if self.errors.empty? and send("#{crypted_password_field}_changed?")
    self.old_passwords ||= []
    self.old_passwords.unshift({:password => send("#{crypted_password_field}"), :salt =>  send("#{password_salt_field}") })
    self.old_passwords = self.old_passwords[0, 10]
  end
end

The method will be triggered after validation before save.

class User
  after_validation :update_old_passwords
end

Next, we need to determine if the password has changed, excluding the very first time when the password is set on the new record.

class User < ActiveRecord::Base
  def require_password_changed?
    !new_record? && password_changed?
  end
end

The validation method itself is implemented below. The idea is to iterate through the stored password salts and encrypt the current password with them using the authlogic mechanism, and then check if the resulting crypted password is already present in the array.

def password_repeated?
  return if self.password.blank?
  found = self.old_passwords.any? do |old_password|
    args = [self.password, old_password[:salt]].compact
    old_password[:password] == crypto_provider.encrypt(args)
  end
  self.errors.add_to_base "New password should be different from the password used last 10 times." if found
end

Now we can plug the validation into the configuration.

class User < ActiveRecord::Base
  acts_as_authentic do |c|
    c.validate :password_repeated?, :if => :require_password_changed?
  end
end

Done!

3 comments:

Anonymous said...

Can you revert this? Now that means that in order to set my password I will need to set 10 unrelated passwords just to get the one I want.

Storing extra data about old passwords provides zero security value.

Anonymous said...

Very clean implementation Marina! Thank you for sharing.

@Anonymous: Generally, I would agree with you, but when your writing software for large corporations (where the lawyers outnumber developers 10-to-1) you sometimes have no choice but to implement features based on ill-formed security concerns... #UX-groinkick ;)

shiju said...
This comment has been removed by the author.