Devise authentication for the Rails 7 API application

by Dominik Šipić, November 07, 2022

Introduction


Today, I will show you how to use Devise and Devise JWT gems for authentication. 
Devise is a user authentication system specifically made for Rails and it has got many benefits:

  • Very secure
  • Flexible and open-source
  • Efficient and can be really fast implemented.
  • Comes with many already built-in features, helpers, and useful methods
  • Allow you to have multiple models signed in at the same time

It’s also a complete MVC solution based on Rails engines, but today I will guide you step-by-step on how we can properly configure it to work for API-based Rails applications. 
Acta non-Verba - enough speaking, let’s get to work!

Rails new project


IMPORTANT NOTE: By the time of writing this blog the actual Rails version is “7.0.4” and the Ruby version is “3.0.2”.

Let’s make a new Rails application from scratch with those two flags:
$ rails new AppName  --api --database=postgresql
 
then run $ rails db:create  

Devise gem installation and configuration for the API-based application.


Add the following line to your #Gemfile
  gem 'devise'                                                                                                                                 
Then run:  $ bundle install 
Next, you need to run the generator: $ rails generate devise: install
Once the generator has been successfully installed Devise, we still have to make small tweaks related to the configuration. As suggested after installation, you should configure the default URL for ActionMailer. You can do it by adding the following line to the #conifg/environments/development.rb 
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }   
In this tutorial, I will take the User model as an example. We can easily generate a User model with the following command: $ rails generate devise User then run $ rails db:migrate
In order to finish the configuration for API-based applications we still need to do some more tweaks.
In the  #app/controllers/application_controller.rb you have to add two lines:

respond_to :json
include ActionController::MimeResponds
                                   
Also in the #config/routes.rb for Devise routes set .json format as a default format with this line:
devise_for :users, defaults: { format: :json }        
                      
                

Devise-JWT gem installation and configuration


devise-jwt is a Devise extension that uses JWT tokens for user authentication. It follows the secure by default principle. devise-jwt is just a thin layer on top of warden-jwt_auth that configures it to be used out of the box with Devise and Rails.

Installation is pretty straightforward, add those lines to your application's #Gemfile
gem 'devise-jwt'
gem 'rack-cors'

Then as usual execute  $ bundle
First thing after an installation we need to generate the devise_jwt_secret key we can do it by running the following command in the terminal: $ bundle exec rake secret then paste it somewhere temporarily and open Rails credentials with nano editor:  $  EDITOR="nano --wait" bin/rails credentials: edit

At the bottom of the file add devise_jwt_secret: <Here goes the key that you have previously generated>
Save the file with control + y then exit with control + x (I am using macOS, if you are using another operating system it may be different).
When it comes to revocation strategy, there are three main strategies. Those are Denylist, Allowlist, and JTI Matcher and you can even make your own custom strategy. I am not going to dive deep into each one of them. For this showcase, I will be using my favorite - the JTI Matcher revocation strategy. It works like the following:
When a token is dispatched to a user, the JTI claim is taken from the JTI column in the model (which was initialized when the record had been created).
At every authenticated action, the incoming token JTI claim is matched against the JTI column for that user. Authentication only succeeds if they are the same.
When the user requests to sign out, its JTI column changes, so that the provided token won't be valid anymore.
In order to use it, you need to add the JTI column to the user model. So, first, we need to generate a migration, we can do it via terminal with: $ rails g migration AddJtiToUsers

It will generate a file with the following path: 
#db/migrate/2022XXXXXX_add_jti_to_the_users.rb
Inside that file paste the following code:

   add_column :users, :jti, :string, null: false
   add_index : users, :jti, unique: true
   
!!! Don’t forget to run $ rails db:migrate immediately after.

In the User model, we have to include the JTI Matcher revocation strategy and its corresponding devise modules:

 include Devise::JWT::RevocationStrategies::JTIMatcher
 devise :database_authenticatable, :registerable,
        :recoverable, :rememberable, :validatable,
        :jwt_authenticatable, jwt_revocation_strategy: self

In the #config/initializers/cors.rb you will find everything commented out, delete everything and replace it with something like this:

Rails.application.config.middleware.insert_before 0, Rack::Cors do 
  allow do
   # origins '*'
   origins ['your-frontend-url']
   resource '*',
            headers: :any,
            expose: %w(Authorization),
            methods: %i[get post put patch delete head options]
  end
end

 
We are almost done! The last thing left to do is add some configuration to the #config/initializers/devise.rb

  config.jwt do |jwt|
   jwt.secret = ENV['DEVISE_JWT_SECRET_KEY']
   jwt.dispatch_requests = [
     ['POST', %r{users/sign_in}]
   ]
   jwt.revocation_requests = [
     ['DELETE', %r{users/sign_out}]
   ]
   jwt.expiration_time = 15.day.to_i
 
   jwt.request_formats = {
     user: [:json]
   }
 end


WARNING!!! Do NOT use regex patterns from the official  GitHub page of the gem, because most likely they won’t work properly.

Conclusion


When it comes to a complete MVC Rails engine solution or a Rails API-based application, Devise is certainly the best option. As you have seen, there is just a little bit of proper configuration and everything works behind the scenes. I hope that my code reaches everyone who is struggling with the devise-jwt gem for Rails API-based applications.

If you are interested to learn how to create user-specific channels in a Rails API-based application that use Devise gem for authentication, check out the blog of my colleague: https://www.thespian.hr/blog/how-to-create-user-specific-channels-in-a-rails-api-that-uses-devise




About the author:
Author avatar
Dominik Šipić
Backend Developer
"When it feels scary to jump, that's exactly when you jump. Otherwise you end up staying in the same place your whole life. And that I can't do."
Need help with Devise authentication for the Rails 7 API application? Contact us!