Simple approach to Rails 5 API authentication with Json Web Token

Published Oct 09, 2017Last updated Dec 17, 2017
Simple approach to Rails 5 API authentication  with Json Web Token

Many programmers believe ruby is too easy…, yes I agree. I think ruby makes programmers happy. Rails is written in Ruby, the programming language which is also used alongside Rails. Before we move further, it is better to understand that ruby is a language such as php or python while rails is a framework such Laravel and Django respectively. Ruby on Rails offers more than just pure speed, it’s faster to develop an initial working product in Rails. One of key principles of Ruby on Rails development is convention over configuration. This means that the programmer does not have to spend a lot of time configuring files in order to get setup, Rails comes with a set of conventions which help speed up development.

Why JSON Web Token ?

With JWTs, you can save it in sessions or cookies without worrying about it being manipulated by the user or another third party, thanks to it being signed by the server and secret key. You also need to be more careful with the kind of data you store with JWTs, it is not proper to store sensitive data in JWTs.

How do JSON Web Tokens work?

During authentication, when the user successfully logs in using their credentials, a JSON Web Token will be returned which could be saved in sessions, localstorage or cookies,whenever the user wants to access a protected route or resource, the user agent(AJAX call or Postman request) should send the JWT with the Authorization header using the Bearer schema. The content of the header should look like the following:

Authorization: Bearer <token>

The following diagram shows this process:
jwt-diagram.png

What is the JSON Web Token structure?

The token is separated in three base-64 encoded, dot-separated values, each one representing a different type of data:

  • Header
  • Payload
  • Signature

Header
The header contains of two parts: the type of the token, which is JWT, and the hashing algorithm being used, such as HMAC SHA256 or RSA.

Payload
The payload contains user detail such as id or roles, remember I said it is advisable not save sensitive data in JWT.

Signature
Signature is a unique key used to verify the authentic owner of the JWT and also to check if the token has not been manipulated. We can use (Rails.application.secrets.secret_key_base)(which is provide by rails) to generate a unique and secure key, but you can also supply your own secret key if you want.

Wahooooo !!!, enough of stories. Lets get straight to work. We will create a simple rails API to demonstrate the use of JWT in rails.

System requirements

Before you start anything, you must have all this things installed on your system

  1. Ruby version > 2.4.0
$ brew install ruby
  1. Rails version 5
$ gem install bundler
$ gem install rails

Visit Ruby on Rails - Installation for more installation details for windows and Mac users.

  1. Postman

So lets get started, to generate our application run the command below in your terminal, ensure you navigate to the directory where you want to keep your project.

$ rails new my_api_app --api -T

Don’t get freaked out with the commands, let me explain what they all mean. rails as you know by now is the the framework we are using , so we use rails to generate migrations, models, controllers, new project(my_api_app), among others. --api command tells rails that we need just  API application. -T excludes Minitest when its generating the application. Minitest is a default testing framework for rails, this doesn’t mean we are not going to write test, the thing is we don’t want to use rails default test suit, rather we will use RSpec to test our application. I will explain more about the RSpec test when we get to that part, for now lets get our app wired up for authentication.

Here are the list of things we will cover.

  • Create a model
  • Set up controller for signup and logging user to the application
  • Encoding and decoding user data with JWT.
  • We need to validate user credentials
  • We need authorization

Create Model

$ cd my_api_app (`to move into root directory of our application`)
$ rails g model User name email password_digest

This command generates User model for us (g means generate) and also a migration file. The content of migration file is what will help us create a table in the database when run our migration command. it also specifies the fields that we added to our model. You see, I told you rails is easy and it makes you happy, it does all the configuration for you.

Migration

$ rails db:migrate

Running this commands automatically creates our table for us in the database.

We need to ensure our password is not saved in plaintext, in other to achieve this we need to tell our model to encrypt the password before saving into the database. Well, in rails we have a 'gem' package that does that for us.

#Gemfile.rb
 gem 'bcrypt', '~> 3.1.7'
$ bundle install

