Securing an API in Rails 5 using Token Based Authentication

The user can login by providing a valid email and password. In this article, we will use Curl command that sends the user credentials in plain text format. In a real project, this will be sent over a SSL connection. Create a user model that has token for Token based authentication and password_digest for storing login password field. The password_digest is required for has_secure_password method provided by Rails. It adds methods to set and authenticate against a BCrypt password.

rails g model user name token email password_digest

Add the index for the token field in the migration.

add_index :users, :token

Declare has_secure_password in the user model.

class User < ApplicationRecord
  has_secure_password
end

We need to install bcrypt gem to use the has_secure_password Rails builtin functionality for storing encrypted passwords in our database. It also provides authenticate method to check if the password provided by the user is correct. Add the bcrypt gem to Gemfile.

gem 'bcrypt'

Run bundle install. This will install bcrypt gem version 3.1.11. Create some sample records in seeds.rb.

User.destroy_all
User.create(name: 'bugs', email: 'bugs@rubyplus.com', password: '123456')
User.create(name: 'daffy', email: 'daffy@rubyplus.com', password: '123456')

Run the migration and populate the database.

rails db:migrate
rails db:seed

Login User

Let's implement login functionality. When a user successfully logs in, we will return token otherwise we will return error in json format. This token will be used in subsequent calls to access the protected API endpoints. When the user logs out, the token will become invalid and no further calls to the protected endpoints can use the expired token. When a user logs in again, a new token will be generated. Let's create a controller that holds all the API related functionality.

rails g controller api

The code for the api_controller is as shown below:

class ApiController < ActionController::Base
  def require_login
    authenticate_token || render_unauthorized("Access denied")
  end

  def current_user
    @current_user ||= authenticate_token
  end

  protected

  def render_unauthorized(message)
    errors = { errors: [ { detail: message } ] }
    render json: errors, status: :unauthorized
  end

  private

  def authenticate_token
    authenticate_with_http_token do |token, options|
      User.find_by(token: token)
    end
  end  
end

The authenticate_with_http_token takes the token provided in the header of the http request and makes it available in the token block variable. We ignore the options block variable, since we don't need it. Create a sessions controller that inherits from the ApiController.

class SessionsController < ApiController
  skip_before_action :require_login, only: [:create], raise: false

  def create
    if user = User.valid_login?(params[:email], params[:password])
      allow_token_to_be_used_only_once_for(user)
      send_auth_token_for_valid_login_of(user)
    else
      render_unauthorized("Error with your login or password")
    end
  end

  def destroy
    logout
    head :ok
  end

  private

  def send_auth_token_for_valid_login_of(user)
    render json: { token: user.token }
  end

  def allow_token_to_be_used_only_once_for(user)
    user.regenerate_token
  end

  def logout
    current_user.invalidate_token
  end
end

In Rails 5, we need to use the raise: false in skip_before_action filter to return boolean instead of raising an exception. The user model is as shown below.

class User < ApplicationRecord
  has_secure_password
  has_secure_token

  # This method is not available in has_secure_token
  def invalidate_token
    self.update_columns(token: nil)
  end

  def self.valid_login?(email, password)
    user = find_by(email: email)
    if user && user.authenticate(password)
      user
    end
  end
end

The invalidate_token method is required to expire an user's token. It should have been part of the has_secure_token functionality. Unfortunately, we have to implement it. The has_secure_token by default expects token column in the users table. We can customize it by providing it as an argument to the has_secure_token method:

has_secure_token :auth_token

We are using has_secure_token to use the Rails builtin functionality to create unique tokens. Define the routes to handle the API protected endpoints and the login/logout functionality. We only allow the json format request by specifying the format in the constraints option.

Rails.application.routes.draw do
  get 'hacker_spots/index'

  scope :format => true, :constraints => { :format => 'json' } do
    post   "/login"       => "sessions#create"
    delete "/logout"      => "sessions#destroy"
  end
end

The protected endpoint is a simple implementation that returns a json structure.

class HackerSpotsController < ApiController
  before_action :require_login

  # This is protected by API token
  def index
    render json: { spots: 'List of places to work in coffee shops'}
  end
end

The before_action filter enforces the login requirement. The require_login is implemented in the ApiController. Here is the list of Curl commands to test the API from a terminal.

Initial Authorization

