Live without Devise: a simple Warden authentication service

Nowadays, almost every Rails project has authentication delivered by a powerful devise gem. But, in some circumstances using this gem may be just like shooting a fly with a cannon. For instance, you want to create a simple API that gives access to resources via the authorization access token sent in the header. In this case, after sign in, a user receives an initial token and then uses it in a first request. A response to this request returns a new access token in its header and so on. Pretty straightforward isn’t it. But in the pure devise gem it's quite hard to achieve. You have to look into the gems controllers to inject your authorization logic. Also, a solid test suite should be created to ensure that everything works fine. So, why not create a simple authorization service and abandon devise?

Warden - the spine of Devise

The devise gem is basically based on a warden gem, which gives an opportunity to build authorization direct on a Ruby Rack Stack. This gem is pretty straightforward and well documented. Warden fetches a request data and checks if the request includes valid credentials, according to a defined strategy. If a user has access, warden establishes the request sender in an application context and then passes the request to the next part of Rails Rack Middleware Stack. If verification fails, it calls a special failure procedure, which deals with a no access case.

Let’s create a plain Cars API to demonstrate warden capabilities.

$ rails new car_api --api

We have to add warden and rspec-rails gems to our project.

# Gemfile
gem 'warden'
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails'
end      

Bundle it and install rspec:

$ bundle
$ bundle exec rails g rspec:install

We will need two models: User, which makes requests and wants to get access to API and Car, that plays a role of a restricted resource.

$ bundle exec rails g model User name token:uniq 
$ bundle exec rails g model Car name brand user:belongs_to 
$ bundle exec rake db:migrate 

We’ve done our model layer, so it’s time to add some specs for login endpoint:

# spec/requests/api/v1/sing_in_spec.rb
require 'rails_helper'

RSpec.describe 'Sing in', type: :request do
  describe 'GET /api/v1/login' do
    let!(:user) { User.create(id: 'Stive') }

    it "returns user’s access token" do
      post '/api/v1/login', params: { id: user.id }
      expect(user.token).to be_present
      expect(JSON.parse(response.body)).to eq({ 'token' => user.token })
    end
  end
end    

Let’s make it simple. A client sends id, then in a response, he will receive a first one-time usage token.

Here is an implementation of a sing in functionality:

# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      post 'login' => 'auth#login'
    end
  end
end     
# app/controllers/api/v1/auth_controller.rb
class Api::V1::AuthController < ApplicationController
  def login
    render json: User.find(params[:id]).attributes.slice('token')
  end
end      

If we run our 'request sign in spec', it will fail:

image alt text

The reason of a failure is a lack of user’s secure token. Fortunately, Rails Framework has built-in functionality: ActiveRecord::SecureToken. It allows generating token like values.

Let’s add some specs for the User model and secure token declaration in the model class.

# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  it 'has secure token' do
    expect(User.create(name: 'Bob').token).to be_present
  end
end  
# app/models/user.rb
class User < ApplicationRecord
  has_secure_token

  has_many :cars
end  

Now, all specs should pass.

We have the access token, so let’s make some request to protected content. Before we start implementing devise functionality, we should add specs in TDD manner.

# spec/requests/api/v1/cars_spec.rb
require 'rails_helper'

RSpec.describe 'Cars management', type: :request do
  let!(:user) { User.create(name: 'Bill') }
  let!(:car) { Car.create(name: 'Passat', brand: 'VW', user: user) }

  describe 'GET /api/v1/cars' do
    it 'is protected content' do
      get '/api/v1/cars'

      expect(response.status).to eq 401
    end

    it 'returns cars list for a user with access' do
      get '/api/v1/cars', headers: { access_token: user.token }

      expect(response.status).to eq 200
      expect(JSON.parse(response.body)).to eq [car.attributes.slice('id', 'name', 'brand').as_json]
    end

    it 'cannot use twice same token' do
      get '/api/v1/cars', headers: { access_token: user.token }

      expect(response.status).to eq 200

      last_valid_token = response.header['Access-Token']

      get '/api/v1/cars', headers: { access_token: user.token }

      expect(response.status).to eq 401
      expect(response.header['Access-Token']).to be_nil

      get '/api/v1/cars', headers: { access_token: last_valid_token }

      expect(response.status).to eq 200
    end
  end

  describe 'POST /api/v1/cars' do
    let(:car_params) { { 'brand' => 'Honda', 'name' => 'Civic' } }

    it 'is protected content' do
      get '/api/v1/cars', params: { car: car_params }

      expect(response.status).to eq 401
    end

    it 'creates a car' do
      post '/api/v1/cars', params: { car: car_params }, headers: { access_token: user.token }

      expect(response.status).to eq 201

      new_car = JSON.parse(response.body)

      expect(new_car.slice('id', 'name', 'brand')).to eq(car_params.merge({'id' => Car.last.id}))
      expect(user.cars.count).to eq 2
    end
  end
end

We’ll add a controller for a car resource.

# app/controllers/api/v1/cars_controller.rb
class Api::V1::CarsController < ApplicationController
  prepend_before_action :authenticate!

  def index
    render json: Car.select(:id, :name, :brand)
  end

  def create
    car = current_user.cars.create(car_params)

    render json: car, status: :created
  end

  protected

  def car_params
    params.require(:car).permit(:brand, :name)
  end