First we add gem package(bcrypt) to Gemfile.rb file. and the command bundle install installs the package to our project. With the gem installed we can use method has_secure_password in our model. This method automatically encrypts the password we send into the database from the controller. This is awesome, yes you can say that again.

#app/models/user.rb
class User < ApplicationRecord
   #Validations
   validates_presence_of :name, :email, :password_digest
   validates :email, uniqueness: true
 
   #encrypt password
   has_secure_password
end

validates_presence_of is a rails validation method which ensures that values for the specified fields are provided. Our model is now ready for connection.

Create controller

We need to create user before we can authenticate.

$ rails g controller Users

Implement controller

# app/controllers/users_controller.rb
class UsersController < ApplicationController

 # POST /register
  def register
    @user = User.create(user_params)
   if @user.save
    response = { message: 'User created successfully'}
    render json: response, status: :created 
   else
    render json: @user.errors, status: :bad
   end 
  end

  private

  def user_params
    params.permit(
      :name,
      :email,
      :password
    )
  end
end

Create routes

#config/routes.rb
Rails.application.routes.draw do
  #add our register route
   post 'auth/register', to: 'users#register'
end

'auth/register’ is our path (url), ‘user#register’ means we are connecting to user controller, then run register action(method)

In other for our us to connect to and get good response from our application, we need to include some contents in application.rb file.

#config/application.rb

module MyApiApp
  class Application < Rails::Application

   # [...]
    #cors configuration
    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*', :headers => :any, :methods => [:get, :post, :options]
      end
    end
    #autoloads lib folder during production
    config.eager_load_paths << Rails.root.join('lib')

    #autoloads lib folder during development
    config.autoload_paths << Rails.root.join('lib')
    # [...]
  end
end

#Gemfile.rb
gem 'rack-cors', :require => 'rack/cors'
$ bundle install

rack-cors gem provides support for Cross-Origin Resource Sharing (CORS) which allows AJAX calls and postman requests to get good response from our application.

Great work, we are ready for the first test.

Run this command to start the application.

$ rails s

Rails runs on default port 3000

Open your postman so the we can test this implantation

  • Select Post request
  • Add the URL http://localhost:3000/auth/register
  • Select x-www-form-urlencoded
  • Fill the fields(key) and the values
  • Click Send

Screen Shot 2017-10-09 at 3.08.59 PM.png
You should have something like

To continue, please stop the server with the command below or open a new terminal and navigate to the root directory of the project

$ ctrl + c

Encoding and decoding user data with JWT.

We can start the implementation of JWT generation now. We need to install jwt gem which makes encoding and decoding of HMACSHA256 tokens available in our Rails application.

#Gemfile.rb
$ gem 'jwt'
$ bundle install

We can make JWT accessible anywhere in our application, to achieve this we can use singleton class. A singleton class is used for wrapping logic and using it in other constructs I.e You can create a singleton class and defining different logics you need in the class, so that when ever you need any of the method(logic) in our application, you call the class dot the name of the method.

Let us add json_web_token.rb file in the lib folder, since we have configured our application to auto-loaded it in development and production

$ touch lib/json_web_token.rb

JWT singleton class

# lib/json_web_token.rb
class JsonWebToken
# our secret key to encode our jwt

  class << self
    def encode(payload, exp = 2.hours.from_now)
      # set token expiration time 
      payload[:exp] = exp.to_i
      
       # this encodes the user data(payload) with our secret key
      JWT.encode(payload, Rails.application.secrets.secret_key_base)
    end

    def decode(token)
      #decodes the token to get user data (payload)
      body = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
      HashWithIndifferentAccess.new body

    # raise custom error to be handled by custom handler
    rescue JWT::ExpiredSignature, JWT::VerificationError => e
      raise ExceptionHandler::ExpiredSignature, e.message
    rescue JWT::DecodeError, JWT::VerificationError => e
      raise ExceptionHandler::DecodeError, e.message
    end
  end
end

