Building API in Rails - Part 5

Hi there, welcome to the fifth part of Building API with Rails series. In this part we are going to secure our API application by adding token based authentication for which we will be using JWT jwt in ruby on rails

Adding the jwt gem

The first thing we need to do is add the jwt gem and do bundle install

# for authentication
gem 'jwt'

Adding a secret key

In order to decode and encode the jwt we will be needing an secret or private key. We can easily generate a random secret key in the terminal by using rails secret

rails secret

Copy the generated secret key and add it to the .env file.

JWT_SECRET_KEY='paste_your_secret_key'

Adding the decode and encode functionality

Let’s create a file called json_web_token.rb in our model’s directory.

In this file, we need to create a class called as JsonWebToken and inside the class, we need to add two class methods one for encoding the JWT and another for decoding it. After adding it our file will look like this.

# frozen_string_literal: true

# class for authentication with jwt
class JsonWebToken
  JWT_SECRET_KEY = ENV['JWT_SECRET_KEY']
  class << self
    def encode(payload)
      expiration = 7.days.from_now.to_i
      JWT.encode(payload.merge(exp: expiration), JWT_SECRET_KEY, 'HS256')
    end

    def decode(token)
      JWT.decode(token, JWT_SECRET_KEY, true, algorithm: 'HS256').first
    end
  end
end

In order to encode and decode the JWT, we have used the jwt secret key that we had added in the .env file. Also, we have added the expiration time as 7 days from the date of creation of the JWT.

Adding the sessions controller

We need a route for users to login in our application. We can create a sessions controller and add the login actions inside the controller.

# frozen_string_literal: true

module Api
  module V1
    class SessionsController < BaseController
      # POST /api/v1/login
      def login
      end
    end
  end
end

And also we need to add the routes as well.

# frozen_string_literal: true

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :users
      post 'login', to: 'sessions#login'
    end
  end
end

Adding test cases

Let’s write the test case for the sessions controller and make it pass. Inside the request/api/v1 folder we need a file named as sessions_spec.rb. For easily creating it we can use the rails generator

rails g rspec:request api/v1/sessions

Now, when the user login to our application with their correct credentials then the test should expect a 200 status. Let’s add that to our test case and try to pass it.

require 'rails_helper'

RSpec.describe 'Api::V1::Sessions', type: :request do
  describe 'POST /api/v1/sessions' do
    it 'lets user login with correct credentials' do
      user = create(:user)
      request_body = {
        user: { email: user.email, password: user.password }
      }
      post api_v1_login_path, params: request_body
      expect(response).to have_http_status(200)
    end
  end
end

Since we haven’t added anything in our login action the test will fail if we run it. So lets add some code that checks the users email and password. You can also add negative test cases as well i.e for invalid email and passwords.

# frozen_string_literal: true

module Api
  module V1
    class SessionsController < BaseController
      # POST /api/v1/login
      def login
        user = User.find_by(email: login_params[:email])
        if user.present? && user.authenticate(login_params[:password])
          render jsonapi: user, status: :ok, code: '200'
        else
          render jsonapi_errors: [{ title: 'Invalid Email or Password' }],
                 code: '401', status: :unauthorized
        end
      end

      private
        def login_params
          params.require(:user).permit(:email, :password)
        end
    end
  end
end

Here I am using the bcrypt authenticate method to authenticate the password. Now if we go and run the test it will pass :tada: :tada:

Sending the encoded JWT in the reponse

In the above user login response we need to send the encoded JWT as well. So let’s use the encode class method that we had created in json_web_token.rb. To encode the token we can simply use this

token = JsonWebToken.encode(user_id: user.id)

To send this as a response we can add an attribute as auth_token in the user serializer

class UserSerializer
  include JSONAPI::Serializer
  attributes :fullname, :gender, :email

  attribute :auth_token do |user, params|
    params[:auth_token]
  end
end

Finally, we need to change the login action in the sessions_controller.rb which will look something like this now

