JWT Token-based custom user authentication for Rails API only (Part 02)

JWT Token-based custom user authentication for Rails API only (Part 01)
Tutorial explains how to create custom user authentication system for rails API only project with JWT token including email confirmation, reset email, and social logins
PART 01

The code available at:

sulmanweb/rails-api-user-custom-auth
Rails API user custom authentication using JWT project - sulmanweb/rails-api-user-custom-auth
GITHUB Repo
This part emphasis is on user sign up services and if signup is ok create a session with JWT token for that user and user can delete itself from the system.

Gems to be installed:

  • jbuilder - json template engine
  • jwt - encoding and decoding jwt oauth
  • figaro - for environment variables

JSON Web Token library:

Create a library for encoding and decoding of the token to be sent to client for authentication.

To eager-load all libraries on load in config/application.rb enter line:

config.eager_load_paths << Rails.root.join('lib')

Now create a library in lib/json_web_token folder of the project:

class JsonWebToken
  require 'jwt'
  SECRET_KEY = ENV['JWT_SECRET']
  JWT_EXPIRY = 1.day

  def self.encode(payload, exp = JWT_EXPIRY.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET_KEY, 'HS512')
  end

  def self.decode(token)
    decoded = JWT.decode(token, SECRET_KEY, true, {algorithm: 'HS512'})[0]
    res = HashWithIndifferentAccess.new decoded
    if Time.at(res[:exp]) > Time.now
      res
    else
      nil
    end
  rescue
    return nil
  end
end

SECRET_KEY could be the key on which encryption of the payload will be done. encode method will encode the payload and returns the encoded string. decode method decodes the given string based on secret key and return data or nil if data is un—decodable.

Session Create Concern:

Now create a concern in controllers directory for session create so that we can call session wherever we want to call it in controllers. So create file app/controllers/concerns/create_session.rb:

module CreateSession
  extend ActiveSupport::Concern
  require 'json_web_token'

  def jwt_session_create user_id
    user = User.find_by(id: user_id)
    session = user.sessions.build
    if user && session.save
      return JsonWebToken.encode({user_id: user_id, token: session.token})
    else
      return nil
    end
  end
end 

Concerns are a great way to DRY the code, but some people suggest don’t use and some suggest you use. More on concerns are available at https://blog.appsignal.com/2020/09/16/rails-concers-to-concern-or-not-to-concern.html.

jwt_session_create method gets user ID and create a session in sessions table and encoded using our library the token and ID of the user and returns JWT token to us.

Registrations Controller:

Now we create the path /auth/sign_up using registrations controller. Create a new directory app/controllers/auth and new file registrations_controller.rb in newly created directory with data:

class Auth::RegistrationsController < ApplicationController
  include CreateSession

  def create
    @user = User.new(registration_params)

    if @user.save
      @token = jwt_session_create @user.id
      if @token
        @token = "Bearer #{@token}"
        return success_user_created
      else
        return error_token_create
      end
    else
      error_user_save
    end
  end

  protected

  def success_user_created
    response.headers['Authorization'] = "Bearer #{@token}"
    render status: :created, template: "auth/auth"
  end

  def error_token_create
    render status: :unprocessable_entity, json: { errors: [I18n.t('errors.controllers.auth.token_not_created')] }
  end

  def error_user_save
    render status: :unprocessable_entity, json: { errors: @user.errors.full_messages }
  end

  private

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

create method gets user’s name, email, and password and tries to create user and if user is created then creates a session and return the token in headers and body with user data.

Now sending this token back in headers while request, user can authenticate itself for that request. We work on authentication next.

Meanwhile, add to config/routes.rb file to give the controller a path of POST request:

namespace :auth do
  post "sign_up", to: "registrations#create"
end

Now send a POST request with email, password, and name to http://localhost:3000/auth/sign_up. The result is shown below:

Sign Up Request Result

An authenticated User can delete itself:

Now we need to create another concern that checks the token from the headers and verifies the token if user is valid or not. So create a file app/controllers/concerns/authenticate_request.rb with following data:

module AuthenticateRequest
  extend ActiveSupport::Concern
  require 'json_web_token'

  def authenticate_user
    return render status: :unauthorized, json: {errors: [I18n.t('errors.controllers.auth.unauthenticated')]} unless current_user
  end

  def current_user
    @current_user = nil
    if decoded_token
      data = decoded_token
      user = User.find_by(id: data[:user_id])
      session = Session.search(data[:user_id], data[:token])
      if user && session && !session.is_late?
        session.used
        @current_user ||= user
      end
    end
  end

  def decoded_token
    header = request.headers['Authorization']
    header = header.split(' ').last if header
    if header
      begin
        @decoded_token ||= JsonWebToken.decode(header)
      rescue Error => e
        return render json: {errors: [e.message]}, status: :unauthorized
      end
    end
  end
end

In the app/controllers/application_controller.rb add the following two lines:

include AuthenticateRequest
before_action :current_user

Now, before every action whether it needs to authenticate or not, current_user method will be called. This method checks whether authorisation token present or not. If token present, this method will decode the token and verifies the user and returns the authenticated user to the current user method which can now be accessed in all requests.

Also, now if we enter authenticate_user in any controllers before action if valid token is not passed then there will be an error of 401.

Now, we create destroy user method. In registrations controller add line in top:

before_action :authenticate_user, only: :destroy

and a method:

def destroy
  current_user.destroy
  success_user_destroy
end

protected

def success_user_destroy
  render status: :no_content, json: {}
end

So now first user will be authenticated based on token and then that user will be destroyed. If user isn’t valid or token is absent then 401 is returned.

Add to routes in auth namespace:

delete "destroy", to: "registrations#destroy"

So, Now try delete operation on http://localhost:3000/auth/destroy with Authorization header with returned token.

Results are below:

Delete action on user self

So, we can now successfully create a user with session in JWT with authentication system. In the next part I will demonstrate sign in and out and confirmation and reset password system in detail.

Happy Coding!