“Thanks goodness, there’s only about a billion of these because DHH doesn’t think auth/auth belongs in the core.” | ||
--Comment at http://del.icio.us/revgeorge/authentication |
I bet every web app you’ve ever worked on has needed some form of user security, and some people assume it makes sense to include some sort of standard authentication functionality in a “kitchen-sink” framework such as Rails.
However, it turns out that user security is one of those areas of application design that usually involves a bit more business logic than anyone realizes upfront.
David has clearly stated his opinions on the matter, to help us understand why Rails does not include any sort of standard authentication mechanism:
Context beats consistency. Reuse only works well when the particular instances are so similar that you’re willing to trade the small differences for the increased productivity. That’s often the case for infrastructure, such as Rails, but rarely the case for business logic, such as authentication and modules and components in general.
For better or worse, we need to either write our own authentication code or look outside of Rails core for a suitable solution. It’s not too difficult to write your own authentication code, to the extent that it isn’t really that difficult to write anything in Rails. But why reinvent the wheel? That’s not the Rails way!
As alluded to in the chapter quote, we have many different options out there to choose from. It seems that since authentication is one of the first features you add to a new application, it is also one of the first projects undertaken by many an aspiring plugin writer.
A multitude of options can be a good thing, but I contend that in this particular case it is not. A Google search for “rails authentication” turns up over 5 million results! Looking through the first page of results alone I can count at least ten different approaches to tackling this problem—and what the heck is a salted password generator?
No need to worry or be confused. It turns out that Rails pros agree that the two best authentication plugins are written by Rails core team member Rick Olson, a.k.a. techno weenie[1]. In this chapter, we take an in-depth look at both of them.
Acts as Authenticated is described by Rick as “a simple authentication generator plugin.” It allows you to easily add form-based authentication to your application. It also provides a standard API for accessing information such as whether a user is logged in or authorized as an admin, as well as accessing the User
object itself.
Install acts_as_authenticated
as a plugin by invoking script/plugin install acts_as_authenticated
. First you generate skeleton code for authentication using a Rails generator included in the plugin, and then you customize the basic implementation so that it behaves according to the needs of your particular application.
The code generator is invoked using script/generate
and takes a couple of parameters: one for model and one for controller name. We’ll invoke the generator, specifying user
and account
as our desired names for our authentication model and controller.
$ script/generate authenticated user account exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/account exists test/functional/ exists test/unit/ create app/models/user.rb create app/controllers/account_controller.rb create lib/authenticated_system.rb create lib/authenticated_test_helper.rb create test/functional/account_controller_test.rb create app/helpers/account_helper.rb create test/unit/user_test.rb create test/fixtures/users.yml create app/views/account/index.rhtml create app/views/account/login.rhtml create app/views/account/signup.rhtml create db/migrate create db/migrate/001_create_users.rb
Somewhat similar to the way scaffolding code is generated, we see that a number of files for a model and controller, migration, and associated tests are created. I’ll walk you through use of the plugin and point out what we can learn from it with regard to our own application code.
First let’s take a look at the migration that was automatically created by the generator: db/migrate/001_create_users.rb
.
class CreateUsers < ActiveRecord::Migration def self.up create_table "users", :force => true do |t| t.column :login, :string t.column :email, :string t.column :crypted_password, :string, :limit => 40 t.column :salt, :string, :limit => 40 t.column :created_at, :datetime t.column :updated_at, :datetime t.column :remember_token, :string t.column :remember_token_expires_at, :datetime end end def self.down drop_table "users" end end
The standard columns should look pretty much like ones you’d associate with a User
model. We’ll cover the meaning of crypted_password
and salt
a little later on in the chapter. If you wanted additional columns, such as first and last names, just add them to this migration.
Now we’ll open app/models/user.rb
and see what our shiny new User
model looks like in Listing 14.1.
Example 14.1. The User
Model Generated by Acts As Authenticated
require 'digest/sha1' class User < ActiveRecord::Base # Virtual attribute for the unencrypted password attr_accessor :password validates_presence_of :login, :email validates_presence_of :password, :if => :password_required? validates_presence_of :password_confirmation, :if => :password_required? validates_length_of :password, :within => 4..40, :if => :password_required? validates_confirmation_of :password, :if => :password_required? validates_length_of :login, :within => 3..40 validates_length_of :email, :within => 3..100 validates_uniqueness_of :login, :email, :case_sensitive => false before_save :encrypt_password # Authenticates a user by their login name and unencrypted password, # returning the user or nil. def self.authenticate(login, password) u = find_by_login(login) # need to get the salt u && u.authenticated?(password) ? u : nil end # Encrypts some data with the salt. def self.encrypt(password, salt) Digest::SHA1.hexdigest("—#{salt}--#{password}--") end # Encrypts the password with the user salt def encrypt(password) self.class.encrypt(password, salt) end def authenticated?(password) crypted_password == encrypt(password) end def remember_token? remember_token_expires_at && (Time.now.utc < remember_token_expires_at) end # These create and unset the fields required # for remembering users between browser closes def remember_me self.remember_token_expires_at = 2.weeks.from_now.utc self.remember_token = encrypt("#{email}--#{remember_token_expires_at}") save(false) end def forget_me self.remember_token_expires_at = nil self.remember_token = nil save(false) end protected def encrypt_password return if password.blank? self.salt = Digest::SHA1.hexdigest("--#{Time.now}--#{login}--") if new_record? self.crypted_password = encrypt(password) end def password_required? crypted_password.blank? || !password.blank? end end
Whoa! That’s certainly a lot more code than we’re used to seeing in a Rails-generated class. Let’s analyze the User
model to see what we can learn.
Sometimes it makes sense to add non-database attributes to your ActiveRecord
models. They’re added using attr_*
macros, which you should know about from practically every Ruby language primer in existence.
At the very top of User
, notice that an attribute has been specified for :password
. Does that seem a little weird? Doesn’t the user’s password need to be kept in the database?
It might make a little more sense if we consider that non-database attributes are often used in ActiveRecord
models to hold transient data. The password exists in plaintext only during a request, when it is being submitted from an HTML form. Before saving, the plaintext password string needs to be encrypted, by the encrypt_password
method.
def encrypt_password return if password.blank? if new_record? self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") end self.crypted_password = encrypt(password) end
Notice that the password
property is referenced in the call to password.blank?
and encrypt(password)
. Since non-database attributes exist outside the knowledge of ActiveRecord
, when does the password
attribute get set? Explicitly?
The fact that we have unit test coverage means we have a great way of seeing exactly where our password
attribute is set. My gut says that most of the time these extra attributes are set via ActiveRecord
constructor methods, but by using the User
unit test I can prove it to you quite vividly.
What unit test? The plugin generated one. Before changing anything, let’s run rake test
to make sure we have passing tests to begin with.
$ rake (in /Users/obie/time_and_expense) /opt/local/bin/ruby -Ilib:test "/opt/local/lib/ruby/gems/1.8/gems/rake- 0.7.1/lib/rake/rake_test_loader.rb" "test/unit/user_test.rb" Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake- 0.7.1/lib/rake/rake_test_loader Started .......... Finished in 0.312914 seconds. 10 tests, 17 assertions, 0 failures, 0 errors /opt/local/bin/ruby -Ilib:test "/opt/local/lib/ruby/gems/1.8/gems/rake- 0.7.1/lib/rake/rake_test_loader.rb" "test/functional/account_controller_test.rb" Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake- 0.7.1/lib/rake/rake_test_loader Started .............. Finished in 0.479761 seconds. 14 tests, 26 assertions, 0 failures, 0 errors /opt/local/bin/ruby -Ilib:test "/opt/local/lib/ruby/gems/1.8/gems/rake- 0.7.1/lib/rake/rake_test_loader.rb"
Green bar—all tests passed! That means it’s safe to do some experimentation. Remember we were wondering about how that password
attribute is used. What will break if we change attribute_accessor
to attribute_reader
, thereby making password
read-only?
class User < ActiveRecord::Base # Non-database attribute for the unencrypted password attr_reader :password
When we run the test suite again, it turns out that a whole bunch of tests broke, effectively pointing to every place in the codebase where the value of password
is set. All of them are indeed ActiveRecord
constructors.
For example, one of the errors occurs in the test for the signup
method of AccountController
.
4) Error: test_should_require_pwd_confirmation_on_signup(AccountControllerTest): NoMethodError: undefined method `password=' for #<User:0x2a45068> active_record/base.rb:1842:in `method_missing' active_record/base.rb:1657:in `attributes=' active_record/base.rb:1656:in `attributes=' active_record/base.rb:1490:in `initialize_without_callbacks' active_record/callbacks.rb:225:in `initialize' app/controllers/account_controller.rb:23:in `signup'
A quick look at the signup
method confirms that the password
attribute is getting passed into User
’s constructor, bundled into the user parameters hash that is generated when the signup form is posted submitted from the view.
def signup @user = User.new(params[:user]) return unless request.post? ... end
Non-database attributes are significant because they can be leveraged to mask implementation choices from the view. In this case, it would present a security risk to expose the salt
and crypted_password
properties to the view.
Turning our attention back to the User
model, we see that Rick has provided sensible defaults for password validation. Of course, you have the freedom to modify these to your heart’s content to match your own requirements and purpose.
validates_presence_of :password :if => :password_required? validates_presence_of :password_confirmation, :if => :password_required? validates_length_of :password, :within => 4..40, :if => :password_required? validates_confirmation_of :password, :if => :password_required?
Those validation rules should only execute under one condition, if the crypted_password
attribute is blank and the password
attribute is not. Without this condition in place, every update to the user model would reset the password.
def password_required? crypted_password.blank? || !password.blank? end
As covered in Chapter 2, “Working with Controllers,” callbacks allow you to point at a method that should be invoked at a certain stage in the life cycle of an ActiveRecord
object, like this:
before_save :encrypt_password
Getting back to the User
model walk-through, notice that there is a need to encrypt the user’s password before saving a new user record to the database. The :encrypt_password
symbol points to the protected method of the same name closer to the bottom of the class:
def encrypt_password return if password.blank? if new_record? self.salt = Digest::SHA1.hexdigest("--#{Time.now}--#{login}--") end self.crypted_password = encrypt(password) end
This says: “First of all, if the password is blank, don’t try to encrypt it. Otherwise, calculate and capture the salt
and crypted_password
attributes for saving.”
The salt
column stores a one-time hashing key, which helps to make our authentication system more secure than if a system-wide constant were used.
Moving along, the two-line authenticate
method serves as a good example of a class method for your application code. It is a generic bit of class logic, not associated with any particular instance. However, its implementation might be somewhat dense and cryptic unless you know Ruby really well. I’ll try to dissect it for you.
# Authenticates a user by their login name and unencrypted password, # returning the user or nil. def self.authenticate(login, password) u = find_by_login(login) # need to get the salt u && u.authenticated?(password) ? u : nil end
The first line attempts to find a user record using the login
value supplied. If the record does not exist, the finder will return nil
, which will be assigned to u
, causing the &&
expression on line 2 to return false
.
However, if a record user is found, then it is time to check the password. Rick’s comment succinctly says: “need to get the salt,” because if we were not using a unique salt value per user, then authentication could be done by simply querying the database for username and the crypted password. Assuming that call to find_by_login
returned a user instance, the ternary expression on line 2 will invoke authenticated?
to determine whether to return the user instance or nil
.
You know how a lot of web applications have a little check box under their login and password fields so that you don’t have to authenticate manually every time? That is accomplished via a shared secret in the form of the remember_token
. Whenever the remember_me
method of User
is invoked, an encrypted string is created to be stored as a cookie on the user’s web browser. Rick’s default implementation lasts two weeks before expiring, but you can change it to meet your need.
The forget_me
method simply clears the attributes.
def remember_me self.remember_token_expires_at = 2.weeks.from_now.utc self.remember_token = encrypt("#{email}--#{remember_token_expires_at}") save(false) end def forget_me self.remember_token_expires_at = nil self.remember_token = nil save(false) end
Here’s a bit of Rails trivia about save
with a Boolean argument, as seen in those last two methods. It means save without running validations. I call this a bit of trivia because the Rails API docs don’t mention that save
takes a Boolean argument. In fact that’s because the normal implementation doesn’t—until you specify validations on your model—meaning that the method signature of save
changes dynamically at runtime.
The remember_token?
method checks to see whether we have an unexpired token to check. Notice that by default it works with UTC, not local time.
def remember_token? remember_token_expires_at && (Time.now.utc < remember_token_expires_at) end
That does it for our walk-through of the acts_as_authenticated
plugin-generated User
model. Of course you can add additional application code to User
to represent your own user-related business logic.
Now, let’s open up app/controllers/account_controller.rb
and examine the actions that were generated for us by the plugin, in Listing 14.2.
Example 14.2. The AccountController
Class
class AccountController < ApplicationController # Be sure to include AuthenticationSystem in Application Controller # instead of here include AuthenticatedSystem # If you want "remember me" functionality, add this before_filter # to Application Controller before_filter :login_from_cookie # say something nice, you goof! something sweet. def index redirect_to(:action => 'signup') unless logged_in? || User.count > 0 end def login return unless request.post? self.current_user = User.authenticate(params[:login], params[:password]) if current_user if params[:remember_me] == "1" self.current_user.remember_me cookies[:auth_token] = { :value => self.current_user.remember_token, :expires => self.current_user.remember_token_expires_at } end redirect_back_or_default(:controller => '/account', :action => 'index') flash[:notice] = "Logged in successfully" end end def signup @user = User.new(params[:user]) return unless request.post? @user.save! self.current_user = @user redirect_back_or_default(:controller => '/account', :action => 'index') flash[:notice] = "Thanks for signing up!" rescue ActiveRecord::RecordInvalid render :action => 'signup' end def logout self.current_user.forget_me if logged_in? cookies.delete :auth_token reset_session flash[:notice] = "You have been logged out." redirect_back_or_default(:controller => '/account', :action => 'index') end end
Note the important instructions given in the comments near the top of the file. There are a couple of lines of code there that we should move to our application controller.
class ApplicationController < ActionController::Base include AuthenticatedSystem
First of all, we should include the AuthenticatedSystem
module in our ApplicationController
so that its methods are available to all the controllers in our system. The AuthenticatedSystem
module gives us reader and writer methods in our controllers for current_user
(as stored in the session). It also gives us a very useful logged_in?
method. We can use these methods in the header of our application layout, for example, to display login and logout links based on whether the user is logged in or not:
<div id="login_message"> <% if logged_in? -%> <%= "Logged in as <strong>#{current_user.name}</strong>" %> | <%= link_to "Logout", :controller => 'account', :action => 'logout' %> <% else -%> <%= link_to "Logout", :controller => 'account', :action => 'login' %> | <%= link_to "Signup", :controller => 'account', :action => 'signup' %> <% end -%> </div>
This brings up a very interesting and potentially puzzling question: How is it that you can access current_user
and logged_in?
from the view? Controller
methods (which these are, via the AuthenticatedSystem
module). They should only be available from the view if they’re declared as helper methods. Taking a quick look at application.rb
, we notice that there isn’t a call to helper_method
in sight.
The answer lies near the middle of authenticated_system.rb
in the self.included
method:
# Inclusion hook to make #current_user and #logged_in? # available as ActionView helper methods. def self.included(base) base.send :helper_method, :current_user, :logged_in? end
Aha! Personally, I don’t like putting included
hooks anywhere except the top of the modules because it can cause a bit of confusion if you don’t see them right away.
To explain, what is happening here is that when AuthenticatedSystem
is included into another class, this hook will be invoked in the class context of the object doing the inclusion, and thus the helper_method
will happen exactly as if it had been hard-coded into the original class. A bit of metaprogramming magic? No, just proper use of Ruby’s modules functionality and very much part of the Rails way.
Second, there’s the optional filter that enables logging in using a browser cookie, which gives us the so-called “Remember me” functionality. For now, let’s assume that we do intend to let users stay logged in to our application, so we move before_filter :login_from_cookie
to ApplicationController
also. Let’s take a peek under the covers at what the login_from_cookie
method of AuthenticatedSystem
actually does:
# When called with before_filter :login_from_cookie will check for an # :auth_token cookie and log the user back in if apropriate def login_from_cookie return unless cookies[:auth_token] && !logged_in? user = User.find_by_remember_token(cookies[:auth_token]) if user && user.remember_token? user.remember_me self.current_user = user cookies[:auth_token] = { :value => self.current_user.remember_token, :expires => self.current_user.remember_token_expires_at } flash[:notice] = "Logged in successfully" end end
This code reads fairly well, but let’s do a quick walk-through as another example of idiomatic Ruby and Rails code, as well as usage of the cookies
object.
The first line of the method demonstrates proper use of Ruby’s optional return
keyword, bailing out unless there is an :auth_token
cookie and we’re not already logged in. It’s important to not burn too many cycles before bailing out, since this line will get executed during each request in the system.
Next step is to use the :auth_token
from the cookie to look up the user from the database. The way that the if
clause is structured is a very common idiom. The &&
will short-circuit and return false if user
is nil
, which prevents the NoMethodError
from occurring if remember_token
was invoked on nil
. You’ll see this particular idiom over and over again in Rails code and you should learn to use it in your own coding.
If the condition passes, we call remember_me
on the user object, and set it as the current user (which is effectively what accomplishes the “login”). Finally, updated cookie values are set and a “Logged in successfully” message is placed in flash storage for display to the user.
My Java-addled brain has occasionally found lines such as self.current_user = user
confusing. “Why on earth would we be setting the current user as an instance variable on the controller!?!?” The thing is that the implementation of the current_user
reader and writer methods are more than just pedestrian getters and setters! They actually have quite a bit of logic in them, albeit written in somewhat terse Ruby code.
# Accesses the current user from the session. def current_user @current_user ||= (session[:user] && User.find_by_id(session[:user])) || :false end # Store the given user in the session. def current_user=(new_user) session[:user]= (new_user.nil? || new_user.is_a?(Symbol)) ? nil : new_user.id @current_user = new_user end
Your own implementation of GuestUser
will vary depending on the needs of your application. You might be inclined to mimic the interface of a real User
object, except with empty attributes.
A different approach would be to give your GuestUser
a method_missing
callback that raises a LoginRequiredError
to be rescued by the authentication system. The idea is to automatically prompt for a login before access to a given resource can continue, instead of having to code the particular case explicitly or even worse, cause a server error.
Rick also gives us a Ruby module named AuthenticatedTestHelper
that we can mix into TestUnit
in our test_helper.rb
file so that its methods are always available in our test cases.
class Test::Unit::TestCase include AuthenticatedTestHelper
The most important method in that module is login_as
, which you can call from a setup
method or a test case to establish a session. Pass it a User
object to log in; or (this is where your user fixtures come in pretty handy), login_as
knows to reference the user fixtures (test/fixtures/users.yml
) when you pass it a symbol instead:
def setup login_as(:quentin) # quentin was added by acts_as_authenticated
AuthenticatedTestHelper
also gives you a method named authorize_as
that simulates HTTP basic authentication, instead of simply assigning the user you identify as the current_user
for the session. You can use authorize_as
to test controller actions that will be serving web services and other users that will authenticate via HTTP, instead of logging in via a form.
Finally, the assert_requires_login
and assert_http_authentication_required
test helper methods take blocks and allow you to verify that given controller actions actually force the user to log in or use basic authentication.
Almost every Rails application needs some sort of login and access control functionality. That’s why it’s so convenient to learn how to use the Acts as Authenticated plugin, the main subject of this chapter. In addition to the plugin’s code generator and user class, we also looked at the account controller it generates, how to log in from a cookie, and how to access the current user from the rest of our application code.
1. | Rick’s website is http://techno-weenie.net/. |
3.133.127.37