Far Beyond Programming — Braindumps by Eric Teubert

Rails: User Authentication using has_secure_password

01 May 2011

When I encountered Rails I was delighted by all those gems that take away the busywork of reimplementing basic modules again and again. Specifically authentication is such a dreaded module. Glance into the Ruby Toolbox and pick any solution! In my recent project, devise was used. What shall I say? It is a pleasure in the beginning but grows more and more into pain the longer the project lasts. Customization is the hell. Yes, devise tries to be as flexible as possible. Yes, you can override each controller and view. However, if you are going to customize that much, why not implement everything yourself in the first place? That way you know exactly what happens and why and you do not have to spend hours to find out which controller to override. Ryan Bates nailed it:

Something as core to your application as authentication – I don’t really like to use engines for that kind of thing because I find I often end up writing/overriding so much of the functionality in the controller and view layers that the engines really loose their benefit and just make things overall more complicated. – Ryan Bates in Railscast 250: Authentication from Scratch (0:43 - 1:00)

So this time, I decided to choose the manual way. And as it turns out, the basics are pretty simple using just plain rails. Basically, I followed Ryans screencast step by step. But since I am toying with rails 3.1, I wanted to try the newly introduced helper method has_secure_password. It takes care of basic password encryption so you do not have to bother with BCrypt hashing and salting. This is Ryans Rails 3.0 solution:

class User < ActiveRecord::Base
  attr_accessible :email, :password, :password_confirmation
  
  attr_accessor :password
  before_save :encrypt_password
  
  validates_confirmation_of :password
  validates_presence_of :password, :on => :create
  validates_presence_of :email
  validates_uniqueness_of :email
  
  def self.authenticate(email, password)
    user = find_by_email(email)
    if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
      user
    else
      nil
    end
  end
  
  def encrypt_password
    if password.present?
      self.password_salt = BCrypt::Engine.generate_salt
      self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
    end
  end
end

This is my solution using has_secure_password:

class User < ActiveRecord::Base
  has_secure_password
  
  attr_accessible :email, :password, :password_confirmation
  
  validates_presence_of :email
  validates_uniqueness_of :email
  
  def self.authenticate(email, password)
    find_by_email(email).try(:authenticate, password)
  end
end

Neat! Have a look at the well documented implementation for further details: https://github.com/rails/rails/blob/master/activemodel/lib/active_model/secure_password.rb

Update

Fork me on GitHub