How I set up JWT Authentication with Rails 5 API + Devise in 10 easy steps

Nandhagopal Ezhilmaran
4 min readJan 10, 2019

We all know how painful it can be to set up a complete authentication flow all by ourselves even if it’s with our awesome Rails 5.

It’s not just the difficulty, it’s the things we need to cover and be careful about while setting up authentication flow ourselves since it’s going to be ‘The Wall’ (Sorry about the GOT reference :P) protecting our apps.

So, this article is a tutorial on setting up complete authentication flow from scratch with JWT tokens in minutes using devise and devise JWT gems with a Rails 5 API application.

Prerequisites:

Basic understanding of Ruby on Rails and authentication through JSON Web Tokens(JWT).

STEP 1: Create a new Rails API app

We just have to run the following command to create a new Rails 5 application in API only mode. Before that, make sure that you have rails installed in your system.

$ rails new my-app --api --database=postgresql

Here, I have also mentioned that the app should be using postgres instead of its default SQLite db.

STEP 2: Configure Rack Middleware

Now, we have to Rack Middleware for handling Cross-Origin Resource Sharing (CORS), which makes cross-origin AJAX requests possible.

To do that, Just uncomment the gem ‘rack-cors’ line from your generated Gemfile. Then, add these lines to your ‘application.rb’ file.

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 also see that I have mentioned that there should be an “Authorization” header exposed which will later be used to dispatch and receive JWT tokens in Auth headers.

STEP 3: Add the needed Gems

We will be needing devise, devise-jwt gems for the setup of authentication and the dispatch and revocation of JWT tokens. So add these two lines to your Gemfile and then, do a bundle install.

gem 'devise'gem 'devise-jwt', '~> 0.5.8'

STEP 4: Install and configure devise

By running the following command, devise will be installed to our app with basic configuration files and devise routes.

$ 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 }

STEP 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.

(Note: It’s advised to use this devise model only for authentication and to create new models for any other user related functionality)

Run the following command to create model User

$ rails generate devise User

Then run migrations using,

$ rake db:setup

or by,

$ rake db:create
$ rake db:migrate

STEP 6: Create devise controllers and routes

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

We don’t even have to override devise controller methods. We simply need to create these controller under app/controllers and specify that they will be responding to JSON requests. The files will looks like,

class SessionsController < Devise::SessionsController
respond_to :json
end

sessions_controller.rb

class RegistrationsController < Devise::SessionsController
respond_to :json
end

registrations_controller.rb

Then, add the routes aliases to override default routes provided by devise in the routes.rb (You can ignore this if you are okay with the default routes)

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

Now, the endpoints will be set to login, logout and signup.

STEP 7: Configure devise-jwt

Create a rake secret and store in and environment variable by running the following command. (direnv works really well for the usecase)

$ bundle exec rake secret

Add the following lines to devise.rb

config.jwt do |jwt|
jwt.secret = ENV['DEVISE_SECRET_KEY']
jwt.dispatch_requests = [
['POST', %r{^/login$}]
]
jwt.revocation_requests = [
['DELETE', %r{^/logout$}]
]
jwt.expiration_time = 5.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 of each token generated. In this example, it’s 5 minutes.

STEP 8: Set up a revocation strategy

There is a huge debate going on about the need for revocation of tokens since it seems like it entirely beats the purpose of JWT tokens. I suggest you read this blog from the author of the gem.

Here, for the revocation of tokens, we will be using one of the 5 strategies provided out of the box by devise-jwt, called Blacklist strategy.

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 generated model

include Devise::JWT::RevocationStrategies::Blacklistself.table_name = 'jwt_blacklists'

The final model (jwt_blacklist.rb) will look like this

class JwtBlacklist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Blacklist
self.table_name = 'jwt_blacklists'
end

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 the commands mentioned in Step 5 to create jwt_blacklists table.

STEP 9: Add respond_with methods

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

class SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(resource, _opts = {})
render json: resource
end
def respond_to_on_destroy
head :ok
end
end

STEP 10: Yayyy, it’s done

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

before_action :authenticate_user!

A working demo-app can be found here.

Hope it helps someone out there, Feedback are welcome! :)

--

--

Nandhagopal Ezhilmaran

Senior Consultant - Developer | DevOps Enthusiast | Automobile Enthusiast