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:
- This project doesn’t need to let people sign up.
- There’s no need to send password recovery instructions
- 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
and run your test
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:
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.
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
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:
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.
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 "
" \ -H "Content-Type: text/xml" Test
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.