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