end
# config/routes.rb 
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :cars, only: [:index, :create]
      post 'login' => 'auth#login'
    end
  end
end

If we run specs suite now, we get an authenticate! method missing error. Also, we don’t have a current_user method defined. So, it’s time to start the implementation of a simple warden authentication service. The warden gem is a part of Rails Rack Stack, so we have to modify config/application.rb file to add it to the Rails middleware.

# config/application.rb
..
..
config.api_only = true  
config.middleware.use Warden::Manager do |manager|
  manager.default_strategies :token
  manager.failure_app = Proc.new { |env| ['401', {'Content-Type' => 'application/json'}, { error: 'Unauthorized', code: 401 }] }
end

In our case Warden::Manager gets two configuration options:

  1. default_stregies allows setting your own algorithm of authentication, which warden will use. In warden’s philosophy users can define multiple strategies, which will be used by the warden in certain cases. We’ll also create one strategy later.

  2. failure_app option is a procedure that warden will call when authentication fails. The proc block as a parameter gets current Ruby Rack Stack environment, where you can find data related to HTTP request that has been made. The block returns three elements array, which represents Rails Rack response. The first element is response status, in our case 401, that means the user has no valid credentials to gain a resource. The second element is a response header, in our case, we are setting a response content type to JSON. The last element represents a response body, in our case, an authorization error message.

After informing Rails application, that we are using warden, we have to define our token strategy.

# lib/authentication/token_strategy.rb  
require 'warden'

module Authentication
  class TokenStrategy < Warden::Strategies::Base
    def valid?
      access_token.present?
    end

    def authenticate!
      user = User.find_by_token(access_token)

      if user.nil?
        fail!('Could not log in')
      else
        user.regenerate_token
        success!(user)
      end
    end

    private

    def access_token
      @access_token ||= request.get_header('access_token')
    end
  end
end

A strategy class inherits from Warden::Strategies::Base. Each new strategy has to implement an authentication! method. The valid? method is optional, by default it returns true. It checks that authentication may be run. In our token strategy, we try to find a user by the token. If we don’t find one, we invoke warden fail! method, which triggers failure app, that we defined. But, if the user exists we create a new token for that user and call warden success! method. Then the user found by the token can be accessed by warden[‘user’] in any controller. Now, we have to tell warden gem, that we defined a new strategy:

# config/initializers/warden.rb
Warden::Strategies.add(:token, Authentication::TokenStrategy)

Also, we need to include the strategy class in config/application.rb:

# config/application.rb
..
..
require_relative '../lib/authentication/token_strategy.rb'

Bundler.require(*Rails.groups)

module CarApi
  class Application < Rails::Application
    config.load_defaults 5.1
    config.api_only = true
    config.autoload_paths += %W( #{config.root}/lib )
    config.middleware.use Warden::Manager do |manager|
      manager.default_strategies :token
      manager.failure_app = Proc.new { |env| ['401', {'Content-Type' => 'application/json'}, { error: 'Unauthorized', code: 401 }] }
    end
  end
end

To finish our authorization service we have to set a new user’s token in the response, that the user might use in the next request. So, let’s add a new layer to the Rails Rack Stack.

# lib/authentication/set_response_token.rb 
module Authentication
  class SetResponseToken
    def initialize app
      @app = app
    end

    def call env
      res = @app.call(env)
      if res[0] < 300 && !skipp_request(env)
        res[1]["Access-Token"] = env['warden'].user.token
      end
      res
    end

    private

    def skipp_request(env)
      env['REQUEST_URI'] =~ /\/login$/
    end
  end
end

This class is a classic Rack Middleware class. A constructor of this class, as an argument receives the Rails application. The call method checks response status and verifies if the response is not from a sign in request. Then if conditions pass, it adds to the response header user's access token. To make it work, we have to add this class to the Rails application middleware.

# config/application.rb
..
..
require_relative '../lib/authentication/token_strategy.rb'
require_relative '../lib/authentication/set_response_token.rb'

Bundler.require(*Rails.groups)

module CarApi
  class Application < Rails::Application
    config.load_defaults 5.1
    config.api_only = true
    config.autoload_paths += %W( #{config.root}/lib )
    config.middleware.use Warden::Manager do |manager|
      manager.default_strategies :token
      manager.failure_app = Proc.new { |env| ['401', {'Content-Type' => 'application/json'}, { error: 'Unauthorized', code: 401 }] }
    end

    config.middleware.use Authentication::SetResponseToken
  end
end

Finally, we can implement authenticate! and current_user methods.

# lib/authentication/helper_methods.rb
module Authentication
  module HelperMethods
    def authenticate!
      request.env['warden'].authenticate!
    end

    def current_user
      request.env['warden'].user
    end
  end
end      

Let’s make those methods accessible to all controllers in our application.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include Authentication::HelperMethods
end         

Now, if we run specs, we should get all green ones.

image alt text

Success! We finished the authorization service.

Conclusion

The warden gem is a simple and powerful tool that allows building custom authentication in Ruby web applications. A solid understating how does it work, not only allows you to create your own authentication services but also allows extending devise gem by adding to it new authentication strategies.