In a previous article we discussed some basic sensibilities about accessors in the controller and views, and storing authenticated sessions in your database.
First we’re going to take a conceptual look at credentials and then think about a clever way to deal with them in our application.
In an abstract way we share and exchange a private piece of information to identify a person. In most cases there is also a semi-public piece of information involved. We call this information the person’s credentials. Most of the times this takes the form of a password, and a username or e-mail address.
People can also authenticate themselves with just a shared secret, for example: a session token, SSL certificate, Kerberos ticket, API key, OAuth token, or PIN.
During login a person presents us with their credentials and we check them against our stored version.
Don’t store plain text password. Try not to store any secrets as readable text when you don’t need to.
When your accounts always have exactly one set of credentials you can store them directly on the accounts table.
create_table :accounts do t.string :email t.string :encrypted_password end
In a very tiny amount of cases you may store information about people separately from their accounts. Never make your database structure more complicated than it needs to be. You can always make things more complicated in the future.
As explained in the previous article I don’t like calling the table
users. When the focus is on the credentials I like to use
accounts. If it also needs to store a lot of personal information about the person, I like
Updating the password
Rails likes to use regular accessor methods when you’re writing forms, so let’s pretend our
Account has a regular password field and then make it behave in such a way that it ‘just works’.
<%= form_for(@account) do |f| %> <%= f.password_field :password %> <% end %>
In the model that means we need to be able to read and write the password. We add a reader for the password and the writer updates the password in our actual database field.
class Account < ActiveRecord::Base attr_reader :password def password=(password) @password = password unless password.blank? self.encrypted_password = self.class.encrypt(password) end end end
Note that this means that the password value will always be empty when it wasn’t set during this request. This is something we need to take into account when writing validations.
class Account protected MINIMAL_PASSWORD_LENGTH = 6 def password_requirements # Nothing was set return if @password.nil? # Can't be blank if @password.blank? errors.add(:password, :blank) # Can't be too short elsif @password.to_s.length < MINIMAL_PASSWORD_LENGTH errors.add( :password, :too_short, count: MINIMAL_PASSWORD_LENGTH ) end end validate :password_requirements end
Remember to add translations for the
errors.messages.too_short key, otherwise it will just say ‘Password too short’.
Now we need to select a nice encryption algorithm to secure the password with. I personally like SCrypt. Please be very careful to select an appropriate algorithm that fits your needs and stores the password in an acceptable way.
require 'scrypt' # Automatically chooses a cost value for # its algorithms based on the speed of # the hardware used. SCrypt::Engine.calibrate class Account def self.encrypt(password) SCrypt::Password.create(password).to_s end end
We also need to be able to check the password against the value in the database.
class Acccount def has_password?(password) SCrypt::Password.new(encrypted_password) == password rescue SCrypt::Errors::InvalidHash, NoMethodError # When feeding an invalid value (e.g. nil or a blank) # String we want this to return false instead of # break. false end end
When you have a form that updates the password and checks the old value in one go you can fetch it from the dirty attributes accessors.
class Account def had_password?(password) SCrypt::Password.new( changed_attributes[:encrypted_password] ) == password rescue SCrypt::Errors::InvalidHash, NoMethodError false end end
Keep in mind that password hashing generally just slows down brute force attacks. When your database is compromised you always:
- Immediately clear all passwords to keep attackers out of the application:
UPDATE accounts SET encrypted_password = ''
- Fix your security holes
- Tell people what happened
- Let people pick a new password through a secure password reset mechanism
You generally design this process upfront so you don’t need to work on these kinds of problems when you’re stressed out because of a security breach.