Techcompose

This article is all about authentication in rails 6 using devise and devise-jwt with fast_jsonapi response.

Fast_jsonapi

A lightning fast JSON:API serializer for Ruby Objects. It is better in performance compared to Active Model Serializer.

Devise and JWT

Devise-jwt is a devise extension which uses JSON Web Tokens(JWT) for user authentication. With JSON Web Tokens (JWT), rather than using a cookie, a token is added to the request headers themselves (rather than stored/retrieved as a cookie). This isn’t performed automatically by the browser (as with cookies), but typically will be handled by a front-end framework as part of an AJAX call.

1. Create a new Rails API app

In this step, We need to create a rails application with api_only mode with optional database params(If you want to change).

				
					$ rails new test-app –api –database=postgresql
				
			

Here, I have created a rails 6 application using postgresql (Default SQLite).
(Note: If you are using postgresql then you have to setup database.yml)

2. Configure Rack Middleware

As this is an API Only application, we have to handle ajax requests. So for that, we have to Rack Middleware for handling Cross-Origin Resource Sharing (CORS)

To do that, Just uncomment the “gem ‘rack-cors’” line from your generated Gemfile. And add the following lines to application.rb.

EDITOR

				
					config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource(
      '*',
      headers: :any,
      expose: ["Authorization"],
      methods: [:get, :patch, :put, :delete, :post, :options, :show]
    )
  end
end
				
			

Here, we can see that there should be an “Authorization” header exposed which will be used to dispatch and receive JWT tokens in Auth headers.

3. Add the needed Gems

Here, we are going to add gem like ‘devise’ and ‘devise-jwt’ for authentication and the dispatch and revocation of JWT tokens and ‘fast_jsonapi’ gem for json response.

				
					gem 'devise'
gem 'devise-jwt'
gem 'fast_jsonapi'
				
			

Then, do ‘bundle install’

4. Configure devise

By running the following command to run a generator

				
					$ rails generate devise:install
				
			

It is important to set our navigational formats to empty in the generated devise.rb by adding the following line since it’s an api only app.

				
					config.navigational_formats = []
				
			

Also, add the following line to config/environments/development.rb

				
					config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
				
			

5. Create User model

You can create a devise model to represent a user. It can be named as anything. So, I’m gonna be going ahead with User. Run the following command to create User model.

				
					$ rails generate devise User
				
			

Then run migrations using,

				
					$ rake db:setup
				
			

or by,

				
					$ rake db:create
				
			
				
					$ rake db:migrate
				
			

6. Create devise controllers and routes

We need to create two controllers (sessions, registrations) to handle sign ups and sign ins. By,

				
					rails g devise:controllers users -c sessions registrations
				
			

specify that they will be responding to JSON requests. The files will looks like,

				
					class Users::SessionsController < Devise::SessionsController
  respond_to :json
end
				
			
				
					class Users::RegistrationsController < Devise::SessionsController
  respond_to :json
end
				
			

Then, add the routes aliases to override default routes provided by devise in the routes.rb

				
					Rails.application.routes.draw do
  devise_for :users, path: '', path_names: {
    sign_in: 'login',
    sign_out: 'logout',
    registration: 'signup'
  },
  controllers: {
    sessions: 'users/sessions',
    registrations: 'users/registrations'
  }
end
				
			

7. Configure devise-jwt

Create a rake secret by running the following command.

				
					$ bundle exec rake secret

				
			

Add the following lines to devise.rb

				
					config.jwt do |jwt|
  jwt.secret = GENERATED_SECRET_KEY
  jwt.dispatch_requests = [
    ['POST', %r{^/login$}]
  ]
  jwt.revocation_requests = [
    ['DELETE', %r{^/logout$}]
  ]
  jwt.expiration_time = 30.minutes.to_i
end
				
			

Here, we are just specifying that on every post request to login call, append JWT token to Authorization header as “Bearer” + token when there’s a successful response sent back and on a delete call to logout endpoint, the token should be revoked.

The jwt.expiration_time sets the expiration time for the generated token. In this example, it’s 30 minutes.

8. Set up a revocation strategy

Revocation of token is conflicting with the main purpose of JWT token. Still devise-jwt comes with three revocation strategies out of the box. Some of them are implementations of what is discussed in the blog post JWT Revocation Strategies

Here, for the revocation of tokens, we will be using one of the 3 strategies.

Create a jwt_blacklist model by the following command

				
					$ rails g model jwt_blacklist jti:string:index exp:datetime
				
			

Add these two lines to the “jwt_blacklist.rb

				
					include Devise::JWT::RevocationStrategies::Blacklist
self.table_name = 'jwt_blacklists'
				
			

Add these two options to your devise User model to specify that the model will be jwt authenticatable and will be using the blacklist model we just created for revocation.

				
					:jwt_authenticatable, jwt_revocation_strategy: JwtBlacklist
				
			

The final user model will look like this

				
					class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
  :recoverable, :rememberable, :validatable,
  :jwt_authenticatable, jwt_revocation_strategy: JwtBlacklist
end
				
			

Now run migrations using “rails db:migrate”

9. Add respond_with using fast_jsonapi method

As we already added a fast_jsonapi gem. For json response for user data, we have to create a user serializer. By following command,

				
					$ rails generate serializer user
				
			

It will create a serializer with predefined structure.Now, we have to add the attributes which we have to set as a user response. So I have added user’s id, email and created_at.So the final version of user_serializer.rb

				
					class UserSerializer
  include FastJsonapi::ObjectSerializer
  attributes :id, :email, :created_at
end
				
			

We can access serializer data for single record by,

UserSerializer.new(resource).serializable_hash[:data][:attributes]
And multiple records by,
UserSerializer.new(resource).serializable_hash[:data].map{|data| data[:attributes]}

Now, we have to tell devise to communicate through JSON by adding these methods in the RegistrationsController and SessionsController

				
					class Users::RegistrationsController < Devise::SessionsController
  respond_to :json
  private

  def respond_with(resource, _opts = { + })
    render json: {
      status: {code: 200, message: 'Logged in successfully.'},
      data: UserSerializer.new(resource).serializable_hash[:data][:attributes]
    }
  end
end
				
			
				
					class Users::SessionsController < Devise::SessionsController
  respond_to :json
  private

  def respond_with(resource, _opts = { + })
    render json: {
      status: {code: 200, message: 'Logged in successfully.'},
      data: UserSerializer.new(resource).serializable_hash[:data][:attributes]
    }
  end

  def respond_to_on_destroy
    head :ok
  end
end
				
			

You can modify the column name and data format by overwrite attribute:

attribute :created_date do |user|
user.created_at.strftime(‘%d/%m/%Y’)
end

Here, I have changed created_at attribute’s column name and its format.

Here you can get detailed information on fast_jsonapi.

10. Finally, it’s done

Now you can add the following line in any controller to authenticate your user.

before_action :authenticate_user!

If you are looking to develop any project on Ruby on Rails then choose us as we are one of the leading Ruby on Rails Development Company that provides quality Ruby on Rails development services. Contact us to hire Ruby on Rails developers for your Ruby on Rails requirement or you can reach us at info@techcompose.com

Share On: