Users and Passwords

Last week I lost several productive hours resetting my ‘insecure’ password on several websites due to a security breach at a website I haven’t used in more than a decade, if you’ve ever used that site, you’d be well advised to change your password pretty much everywhere. In order to prevent this happening in the future, I figured I’d write up a simple best practices article on handling passwords and authentication. There’s nothing particularly new here, but it’s always worthwhile revisiting the basics.

What Not to do?

Never store your users’ cleartext passwords in your database, no exceptions. The most important reasons is that if your site is compromised or your backup drive lost all your users will be in danger. The attacker will have access to a ready-made list of passwords and the related email addresses which they’ll be able to go off and cause havoc with other websites. The second risk is that any one of your staff members could steal that information and use it to attempt to access other systems where your users have used the same password.

While it’d be nice if users never reused a password, the reality is that nearly everyone does it. We need to be responsible and realistic, and take the necessary precautions to protect our users.

Instead what you should be storing is a hash of the user’s passwords. This will let you verify that the password provided matches the one on file, but will never let you recover that password.

Salting

But just storing a hash of the passwords isn’t enough, this would still leave you open to rainbow attacks where an attacker pre-calculates hashes of millions of passwords, then compares the hashes with the values they’ve stolen from your database. To prevent this you need to salt them. This means storing a small random value against each of your users and adding that to the password before you hash it.

Resistence to Brute Force

The last piece of the puzzle we have to solve is what happens when someone manages to get their hands on the password hash. As Coda Hale points out modern hardware can calculate upwards of half a billion hashes a second. This means that our hash has to be both one way, and slow. General purpose hashing algorithms like MD5 will not do.

Putting it Together.

Thankfully the excellent bcrypt-ruby gem provides a convinient wrappper around both a slow-secure hash function and a salt, saving us an awful lot of effort.

So this leaves us with a user model with a +hashed_password+ attribute, and a simple migration like:

1
2
3
4
  create_table :users do |t|
    t.string :email
    t.string :hashed_password
  end

We’ll also need two kinds of method on the model itself, hashing methods and verification methods. We’ll cover the hashing methods first:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  require 'bcrypt'

  class User < ActiveRecord::Base
    # Create two virtual (in memory only) attributes to hold the password and its confirmation.
    attr_accessor :new_password, :new_password_confirmation
    # We need to validate that the user has typed the same password twice
    # but we only want to do the validation if they've opted to change their password.
    validates_confirmation_of :new_password, :if=>:password_changed?

    before_save :hash_new_password, :if=>:password_changed?

    # By default the form_helpers will set new_password to "",
    # we don't want to go saving this as a password
    def password_changed?
      !@new_password.blank?
    end

    private
    # This is where the real work is done, store the BCrypt has in the
    # database
    def hash_new_password
      self.hashed_password = BCrypt::Password.create(@new_password)
    end
  end

The next method we need to implement is the authentication method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  class User < ActiveRecord::Base
    # As is the 'standard' with rails apps we'll return the user record if the
    # password is correct and nil if it isn't.
    def self.authenticate(email, password)
      # Because we use bcrypt we can't do this query in one part, first
      # we need to fetch the potential user
      if user = find_by_email(email)
        # Then compare the provided password against the hashed one in the db.
        if BCrypt::Password.new(user.hashed_password).is_password? password
          # If they match we return the user 
          return user
        end
      end
      # If we get here it means either there's no user with that email, or the wrong
      # password was provided.  But we don't want to let an attacker know which. 
      return nil
    end
  end

Cleanse the Logs

Finally, you need to make sure that your user’s passwords don’t get logged either, thankfully this is built right in with rails.

1
2
3
  class ApplicationController < ActionController::Base
    filter_parameter_logging :password
  end

Wrap Up

All in all using secure password hashing takes around 15 lines of code so there’s no excuse for not doing it in your applications. It’s also relatively simple so I wouldn’t suggest using a plugin if all you want is simple secure login code, those plugins come with tonnes of additional features which you may not want.

Finally a product plug, 1Password from AgileWebSolutions is a really useful tool for generating, storing and recalling secure passwords for the myriad of websites which require logins. It’s much simpler and more secure than re-using some ‘insecure’ password on dozens of websites. It also has the side benefit of being pretty-well immune to phishing attacks.