We have wrapped our JWT in a singleton class to make encoding and decoding method accessible in our application. The encode method generates our token using payload(user data), expiration time and secret key. The decode method takes in the token as parameter and decodes it with same secret key we used to encode it, if this process fails JWT raise exception error which will handle in the next file we will create.

$ touch app/controllers/concerns/exception_handler.rb
module ExceptionHandler
  extend ActiveSupport::Concern

   # Define custom error subclasses - rescue catches `StandardErrors`
  class AuthenticationError < StandardError; end
  class MissingToken < StandardError; end
  class InvalidToken < StandardError; end
  class ExpiredSignature < StandardError; end
  class DecodeError < StandardError; end
  
  included do
    # Define custom handlers
    rescue_from ActiveRecord::RecordInvalid, with: :four_twenty_two
    rescue_from ExceptionHandler::AuthenticationError, with: :unauthorized_request
    rescue_from ExceptionHandler::MissingToken, with: :four_twenty_two
    rescue_from ExceptionHandler::InvalidToken, with: :four_twenty_two
    rescue_from ExceptionHandler::ExpiredSignature, with: :four_ninety_eight
    rescue_from ExceptionHandler::DecodeError, with: :four_zero_one

    rescue_from ActiveRecord::RecordNotFound do |e|
     render json: { message: e.message }, status: :not_found
    end

    rescue_from ActiveRecord::RecordInvalid do |e|
      render json: { message: e.message }, status: :unprocessable_entity
    end
  end

  private

  # JSON response with message; Status code 422 - unprocessable entity
  def four_twenty_two(e)
   render json: { message: e.message }, status: :unprocessable_entity
  end
 
# JSON response with message; Status code 401 - Unauthorized
  def four_ninety_eight(e)
    render json: { message: e.message }, status: :invalid_token
  end

  # JSON response with message; Status code 401 - Unauthorized
  def four_zero_one(e)
    render json: { message: e.message }, status: :invalid_token
  end

   # JSON response with message; Status code 401 - Unauthorized
  def unauthorized_request(e)
    render json: { message: e.message }, status: :unauthorized
  end
end

This modules handles all exceptions raised in our application. You might be wondering why we put this in the concerns folder, we put any method that we need in multiple controllers in this folder and extend ActiveSupport::Concern is a rails mechanism that performs this wonder. Isn’t that amazing.

Validate Credentials

Now that we have user in our database, we can start authentication. We will need another gem called simple_command. Its serves as an helper for our controllers, Instead of using private controller methods and writing too many if conditions, simple_command helps us to avoid that and simplify our work.

let us install the gem to our project.

#Gemfile.rb
gem 'simple_command'
bundle install

Since we want to create authentication and authorization services I prefer to create a separate folder for them.

# create auth folder to house auth services
$ mkdir app/auth
$ touch app/auth/authenticate_user.rb
# app/auth/authenticate_user.rb
class AuthenticateUser
  prepend SimpleCommand
  attr_accessor :email, :password

  #this is where parameters are taken when the command is called
  def initialize(email, password)
    @email = email
    @password = password
  end
  
  #this is where the result gets returned
  def call
    JsonWebToken.encode(user_id: user.id) if user
  end

  private

  def user
    user = User.find_by_email(email)
    return user if user && user.authenticate(password)

    errors.add :user_authentication, 'Invalid credentials'
    nil
  end
end

This command takes in user's email and password as parameters and initializes a class instance with email and password attributes that are accessible within the class. The private method user checks if the user exist in the database, this method also validates the password user supplies to ensure it matches the encrypted password in the database. If everything is true, a token will be returned. If not, the method will return nil.

User Authorization

Now that we have generated JWT successfully, we can use it to authorize users that are using our application. To authorize users accessing our application, the token created during login must must be sent through the request header. We can attach our token to the 'Authorization' header, this will allow the server to interprete request properly.

Create authorization file

$ touch app/auth/authorize_api_request.rb

Here is the code for authorization

# app/commands/authorize_api_request.rb 

