DEV Community

Cover image for JWT Token-based custom user authentication for Rails API only (Part 02)
Sulman Baig
Sulman Baig

Posted on • Originally published at sulmanweb.com

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

The code available at:

GitHub logo sulmanweb / rails-api-user-custom-auth

Rails API user custom authentication using JWT project

This part emphasis is on user sign up services and if signup is ok create a session with JWT token for that user

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')
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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

include AuthenticateRequest
before_action :current_user
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and a method:

def destroy
  current_user.destroy
  success_user_destroy
end

protected

def success_user_destroy
  render status: :no_content, json: {}
end
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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

Results are below:

Alt Text


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!

Discussion (0)