Users and Passwords 17
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.
Putting it Together.
So this leaves us with a user model with two relevant columns, salt and hashed_password, leaving us with a simple migration like:
1 2 3 4 5 |
create_table :users do |t| t.string :email t.string :hashed_password t.string :salt 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 25 26 27 28 29 30 |
require 'digest/sha2' 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 def hash_new_password # First reset the salt to a new random string. You could choose a # longer string here but for a salt, 8 bytes of randomness is probably # fine. Note this uses SecureRandom which will use your platform's secure # random number generator. self.salt = ActiveSupport::SecureRandom.base64(8) # Now calculate the hash of the password, with the salt prepended, store # store that in the database self.hashed_password = Digest::SHA2.hexdigest(self.salt + @new_password) end end |
Of note here is the salt, it’s set to a new random value every time the user changes his password, this will come in handy another day. 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 salt the passwords 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 user.hashed_password == Digest::SHA2.hexdigest(user.salt + 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. I’ll follow up next week with simple secure remember-me tokens which also don’t require anything fancy.
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.

The Rails Way is all about teaching "best practices"
in 