class AuthorizeApiRequest
  prepend SimpleCommand

  def initialize(headers = {})
    @headers = headers
  end

  def call
    user
  end

  private

  attr_reader :headers

  def user
    @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
    @user || errors.add(:token, 'Invalid token') && nil
  end

  def decoded_auth_token
    @decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
  end

  def http_auth_header
    if headers['Authorization'].present?
      return headers['Authorization'].split(' ').last
    else errors.add(:token, 'Missing token')
    end
    nil
  end
end

Hmmmmmmmmmm, this looks like crap right, don’t be afraid. lets go through the code together.
The ||= operator is used to assign @user i.e if there is no value coming in, then use the last value that was saved, so if User.find() returns an empty set or decoded_auth_token returns false@user will be nil. The user method will either return the user if authorization is successful or throw an error if otherwise. Method http_auth_header extracts the token from the authorization header received in the initialization of the class, while the method decoded_auth_token decodes the token received from http_auth_header and retrieves the payload(user's id)

We have implemented all the methods need for our JWT. Now we can create methods in the controller to put our authentication and authorization to work. But first lets create helper method in the application controller which all other controllers will inherit from

Here is the code.

 # app/controllers/authentication_controller.rb

 class ApplicationController < ActionController::API
    before_action :authenticate_request
    attr_reader :current_user
    
    include ExceptionHandler

    # [...]
    private
    def authenticate_request
      @current_user = AuthorizeApiRequest.call(request.headers).result
      render json: { error: 'Not Authorized' }, status: 401 unless @current_user
    end
  end

before_action here is ruby hook that ensures to runs a method before any other method that is being called in the application. Here we want to execute authenticate_request before user access any method in the application, it request for header every time the user makes a request and passes it into AuthorizeApiRequest to authorize the user. If the user is successfully authorize, it return user data and saves in @current_user which will be available to all other controllers. Don't forget we created ExceptionHandler module in the concerns folder, we need to include it here in the application controller in other to make it accessible to other controllers.

lets us create a method to login.

First we need to add the login and a test route to the route.rb file

# config/routes.rb
Rails.application.routes.draw do

  # [...]
  post 'auth/login', to: 'users#login'
  get 'test', to: 'users#test'
 # [...]
end

Login code

# app/controllers/users_controller.rb
class UsersController < ApplicationController
skip_before_action :authenticate_request, only: %i[login register]

 # [...]
  def login
    authenticate params[:email], params[:password]
  end
  def test
    render json: {
          message: 'You have passed authentication and authorization test'
        }
  end
 # [...]

 # [...]
  Private
  def authenticate(email, password)
    command = AuthenticateUser.call(email, password)

    if command.success?
      render json: {
        access_token: command.result,
        message: 'Login Successful'
      }
    else
      render json: { error: command.errors }, status: :unauthorized
    end
   end
 # [...]

end

In our last method, you remember I said authenticate_request runs before any method in the application to authorize users, we do not need authorization for login and register methods because you can not authorize user that has not registered at all or a user that wants to login. skip_before_action helps us to skip the authorization for register and login methods.

Wooooolaaaaaa. Nice job so far. Now it is time to fire up our postman again to test our routes

Start the server

rails s

Remember we created an account early with email: omedale@gmail.com and password: user, you can go back to create with your own email and password.

  • Open your postman
  • Select Post request
  • Add the URL http://localhost:3000/auth/login
  • Select x-www-form-urlencoded
  • Fill the fields(key) and the values
  • Click Send
    Screen Shot 2017-10-08 at 9.05.04 PM.png

Now let us test the protected routes http://localhost:3000/test

  • Copy the token careful
  • Open a new tab or replace the curent url with http://localhost:3000/test
  • Change request button to GET
  • Select header
  • Fill the fields(key) = Authorization and the value = Bearer <token>
  • Click send

Screen Shot 2017-10-08 at 8.53.28 PM.png

Awesomeeeeeeeeeeee

Conclusion

We really covered a lot in this post, we will write test, integrate CI and deploy the app to heroku in our next post. I hope this was helpful. Click here for the full source code.

Discover and read more posts from Oluwafemi Medale
get started