Authenticate User with Devise Gem and Devise JWT in React Application (1/2)
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 formatBearer #{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 formatBearer #{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
jti
claim, which uniquely identifies a token, is persisted. Theexp
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.