Authenticate User with Devise Gem and Devise JWT in React Application (1/2)

Villy Siu
4 min readSep 14, 2022

--

As I was learning Rails on Ruby, i discovered the power and ease of use of Devise gem in user authentication. I started to look up how to use it with React frontend. I hope this tutorial will help you too.

Here is the github link to the application if you want to check it out youself, or clone it and apply it to your application.

How does this work?

According to Devise-JWT documentation,

A user authenticates through Devise create session request (for example, using the standard :database_authenticatable module).

If the authentication succeeds, a JWT token is dispatched to the client in the Authorization response header, with format Bearer #{token} (tokens are also dispatched on a successful sign up).

The client can use this token to authenticate following requests for the same user, providing it in the Authorization request header, also with format Bearer #{token}

When the client visits Devise destroy session request, the token is revoked.

Creating Rails App

To begin, create the a folder called react-user-authenitication. Open the folder in your terminal. Then create a rails application in api mode

$ rails new api --api
$ cd api

In the gem file , uncomment

# api/Gemfile
gem 'rack-cors'

Since we are in gemfile, add the other gems as well

gem 'devise'
gem 'devise-jwt'
# 'dotenv-rails' is for storing secret key in ENV file
gem 'dotenv-rails', groups: [:development, :test]

Then, do

bundle install

Setup CORS

As we are making cross-origin requests, from React frontend to rails API via fetch, we will allow all origins (*) to make request.

We will also be passing JWT token through headers, so we add “Authorization” to the list of allowed request headers and exposed response headers .

Next, copy and paste the contents below toconfig/initialzers/cors.rb.

# api/config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3001'
resource '*',
headers: ["Authorization"],
expose: ["Authorization"],
methods: [:get, :post, :put, :patch, :delete, :options, :head],
max_age: 600
end
end

Create a Private controller

rails generate controller private test

Replace private_controller.rb content with the following.

# api/app/controllers/private_controller.rb
class PrivateController < ApplicationController
before_action :authenticate_user!
def test
render json: {
message: "This is a secret message. You are seeing it because you have successfully logged in."
}
end
end

Setting up Devise

rails generate devise:install

Defined default url options in your environments files

# api/config/environments/development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

To prevent devise from using flash messages which are not presented in Rails api mode. Add this to devise.rb

# api/config/initializers/devise.rb
config.navigational_formats = []

Create Devise User model

rails generate devise User
rails db:create
rails db:migrate

Configure controllers and routes

Create registrations and sessions controllers to handle sign ups and logins.

$ rails g devise:controllers users -c registrations sessions

Replace contents

# app/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
respond_to :json
def create
build_resource(sign_up_params)
resource.save
sign_in(resource_name, resource)
render json: resource
end
end
# app/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(resource, _opts = {})
render json: resource
end
def respond_to_on_destroy
render json: { message: "Logged out." }
end
end

Update devise routes.

Replace the contents with the following.

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

Secret key configuration

Secret key is used to create JWT token. Generate secret key by running

bundle exec rake secret

Create a .env file in the project root and add the secret key inside.

# api/.env
DEVISE_JWT_SECRET_KEY=<your_rake_secret>

Note: Add .env into the gitignore file if pushing to GitHub.

Configure devise-jwt

Add the following to devise.rb

# api/config/initializers/devise.rbconfig.jwt do |jwt|
jwt.secret = ENV['DEVISE_JWT_SECRET_KEY']
jwt.dispatch_requests = [
['POST', %r{^/login$}],
]
jwt.revocation_requests = [
['DELETE', %r{^/logout$}]
]
jwt.expiration_time = 5.minutes.to_i
end

If the frontend request to login suceeds, a new JWT token, generated based on the secret key, is dispatched to the client in the Authorization response header, in Bearer #{token} . On a successful sign up, a sign in call is made in the controller. Frontend will receive JWT token in the header too.

If a logout request is made, upon successful destroy, the token will be revoked.

Revocation strategies

Devise-jwt comes with three revocation strategies. We will use Denylist. According to the devise-jwt documentation,

In this strategy, a database table is used as a list of revoked JWT tokens. The jticlaim, which uniquely identifies a token, is persisted. The exp claim is also stored to allow the clean-up of stale tokens.

Create the denylist table

rails generate migration create_jwt_denylist

Replace the content with the following:

# api/db/migrate/[date]_create_jwt_denylist.rb
def change
create_table :jwt_denylist do |t|
t.string :jti, null: false
t.datetime :exp, null: false
end
add_index :jwt_denylist, :jti
end

Then run

rails db:migrate

Create a JwtDenylist model and replace the content to:

# api/app/models/jwt_denylist.rb
class JwtDenylist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Denylist
self.table_name = 'jwt_denylist'
end

Update user model to use JWT token.

# api/app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
end

Now if we run

rails s

and visit

http://localhost:3000/private/test

You will get the message “You need to sign in or sign up before continuing.” Our backend is completed, we will discuss the React frontend in the next post.

Next we will work on the react frontend, see you in part 2.

--

--