How I set up JWT Authentication with Rails 5 API + Devise in 10 easy steps
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
enddef 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! :)