# POST /api/v1/login
def login
  user = User.find_by(email: login_params[:email])
  if user.present? && user.authenticate(login_params[:password])
    token = JsonWebToken.encode(user_id: user.id)
    render jsonapi: user, params: { auth_token: token },
            status: :ok, code: '200'
  else
    render jsonapi_errors: [{ title: 'Invalid Email or Password' }],
            code: '401', status: :unauthorized
  end
end

Let’s add another example in test case that expects an auth token after users login.

  it 'expects auth token in response' do
    user = create(:user)
    request_body = {
      user: { email: user.email, password: user.password }
    }
    post api_v1_login_path, params: request_body
    result = JSON.parse(response.body)
    token = JsonWebToken.encode(user_id: user.id)
    expect(result["data"]["attributes"]["auth_token"]).to eq(token)
    expect(response).to have_http_status(200)
  end

If we run the test cases it will pass :tada: :tada:

Authenticating the request with jwt

Great we have added the jwt successfully and send back the auth token as a response. But we are still not done yet. We need to add a functionality that authenticate the users who request for the protected API resources.

Let’s add a method to change users password in users_controller.rb

# UPDATE api/v1/change_password
def change_password; end

Add the routes as well

patch 'change_password', to: 'users#change_password'

Obiviously, before user changes their password we must authenticate the request first and then only allow them to change it. Let’s add a action callback method authenticate_request! that will be called before the change_password action.

# frozen_string_literal: true

module Api
  module V1
    class UsersController < BaseController
      before_action :authenticate_request!, only: [:change_password]
      ...

Add the code for authenticating the user in our parent class base_controller.rb

# frozen_string_literal: true

module Api
  module V1
    class BaseController < ApplicationController

      def authenticate_request!
        @decoded = JsonWebToken.decode(auth_token).deep_symbolize_keys
        set_current_user
      rescue JWT::ExpiredSignature
        render jsonapi_errors: [{ title: e.message }], code: '401', status: :unauthorized
      rescue JWT::DecodeError => e
        render jsonapi_errors: [{ title: e.message }], code: '401', status: :unauthorized
      end

      def set_current_user
        if @decoded[:user_id].present?
          @current_user = User.find(@decoded[:user_id])
        end
      end

      def auth_token
        @auth_token ||= request.headers.fetch('Authorization', '').split(' ').last
      end
    end
  end
end

The first thing we have done here is fetch the auth_token from the request headers that is sent by the client.

Request Headers
Authorization: Bearer <token>

After that we have decode the auth token with the def decode action that we had created in json_web_token.rb. While decoding if the token is invalid or expired it will raise an exception according to it, else it will set a current_user and goes to the API action for further process.

Add test case for change_password

We have added the action change_password and then before action callback to authenticate the user request. Let’s check if it works by adding a test case for it and passing it.

In the same users_spec.rb we can add an example like this

  ...
  describe 'PATCH api/v1/change_password' do
    it 'changes user password' do
      user = create(:user)
      request_body = { user: {password: 'new_password'} }
      token = JsonWebToken.encode(user_id: user.id)
      patch api_v1_change_password_path, params: request_body, headers: {'Authorization': token}
      expect(response).to have_http_status(200)
    end
  end
  ...

Then to pass it we can update the change_password action as

  ...
  # UPDATE api/v1/change_password
  def change_password
    if params[:user][:password] && @current_user.update(password: params[:user][:password])
      render jsonapi: @current_user, status: :ok, code: '200'
    else
      render jsonapi_errors: @current_user.errors, status: :unprocessable_entity, code: '422'
    end
  end
  ...

Simillary, lets add a negative test case with invalid auth token.

  ...
  it 'doenot change password for invalid token' do
    user = create(:user)
    request_body = { user: {password: 'new_password'} }
    token = JsonWebToken.encode(user_id: user.id)
    token = '123213'
    patch api_v1_change_password_path, params: request_body, headers: {'Authorization': token}
    expect(response).to have_http_status(401)
  end
  ...

If we run the test case it will pass. Nice ​:muscle: we are finally done with addding jwt in our application and using it to authenticate a user.

Great :tada: :tada: we have come to the end of the part 5 and thanks for reading till the end. The code base is available here. :beers:

Please feel free to give your feedback on the comment section below or ping me at or . Have a great time :smiley_cat:

Building API in Rails - Part 5