#305 Authentication with Warden pro
- Download:
- source codeProject Files in Zip (96.9 KB)
- mp4Full Size H.264 Video (22.8 MB)
- m4vSmaller H.264 Video (13.1 MB)
- webmFull Size VP8 Video (16.8 MB)
- ogvFull Size Theora Video (29.3 MB)
Warden is a Ruby gem that allows you to perform authentication through Rack Middleware. When you’re writing an application’s authentication from scratch it’s normal to do this entirely within the application but there are several advantages to using Warden. If we use it we can access the authentication through other Rack apps such as mountable engines and we can access authentication outside the application’s controllers for example in the routes file to create routing constraints. We’ll show you how to do this at the end of the episode but before we do we’ll show you what’s involved in adding Warden to a Rails application.
Our Application’s Existing Authentication
The application we’ll be working with is shown below. We already have some authentication set up in it with a sign-up page for creating a new user and a login page where users who have registered can sign up.
This authentication is handled within the Rails application. Before we start moving it over to Warden let’s take a look at what we’ve got so far, starting with the User
model. This model is fairly simple; the key line in it is has_secure_password
. This method is new in Rails 3.1 and it hashes a password into a password_digest
field. We covered this in more detail back in episode 270.
class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmation validates_presence_of :password, on: :create validates_presence_of :email has_secure_password end
Most of the authentication logic takes place in the SessionsController
’s create
action.
def create user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password]) session[:user_id] = user.id redirect_to root_url, notice: "Logged in!" else flash.now.alert = "Invalid email or password" render "new" end end def destroy session[:user_id] = nil redirect_to root_url, notice: "Logged out!" end
In this action we try to find a user by their email address. If we find one we call authenticate
on the password the user supplied which will check that it matches the encrypted password in the database. If it does we set the user_id
session variable to that user’s id
and redirect back to the home page. If the user isn’t found or the password entered is incorrect we’ll show an error message and render the form again.
When a user logs out the destroy action is called. Here we clear the user_id
session variable and redirect to the home page with a message telling the user that they have logged out.
Using Warden For Authentication
There’s more information on the way we’ve implemented the authentication in this application see episode 250. Next, let’s see what we need to do to move it into Warden.
Most of the documentation for Warden can be found on its wiki page. A good place to start is the setup page which shows us how to add Warden as Rack Middleware and this is what we’ll do first. We’ll need to add the warden gem to the gemfile and run bundle
to install it. There is a rails_warden
gem which makes using Warden with Rails a little easier but we won’t use it here as we’ll get a better idea how Warden works by using it directly.
source 'http://rubygems.org' gem 'rails', '3.1.3' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.5' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' # To use ActiveModel has_secure_password gem 'bcrypt-ruby', '~> 3.0.0' gem 'warden'
We can now add Warden as Rack Middleware through an initializer.
Rails.application.config.middleware.use Warden::Manager do |manager| manager.default_strategies :password end Warden::Manager.serialize_into_session do |user| user.id end Warden::Manager.serialize_from_session do |id| User.find(id) end
This code adds Warden::Manager
to our application’s Middleware. We need to define a default strategy in this code’s block and we’re going to use one called password
. Warden doesn’t come with a built-in password strategy so it’s up to us to write it. We can call it anything we like, the name password is entirely arbitrary, and we’ll write this strategy shortly.
The rest of the code in this file tells Warden how to store the currently logged-in user in the session, which we do by storing their id
, and how to fetch the current user from the id
in the session.
We still need to write our password strategy and we’ll do that now. We do this by calling Warden::Strategies.add
, passing it the name of our strategy and a block. We need to define an authenticate!
method in this block which should authenticate the user based on the user request parameters, just like we do in the SessionsController
. The code we’ll need in this method is very similar to the code in the create
action so we can copy it into authenticate!
and modify it a little.
Warden::Strategies.add(:password) do def authenticate! user = User.find_by_email(params['email']) if user && user.authenticate(params['password']) success! user else fail "Invalid email or password" end end end
As we’re in a Rack application we need to use strings instead of symbols when we fetch the email
and password
parameters. If the authentication succeeds we call a success!
method and pass in the user. If it fails we call fail
, passing in a message explaining why.
We can change the SessionsController
now so that it uses Warden for authentication.
def create user = env['warden'].authenticate if user redirect_to root_url, notice: "Logged in!" else flash.now.alert = env['warden'].message render "new" end end def destroy env['warden'].logout redirect_to root_url, notice: "Logged out!" end
We can access Warden at any time through the Rack environment by calling env['warden']
. In the create
action we now use the authenticate
method to authenticate the user against the parameters from the login form. This method will return a User
object if a matching user is found or nil
otherwise. If the authentication fails we can call env['warden'].message
to fetch the message that was passed to fail
.
We can test this out now. As we’ve modified an initializer we’ll need to restart the Rails server if it’s already running but once we have we can visit the login page and try out our form. If we enter an invalid email address or password we’ll see an error message, but when we enter the correct login information we’re logged in so our authentication appears to be working.
There is a problem, though. We’re logged in now, but our application no longer shows the email address of the currently-logged in user as it’s no longer detecting that user correctly. The code that fetches the current user is in a current_user
method in the ApplicationController
and it finds the current user based on the id stored in the user_id
session variable. We should instead now fetch the user from Warden by calling env['warden'].user
.
We’re already calling env['warden']
a lot in our application’s controllers. It would be more convenient if we had a warden method we could use instead so we’ll write one in the ApplicationController
now and use it to return the current user in current_user
.
class ApplicationController < ActionController::Base protect_from_forgery private def current_user warden.user end helper_method :current_user def warden env['warden'] end end
When we reload our home page now we’ll be reported as logged in.
There’s one more place where we’re still using the ‘old’ authentication, in the UsersController
’s create
action where the user is signed up. When a user registers its a good idea to log them in and we do so by setting the user_id
session variable to the id
of the newly created user. This, of course, won’t work any more. We need to call Warden’s set_user
method instead.
def create @user = User.new(params[:user]) if @user.save warden.set_user(@user) redirect_to root_url, notice: "Signed up!" else render "new" end end
Adding a Failure Application
Our app is now using Warden for all of its authentication and it was fairly easy to add. There are still a few things we can do to improve it, though. One good thing to add is called a Failure App. We do this inside the Warden::Manager
Middleware block.
Rails.application.config.middleware.use Warden::Manager do |manager| manager.default_strategies :password manager.failure_app = SessionsController.action(:new) end
We can set manager.failure_app
to any Rack application and it will be triggered whenever authentication fails. In Rails 3 any action in a controller can act as a Rack app so we’ve set this to call the SessionController
’s new
action as this is where we want the application to render the login form when authentication fails.
There’s a potential here, however. In development mode this action will be cached and won’t reload automatically. For this reason it’s better to call the action through a lambda, like this.
Rails.application.config.middleware.use Warden::Manager do |manager| manager.default_strategies :password manager.failure_app = lambda { |env| SessionsController.action(:new).call(env) } end
Now the action will only be called dynamically and the form will reload dynamically in development.
We can take advantage of this new approach in the SessionsController
. In create
we call authenticate
which will return nil
if no matching user. If we call authenticate!
(with a exclamation mark) the failure application will be triggered whenever authentication fails. This means that we can simplify the action’s code.
def new flash.now.alert = warden.message if warden.message.present? end def create warden.authenticate! redirect_to root_url, notice: "Logged in!" end
The create
action can now redirect immediately after calling authenticate!
as if this fails the failure app is called which will call new
and show the login form again. We’ll have to move the flash message to new
and add a check to it so that it only shows if the Warden message is present.
We can try this out now, after restarting our Rails server if need be. If we visit the login page and enter an invalid password we’ll see the error message from the fail
method.
When we enter the correct password we’ll be logged in successfully.
The advantage of this is that no matter where we call the authentication the failure app will be called if authentication fails and our application will behave as we expect it to.
Another good practice is to add a valid?
method to the Warden Strategies we create. This method should return a boolean value depending on whether the strategy should try to authenticate. For our strategy this should only happen when both the email
and password
parameters are present.
Warden::Strategies.add(:password) do def valid? params['email'] && params['password'] end def authenticate! user = User.find_by_email(params['email']) if user && user.authenticate(params['password']) success! user else fail "Invalid email or password" end end end
The benefit of valid?
is more apparent when we have multiple strategies in an application. If we had, say, an api_key
strategy alongside our password
strategy then when authenticate
is called the password strategy’s valid?
method will be checked first to see if it’s valid. If it is then its authenticate!
method is called. If it isn’t valid or if its authentication fails the api_key
strategy will be called. We could have many different strategies in our application, say HTTP basic or an OmniAuth strategy and having a valid?
method for each one will make sure that the correct one is used.
If you want to see examples of different Warden strategies it’s worth looking at the source code for Devise. Devise uses Warden internally and has several different strategies. One that’s particularly interesting is Rememberable which handles “Remember Me” functionality. This strategy uses cookies and if you need to do this you should look at the Warden compatibility file as this shows how to define a cookies
method by using ActionDispatch::Request
to make a request
object.
Using Authentication Outside a Controller
Now that we have Warden set up in our application we’ll show you how it can be used outside a Rails controller by adding a constraint to the routes file. We have signup
and login
routes and for the purposes of this example we’ll make these routes unavailable when the user is logged in.
Auth::Application.routes.draw do scope constraints: lambda { |r| r.env['warden'].user.nil?} do get "signup", to: "users#new", as: "signup" get "login", to: "sessions#new", as: "login" end get "logout", to: "sessions#destroy", as: "logout" root to: "home#index" resources :users resources :sessions end
We add the constraints as a scope and fetch the environment warden
object inside a lambda block to check if there’s a current user. We’ll only allow access to the signup
and login
routes if there isn’t one.
If we login to our application now and visit the signup
page we’ll see a routing error.
This isn’t something we’d do in a real application as it provides a poor user experience but it works an an example of how we can use authentication outside a Rails controller. It’s generally better to add behaviour like this through a controller’s before_filter
. If there are multiple routes pointing to the same action then we may still be able to access the page through one of these.