Custom authentication methods with Devise

In the past, we have been asked to include other authentication methods in Devise (e.g. token-based and magic email links). Although it might make sense to include those for some applications, there is no plan to support them in Devise.

But don’t be upset, it turns out you might not need to override Devise’s SessionsController or monkey patch some of its internals. In this article, you’ll learn how to create a token-based authentication for a JSON API by relying on Warden’s features.

Disclaimers

Warden? Huh?

This article will focus on how to include custom Warden strategies in a Rails application that uses Devise. If you want to know more about Warden strategies, I gave a talk last year at RailsConf that explains them in more details.

Show me the code!

The first part of this article will show how to set up a Rails application using Devise. If you want to skip to the token authentication part, click here.

Setup

Create a new Rails application (this example uses Postgres as the database to take advantage of UUIDs to generate the access tokens):

rails new devise-token-based-auth --database=postgresql

Add the devise gem to your Gemfile:

gem 'devise'

Now run the Devise generators:

rails generate devise:install
rails generate devise User

We are going to need a column to store the api_token. For this, we’ll use the pgcrypto extension’s gen_random_uuid() function.

First, create a migration to enable the pgcrypto extension:

rails generate migration enable_pgcrypto_extension

Now edit the migration to call the #enable_extension method:

class EnablePgcryptoExtension < ActiveRecord::Migration[5.2]
  def change
    enable_extension 'pgcrypto'
  end
end

The database is now able to use pgcrypto‘s functions. Now we can create a migration to add the api_token column:

rails generate migration add_api_token_to_users

Now edit the migration like the one below. Notice the default is set to gen_random_uuid():

class AddApiTokenToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :api_token, :string, default: -> { 'gen_random_uuid()' }
    add_index :users, :api_token, unique: true
  end
end

Don’t forget to create the database and run the migrations:

rails db:create db:migrate

The next step is to create a user using rails console and grab its api_token:

rails console
Running via Spring preloader in process 60784
Loading development environment (Rails 5.2.2)
irb(main):001:0> user = User.create!(email: '[email protected]', password: '123123')
=> #
irb(main):002:0> user.reload.api_token
=> "a4839b85-4c96-4f22-96f1-c2568e5d6a7f"

It’s time to create the Warden strategy now!

The Api Token Strategy

Create a file app/strategies/api_token_strategy.rb with the following content:

class ApiTokenStrategy < Warden::Strategies::Base
  def valid?
    api_token.present?
  end

  def authenticate!
    user = User.find_by(api_token: api_token)

    if user
      success!(user)
    else
      fail!('Invalid email or password')
    end
  end

  private

  def api_token
    env['HTTP_AUTHORIZATION'].to_s.remove('Bearer ')
  end
end

In short, the strategy tries to find a user for the token sent in the Authorization header. If it does, it signs the user in. Otherwise, it returns an error. If you are not familiar with the success! and fail! methods, watch the talk on the start of the blog post to get a sense on how Warden works.

Warden needs to know about this strategy. Create a file config/initializers/warden.rb with the following code:

Warden::Strategies.add(:api_token, ApiTokenStrategy)

This allows Warden to recognise that it should call the ApiTokenStrategy when it receives the :api_token symbol.

Authenticating a user

Now it’s time to use the strategy. Create an UsersController that renders the current_user in JSON:

class UsersController < ApplicationController
  def show
    render json: current_user.to_json
  end
end

Don’t forget to add a route for this controller action. Open config/routes.rb in your editor and include the following:

Rails.application.routes.draw do
  devise_for :users
  resource :user, only: :show
end

To require authentication in the controller, the method #authenticate! should be called passing the desired strategy as a parameter:

class UsersController < ApplicationController
  def show
    warden.authenticate!(:api_token)
    render json: current_user.to_json
  end
end

You can see that this works using a simple curl request:

curl http://localhost:3000/user -H 'Authorization: Bearer a4839b85-4c96-4f22-96f1-c2568e5d6a7f'

{"id":3,"email":"[email protected]","created_at":"2018-12-26T13:45:37.473Z","updated_at":"2018-12-26T13:45:37.473Z","api_token":"a4839b85-4c96-4f22-96f1-c2568e5d6a7f"}

It is also possible to define :api_token as a default strategy so that it’s called when no strategy is passed as a parameter. Add the following code in the config/initializers/devise.rb file:

Devise.setup do |config|
   # The secret key used by Devise. Devise uses this key to generate...
   config.warden do |manager|
     manager.default_strategies(scope: :user).unshift :api_token
   end

# ==> Mountable engine configurations...
end

This will add the :api_token strategy in the first position, followed by Devise’s default strategies (:rememberable and :database_authenticatable).

Now it’s possible to use Devise’s #authenticate_user! helper, and the :api_token will still be used:

class UsersController < ApplicationController
  before_action :authenticate_user!

  def show
    render json: current_user.to_json
  end
end

Summary

And… we’re done! The focus here was to show how to include custom Warden strategies in a Rails application. The example was straightforward but you can follow this structure to create custom authentication logic to suit your application’s needs.

The entire application used in this article can be found in GitHub.

Comments are closed.