More best practices for authentication in Ruby on Rails

Manfred Stienstra

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.

Storing credentials

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 people.

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:

  1. Immediately clear all passwords to keep attackers out of the application: UPDATE accounts SET encrypted_password = ''
  2. Fix your security holes
  3. Tell people what happened
  4. 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.