For most of us, using warden means using devise. Devise served me well but it’s big and there’s a lot of convention that you need to abide by. Getting up & running with devise is fairly simple. Warden needs a little bit more to get started and I’m going to show you how to implement it to have a similar setup.
Warden is great. But it might not be as simple to get started with it than it is with devise. On the other hand, the whole authentication process is done by you which means you have a lot more control and flexibility when your need changes.
If you are moving from devise to warden, the nice thing is you can keep your database schema intact.
What devise use
Warden uses strategies to try to authenticate sessions. However, it provides no strategy, you have to implement them yourself. Devise implements a few that you may already know.
What devise adds to warden
Devise adds a lot of features and stuff, and before you take the plunge it’s probably a good idea to know what is going to be missing by using warden alone.
- Helpers like
current_user
,user_signed_in?
, etc; - Default failure application that handles guest / redirection;
- All the controllers. Don’t worry, it sounds worse than it actually is;
- Many default configuration options.
Warden, sounds cool. How do I implement it?
Now that all the disclaimers have been raised, let’s get down to business and see what can be done with warden alone. First of all, you will need to add warden to your project’s Gemfile.
# Gemfile
gem 'warden'
Migration
In this example, I’ll use a model name User
. Our model will have the following features:
- Encrypted password;
- Confirmation token after registration.
Here’s the migration needed for these to works
# Doesn't have to make sense for now. Trust me!
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email, :null => false
t.string :encrypted_password, :null => false, :default => ""
# Confirmable
t.string :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
end
add_index :users, :email, :unique => true
add_index :users, :confirmation_token, :unique => true
end
Routes
Three controllers needs to take care of log in, registration and confirmation. This can be set the way you want. For clarity’s sake, my routes are going to be very simple.
YourProject::Application.routes.draw do
resources :users do
collection do
resource :registrations, only: [:show, :create]
resource :sessions, only: [:new, :create, :destroy]
resource :confirmations, only: [:show]
end
end
end
Add warden as a rack middleware
Rack is a specification that lets you connect many application to your server. Maybe you didn’t know but rails has a lot of rack instances. Each of them handles a part of the request/response process and the order matters. In your project’s root folder list all the middleware like so.
$ rake middleware
use ActionDispatch::Static
use Rack::Lock
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use ActionDispatch::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionDispatch::BestStandardsSupport
run YouProject::Application.routes
Since the order is important where would warden go? Wiki says that you should add warden after a session middleware. Don’t. If you add it just after the session store, the flash won’t be available to you in some places and you won’t be able to access them.
You want to add warden after ActionDispatch::Flash
.
In rails application.rb
there’s a convenience method to add warden safely.
Warden::Manager.serialize_into_session do |user|
user.id
end
Warden::Manager.serialize_from_session do |id|
User.find_by_id(id)
end
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
end
I also added a way for Warden to serialize from and to the session cookie. This way, Warden knows how to get the User from the database.
Let’s make sure warden is now a member of our middleware stack.
$ rake middleware
use ActionDispatch::Static
use Rack::Lock
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use Warden::Manager
use ActionDispatch::ParamsParser
use ActionDispatch::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionDispatch::BestStandardsSupport
run YouProject::Application.routes
Configure warden to use a failure application.
Now, warden needs a failure application. what does this means? How complicated is this going to be? It’s not obvious. When authentication fails, warden calls this application and you handle the request from there.
Ideally, you want to redirect to the log in page and show a flash message. Browsing devise’s source, I learned that ActionController::Metal
is Rack compliant. That means that my rack application can be a controller! Well now it’s become very much easier to deal with and well, let’s use devise’s concept and apply it to our configuration.
class UnauthorizedController < ActionController::Metal
include ActionController::UrlFor
include ActionController::Redirecting
include Rails.application.routes.url_helpers
include Rails.application.routes.mounted_helpers
delegate :flash, :to => :request
def self.call(env)
@respond ||= action(:respond)
@respond.call(env)
end
def respond
unless request.get?
message = env['warden.options'].fetch(:message, "unauthorized.user")
flash.alert = I18n.t(message)
end
redirect_to new_sessions_url
end
end
First, UnauthorizedController
is a subclass of ActionController::Metal
, not ApplicationController
Failure application is called outside of your expected rails environment. A few modules are included to provide methods that you would normally expect in a controller.
def self.call(env)
is the method that makes this controller Rack compliant. The action(:respond)
method is then called and returned. What this does is that it initialize the UnauthorizedController
and call the method UnauthorizedController#respond
which is pretty much like a normal action in your normal controller.
I added a small condition to check wether the request is a GET method or something else. This was just because I wanted to show a flash message only if it’s not a GET request. Your mileage may vary.
The use of the flash here is the reason why warden had to be inserted after flash or it wouldn’t have worked: Flash would be initialized with request’s hash after the call to this failure application.
At the end, a simple redirection to the login page.
Warden doesn’t know yet about this failure application. Inform it in application.rb
.
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
manager.failure_app = UnauthorizedController
end
User’s password & strategy
So far, we’ve set some stuff that is indirect to a user logging in. It’s time for our User
model to receive some love. Handling password can be scary. Bcrypt is very easy to use is very light weight. Add it to your gemfile
gem 'bcrypt-ruby'
Now, open up your User
model and include Bcrypt
to your model.
require 'bcrypt'
class User < ActiveRecord::Base
include BCrypt
attr_accessible :email
validates :email, :presence => true
before_create :generate_confirmation_token
def password
@password ||= Password.new(self.encrypted_password)
end
def password=(new_password)
@password = Password.create(new_password)
self.encrypted_password = @password
end
def confirm!
self.confirmation_token = nil
self.confirmed_at = Time.now.utc
self.save!
end
private
def generate_confirmation_token
loop do
token = SecureRandom.urlsafe_base64
unless User.where(:confirmation_token => token).any?
self.confirmation_token = token
self.confirmation_sent_at = Time.now.utc
break
end
end
end
end
I added the confirmation algorithm here as well because it’s pretty straightforward and shouldn’t cause too much confusion.
Password handles hashing and salting. Would you need to add a pepper in the mix, you would have to do it yourself.
Now that the User
model can handle passwords, it’s time to implement what warden is known for: strategies. Strategies are classes that tries to authenticate a session via a method name authenticate!
It can either make the authentication process fail!
or let the user in by calling success!
A strategy can also be skipped if it returns false
to the valid?
method.
You can create a folder name strategies in app/ and use namespace to encapsulate your strategies. This exercise is left to you, the reader.
Here’s a strategy that will check if an e-mail and password match and fail otherwise.
class PasswordStrategy < ::Warden::Strategies::Base
def valid?
return false if request.get?
user_data = params.fetch("user", {})
!(user_data["email"].blank? || user_data["password"].blank?)
end
def authenticate!
user = User.find_by_email(params["user"].fetch("email"))
if user.nil? || user.confirmed_at.nil? || user.password != params["user"].fetch("password")
fail! :message => "strategies.password.failed"
else
success! user
end
end
end
Warden::Strategies.add(:password, PasswordStrategy)
Note: If you’re used to HashWithIndifferentAccess, don’t use symbol here. Params is a simple hash. Strategies are called at Rack’s level, not inside Rails routing system.
I use the valid?
method to skip on this strategy if the request is a GET method or if the params do not include a user to log in.
After your class, you need to add your strategy to Warden::Strategies
. Here’s why:
In application.rb, we have configured warden to use a failure application but still haven’t configured warden to use a default strategy. Let’s do this now that we have a strategy to work with.
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
manager.default_strategies :password
manager.failure_app = UnauthorizedController
end
Controllers
The sad thing about everything we did is that it’s hard to know if everything we’ve done is going to work or fall apart. Obviously, testing the algorithms would give a sense of things working, but you still can’t create a new user and log in. Time to fix this!
Remember at the beginning, we created 3 different routes? Before implementing them, let’s add a few helpers that devise usually comes with. These helper methods can be safely added to ApplicationController
:
class ApplicationController < ActionController::Base
protect_from_forgery
prepend_before_filter :authenticate!
helper_method :warden, :signed_in?, :current_user
def signed_in?
!current_user.nil?
end
def current_user
warden.user
end
def warden
request.env['warden']
end
def authenticate!
warden.authenticate!
end
end
class SessionsController < ApplicationController
skip_before_filter :authenticate!
def create
authenticate!
redirect_to :root
end
def destroy
warden.logout
redirect_to :root
end
end
class ConfirmationsController < ApplicationController
skip_before_filter :authenticate!
before_filter :redirect_if_token_empty!
def show
@user = User.where(:confirmation_token => params[:token]).first
if @user.nil?
flash.alert = t("confirmations.user.errors")
redirect_to :root and return
else
flash.notice = t("confirmations.user.confirmed")
@user.confirm!
warden.set_user(@user)
redirect_to user_path(@user) and return
end
end
protected
def redirect_if_token_empty!
unless params.has_key?(:token)
flash.alert = t("confirmations.token.empty")
redirect_to :root and return
end
end
end
class RegistrationsController < ApplicationController
skip_before_filter :authenticate!
def new
@user = User.new
end
def create
@user = User.new(params[:user])
if @user.save
flash[:notice] = t("registrations.user.success")
redirect_to :root
end
end
end
I didn’t include the views because I believe you are capable of creating your own forms & links. If not, there’s a lot of resource to help you with that.
While this post is lengthy, the implementation of warden can be done in about 30 minutes. I (maybe) over-detailed the steps but it’s because I wanted to make sure nothing was left unanswered.
When you create a new user, a confirmation token will be created. If you type in your browser localhost:xxxx/confirmations?token=confirmation_token, your user will be confirmed and will be able to log in to your site. I wanted to add the mailing process of the confirmation token but this post is long enough as it is. If you would really want to have it explained, drop me a line in the comment and I’ll do a follow up post.