Simple Authentication with Bcrypt and Warden
However, let’s face it – Devise can be really hard to work with if you don’t follow The Rails Way™ or at least The Devise Way (not sure if this one is trademarked or I just made it up 😉 ). A lot of things happen under the hood, which can be extremely handy, but if you’re trying to escape a little from Rails framework and follow any other code principles (e.g. CQRS) or maintain sane level of code transparency, it might not be the best solution… Not exactly a pain in the back, but I felt bad committing the code I wrote having to conform to some Devise features to what I wanted to achieve.
There came a moment, when I thought “Maybe it’s time to work on another solution? Something that gives me the level of flexibility that I want and is more style-agnostic than Devise?”.
The solution
I knew that Devise is built on top of Warden – a Rack solution for authenticating requests – so I decided to try it by myself. Here’s what I wanted to achieve:
- keeping passwords in the database in a secure manner
- basic user signup, with password validation
- storing session in a secure manner
So, shall we get going?
First things first, add the gems bcrypt
and warden
to your Gemfile. You should already have a little understanding of what the latter does. The former, though, take care of converting your passwords to a secure format, using the Bcrypt encryption algorithm (read more on the bcrypt-ruby GitHub page or here). Its usage is really simple.
Run bundle install
. Now, to encrypt the password using the Bcrypt algorithm, you run
BCrypt::Password.create('password')
Assuming a User
model with email
and encrypted_password
fields, the above might look like this in the scope of a command object:
class User::Register
include Virtus.model
include ActiveModel::Validations
attribute :email, String
attribute :encrypted_password, String
attribute :password, String
attribute :password_confirmation, String
validates :password, confirmation: true
validates :password, length: { minimum: 8 }
attr_reader :user
def call
validate!
encrypt_password
save_user
end
private
def encrypt_password
@encrypted_password = BCrypt::Password.create(password)
end
def save_user
@user = User.create(attributes.except(:password, :password_confirmation)) # it’s important to store neither of those in the database
end
end
And in authentication command it might look more or less like that :
class User::Authenticate
def initialize(user, password)
@user = user
@password = password
end
def call
authenticate
end
private
def authenticate
BCrypt::Password.new(@user.encrypted_password) == @password
end
end
The authenticate method here checks if the stored password matches the one provided by the user. Basically it comes down to this:
BCrypt::Password.new(@user.encrypted_password) == 'password'
With only a little hassle you can also expand this command to update signed_in_at
or last_sign_in_ip
attributes. It’s all up to you.
To sum up, Bcrypt allows us to hash passwords provided by the users and compare the ones given on authentication with the ones that are already hashed.
Now that we’ve got Bcrypt set up, let’s do some Warden magic.
Warden setup
OK, so as you might already know, Warden is a Rack-based middleware. This means it generally works “underneath” the Rails layer of the application. Its place is in the Rack stack, after the session middleware. So let’s start with actually configuring it, somewhere within the initializers:
Rails.application.config.middleware.insert_after ActionDispatch::Session::CookieStore, Warden::Manager do |manager|
manager.default_strategies :password
manager.failure_app = lambda { |env| SessionsController.action(:new).call(env) }
end
Here, we tell Rails where to insert the Warden::Manager
object, and then define its config parameters. We tell it the name of the default Warden strategy (more on strategies later) and also set SessionsController
as a failure app, because every Warden::Manager
has to have a failure app.
The failure app is what gets called when the authentication fails. The setup above just redirects the user to the sign in form.
Next thing we have to do is to set up serializing the data to and out of the session. The Warden wiki suggests starting with serializing the user’s id. It’s the best way to show you how it works:
Warden::Manager.serialize_into_session do |user|
user.id
end
Warden::Manager.serialize_from_session do |id|
User.get(id)
end
But I decided to generate a unique session token for each user instead:
Warden::Manager.serialize_into_session do |user|
User::SessionTokenGenerator.new(user).call
end
Warden::Manager.serialize_from_session do |session_token|
User.find_by(session_token: session_token)
end
And the User::SessionTokenGenerator looks like this:
class User::SessionTokenGenerator
def initialize(user)
@user = user
end
def call
generate_and_save_session_token
end
private
def generate_and_save_session_token
@user.update(session_token: session_token)
session_token
end
def session_token
@session_token ||= Digest::SHA1.hexdigest("#{Time.now}-#{@user.id}-#{@user.updated_at}")
end
end
One more thing regarding session handling – if we decide to use the session token, we need to clear it on logout. Et voilà:
Warden::Manager.before_logout scope: :user do |user, auth, opts|
user.update(session_token: nil)
end
Next thing we need to do is… declare some strategies, at last! So here we go:
Warden::Strategies.add(:password) do
def valid?
params['email'] && params['password']
end
def authenticate!
user = User.find_by(email: params['email'])
return success!(user) if user && User::Authenticate.new(user, params['password']).call
fail 'Invalid email or password'
end
end
So now for a brief explanation of what’s going on here.
The valid?
method sets the conditions to run the strategy. If the conditions are fulfilled, then the strategy can be run whenever warden.authenticate!
is called. So in this case, if there are both email and password parameter in the request, this strategy can be ran.
On the other hand, the authenticate!
method takes care of actually authenticating the request. Calling success!
and passing it the authenticated object will treat the object as authenticated and serialize it into session as described above. The fail
method halts the chain and returns the specified message. Please note that I’m calling my own Authenticate
class here (shown a few paragraphs above), providing it with user we try to sign in and password provided by him.
Strategies have access to many other methods, if you want to know more, the Warden Strategies wiki is a place to go.
Accessing Warden from your controllers
To simplify things a little bit, let’s define some useful methods in ApplicationController
:
def warden
request.env['warden']
end
def current_user
warden.user
end
helper_method :current_user
The first one shortens our access to warden
, sitting somewhere in the request environment. The second one is a shortcut to the signed in user, in a style somewhat similar to Devise. I also set it as a helper_method
, to freely access it also in your views.
And now for actually using Warden for serving the request. It’s really simple, to be honest.
Call warden.authenticate!
to authenticate the user and trigger the failure app if the authentication fails. You can specify which strategy to use, too – just pass the strategy name as a symbol to the function.
You also have an option to just authenticate the request and proceed if it fails – you should just use warden.authenticate
.
To check if the request was authenticated – call warden.authenticated?
. To log the user out, just call warden.logout
.
To get the message sent to warden with the fail
method you call warden.message
. It’s that simple.
Have a peek into my SessionsController:
class SessionsController < ApplicationController
def new
render :new
end
def create
warden.authenticate(:password)
return render :new, alert: warden.message unless warden.authenticated?
respond_to do |format|
format.html { redirect_to root_url, notice: 'Logged in' }
end
end
def destroy
warden.logout
redirect_to root_url, notice: 'Logged out'
end
end
Pretty plain and simple, isn’t it? 🙂
The last thing left…
Specs!
We’ll use the warden-rspec-rails
gem. It’s actually really simple to use. Long story short it mocks Warden so that you can manipulate it in your specs. Basically, you need to tell your spec_helper.rb
to include Warden ControllerHelpers in controller specs and define a helper method for signing the user in:
RSpec.configure do |c|
c.include Warden::Test::ControllerHelpers, type: :controller
def sign_in(user)
warden.set_user(user)
end
end
For more info, check this gem’s github page.
I also used rails-controller-testing
gem here, to get access to some of its matchers – just so you know.
A sample sessions controller spec might look like this:
describe '#create' do
subject { post :create, params: params }
context 'when user is not signed in' do
let(:params) { { email: user.email, password: 'password' } }
it 'redirects to root' do
subject
expect(response).to redirect_to('/')
end
end
end
describe '#logout' do
subject { delete :destroy }
context 'when user is signed in' do
before do
sign_in user
end
it 'redirects to root' do
subject
expect(response).to redirect_to('/')
end
end
end
And that’s all, folks!
To sum up: Warden enables you to tailor your own authentication strategies in quite an easy way. After you get used to it, it lets you set up an authentication quick’n’easy, and also can be easily expanded, even by strategies written by someone else (see warden_strategies, for example). So… happy authenticating!