Simple authentication with bcrypt and warden cover

Whenever you think about solving authentication problems in Rails applications, the default solution is Devise. It’s flexible, provides you with various strategies and helpers out of the box, and it should Just Work™

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, takes 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 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” 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!

 

Post tags:

Join our awesome team
Check offers

Work
with us

Tell us about your idea
and we will find a way
to make it happen.

Get estimate