curl -X POST --data "email=bugs@rubyplus.com&password=123456" http://localhost:3010/login.json

Incorrect Login Credentials

curl -X POST --data "email=bugs@rubyplus.com&password=123" http://localhost:3010/login.json

Protected API Calls

curl -H "Authorization: Token token=aQNeG5FtnrgU49eC42mShNjX" http://localhost:3010/hacker_spots/index.json

Sign out

curl -X DELETE -H "Authorization: Token token=aQNeG5FtnrgU49eC42mShNjX" http://localhost:3010/logout.json

To mitigate timing attacks, change the API controller as follows:

def authenticate_token
  authenticate_with_http_token do |token, options|
    # Compare the tokens in a time-constant manner, to mitigate timing attacks.
    if user = User.find_by(token: token)
      ActiveSupport::SecurityUtils.secure_compare(
                      ::Digest::SHA256.hexdigest(token),
                      ::Digest::SHA256.hexdigest(user.token))
      user
    end
  end
end  

Expiration of Tokens

We can expire the tokens to time out the inactive login sessions and force the client to login again. Add a token_created_at field to users table.

$ rails g migration add_token_created_at_to_users token_created_at:datetime

Add compound index to the generated migration:

class AddTokenCreatedAtToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :token_created_at, :datetime
    remove_index :users, :token
    add_index :users, [:token, :token_created_at]
  end
end

Run the migration.

rails db:migrate

We need to update the token_created_at attribute when we create and destroy tokens. In Api controller, call the with_unexpired_token User class method:

def authenticate_token
  authenticate_with_http_token do |token, options|
    if user = User.with_unexpired_token(token, 2.days.ago)
      # Compare the tokens in a time-constant manner, to mitigate timing attacks.
      ActiveSupport::SecurityUtils.secure_compare(
                      ::Digest::SHA256.hexdigest(token),
                      ::Digest::SHA256.hexdigest(user.token))
      user
    end
  end
end  

In user model, implement with_unexpired_token method. The complete source code for user is as follows:

class User < ApplicationRecord
  has_secure_password
  has_secure_token

  def self.valid_login?(email, password)
    user = find_by(email: email)
    if user && user.authenticate(password)
      user
    end
  end

  def allow_token_to_be_used_only_once
    regenerate_token
    touch(:token_created_at)
  end

  def logout
    invalidate_token
  end

  def with_unexpired_token(token, period)
    where(token: token).where('token_created_at >= ?', period).first
  end

  private

  # This method is not available in has_secure_token
  def invalidate_token
    update_columns(token: nil)
    touch(:token_created_at)
  end
end

The touch method updates the timestamp of the token_created_at attribute in the users table. Tip: Use -I switch in Curl to view the http response headers. The refactored sessions controller is shown below:

class SessionsController < ApiController
  skip_before_action :require_login, only: [:create], raise: false

  def create
    if user = User.valid_login?(params[:email], params[:password])
      user.allow_token_to_be_used_only_once
      send_auth_token_for_valid_login_of(user)
    else
      render_unauthorized("Error with your login or password")
    end
  end

  def destroy
    current_user.logout
    head :ok
  end

  private

  def send_auth_token_for_valid_login_of(user)
    render json: { token: user.token }
  end
end

You can download the source code for this article from tokie.

Feedback from Github User jaeming

I was checking out the related tutorial for this and found an error in the migration:

add_index :users, :token

This doesn't make sense as indexing foreign keys can only happen between two different tables. You would not index a column to a table to which it already belongs. In other words, you'd only do something like this if Token was a separate table.

If you switch your db to postgres you'll get an error while trying to migrate:

PG::UndefinedTable: ERROR:  relation "users" does not exist
: CREATE  INDEX  "index_users_on_token" ON "users"  ("token")
The reason it work in SQLite is because SQLite basically ignores foreign keys.

References


Related Articles

Watch this Article as Screencast

You can watch this as a screencast Securing an API in Rails 5 using Token Based Authentication


Ace the Technical Interview

  • Easily find the gaps in your knowledge
  • Get customized lessons based on where you are
  • Take consistent action everyday
  • Builtin accountability to keep you on track
  • You will solve bigger problems over time
  • Get the job of your dreams

Take the 30 Day Coding Skills Challenge

Gain confidence to attend the interview

No spam ever. Unsubscribe anytime.