Basic Authentication in Ruby on Rails – In Case You Forgot

Posted by Brian in News (March 11th, 2010)

There are so many authentication choices in Rails these days, but sometimes the simplest approach is the best approach. I’m working with a client right now building an application that has a mostly-public interface. Only a handful of people need to log in to the site, and they only need to modify content occasionally. It’s not a complicated project at all, and while my first instinct was to reach for my starter project that I usually use for these kinds of things, I thought again and realized the following:

  1. This project doesn’t need to let people sign up.
  2. There’s no need to send password recovery instructions
  3. Much of the data entry will be done with a mobile device connecting to the app’s REST-style XML API.

Something like Authlogic, or even Restful Authentication seems like overkill for something this simple. Many Rails developers are probably used to their solutions because many Rails projects are more complex than this. In the spirit of keeping things simple, I’m going to show you how to do authentication of users without any plugins, the way we used to do it in 2005. (For those of you that have been working with Rails as long as me, this will be a good refresher for you. When was the last time you hand-rolled your authentication solution?)

Back To Basic (Authentication, that is)

Since Rails 2.0, Basic Authentication has been an option, as Ryan Bates explains in Railscast #82. We’ll use that and a basic user model to authenticate our users.

First, generate a user mode with login and a hashed password fields. I’m not going to do any salting here, as it’s not necessary. If you want it, you should be able to add it easily.

ruby script/generate model User login:string hashed_password:string

Now we need to modify the User model to do the password encryption.

First, set up the validations and the attribute accessors for the password and password_confirmation fields.

  validates_presence_of :login
  validates_confirmation_of :password
  attr_accessor :password

Next, be a good developer and write a unit test for hashing the password.

  test "should create a user with a hashed password" do
    u = User.create(:login => "homer",
                 :email =>"homer",
                 :password => "1234",
                 :password_confirmation => "1234")
    u.reload # (make sure it saved!)            
    assert_not_nil u.hashed_password
  end

Prepare your test database

rake db:test:clone

and run your test

rake test:units

With the test in place, write the code to encrypt the password on save. Add the SHA1 digest library at the top of your class:

require 'digest/sha1'

Then add the before filter and an encryption method:

  before_save :encrypt_password
 
  def encrypt_password
    unless self.password.blank?
      self.hashed_password = Digest::SHA1.hexdigest(self.password.to_s)
      self.password = nil
    end
    return true 
  end

Run your test again and everything should pass.

rake test:units

Authenticating Users

Now we need to write a class method that we can use to grab a user from the database by looking up their username and hashed password. We’ll use a simple pattern for this. First, let’s write a quick test:

  test "given a user named homer with a password of 1234, he should be authenticated with 'homer' and '1234' " do
    User.create(:login => "homer",
                 :email =>"homer",
                 :password => "1234",
                 :password_confirmation => "1234")
    assert User.authenticated?("homer", "1234")
  end

We create a user and then call User.authenticated?. If its return value evaluates to True, we’ve got a good set of credentials. Add this class method to your User model to make your test pass:

  def self.authenticated?(login, password)
    pwd = Digest::SHA1.hexdigest(password.to_s)
    User.find_by_login_and_hashed_password(login, pwd)
  end

Notice that here, I’m actually returning the user object, rather than a boolean. If no user is found, nil is returned which evaluates to false. Remember, in Ruby, everything except nil and false evaluates to True.

Our entire user model looks like this:

require 'digest/sha1'
class User < ActiveRecord::Base
 
  validates_presence_of :login
  validates_confirmation_of :password
  attr_accessor :password
 
  before_save :encrypt_password
 
  def self.authenticated?(login, password)
    pwd = Digest::SHA1.hexdigest(password.to_s)
    User.find_by_login_and_hashed_password(login, pwd)
  end
 
  private
 
  def encrypt_password
    unless self.password.blank?
      self.hashed_password = Digest::SHA1.hexdigest(self.password.to_s)
      self.password = nil
    end
    return true 
  end
end

Creating the Filter

Let’s create a simple Projects scaffold. We’ll use our authentication to protect this scaffolded interface.

ruby script/generate scaffold Project name:string description:text completed:boolean

Now, open app/controllers/application_controller.rb and this code:

  def authenticate_with_basic_auth
    authenticate_or_request_with_http_basic do |username, password|
      @current_user = User.authenticated?(username, password)
    end
  end

This tiny bit of code will pop up a Basic Authentication credentials box and look the user up in our database using the supplied credentials. If we find our user, we put it in the @current_user instance variable. It’s common practice in Rails apps to have a current_user helper method, so we can add that to application_controller.rb as well.

  helper_method :current_user
 
  def current_user
    @current_user
  end

With that, we simply need to invoke the filter. Open your projects_controller.rb file and add this to the top:

before_filter :authenticate_with_basic_auth

And that’s it! You’ve protected the application. Create a user via the Rails runner, fire up script/server and test it out!

ruby script/runner 'User.create(:login => "homer", :password => "1234", :password_confirmation => "1234")
ruby script/server

You should also be able to use cURL to play with the XML REST-style API provided by the scaffold generator.

Get projects:

curl http://localhost:3000/projects.xml -u homer:1234

Create project via XML

curl http://localhost:3000/projects.xml \
-u homer:1234 \
-X POST \
-d "Test" \
-H "Content-Type: text/xml"

Simple is Good

This simple solution is easy to write, easy to maintain, and easy to extend. It’s also something that anyone with any practical experience with Rails should be able to write in as much time as it would take to configure Authlogic.

So what’s left to do with this? First, SHA1 isn’t great encryption – it’s just hashing. BCrypt might be better, Adding a salt to this hash might be another good idea too. Some more tests would be great. So, go write them, make this fit your security needs, and have fun! As always, I’d love to hear your comments.

Leave a reply

:mrgreen: :neutral: :twisted: :shock: :smile: :???: :cool: :evil: :grin: :oops: :razz: :roll: :wink: :cry: :eek: :lol: :mad: :sad: