This content originally appeared on DEV Community and was authored by Ari Karim
Introduction
I know that you might tried a lot of articles and searched a lot and you may be tired, or my article may be long and you may want to jump to the code right away but let me tell you that this article is all you need to build a complete back-end authentication for your website.
But first let me tell you a brief history about authentication API and how it works.
It is this simple steps:
1- User logs in to the website
2- You create a token for the user and send it back.
3- with each request the user perform you send back the token.
Explanation of the above steps:
1-2- When the user logs in or signs up into your website your back-end no matter which program you use(Rails, Node...) will create a token and send it back to the user in the Response headers called authorization.
3- with each request that the user perform in order for the back-end to know that this person is logged in and it is the correct user, we send back the token in the request headers mainly like Authorization: Bearer Your token here, the back-end se the token and if it was the right token, it will allow you to perform your request, pretty simple isn't it? :).
Lets begin.
- First lets create a new project, so run this:
Rails new App-Name --api -d postgresql
then cd inside the project.
we will use devise and devise-jwt and rack-cors gems for authentication, so put these gems inside the Gemfile:
Gem 'devise'
Gem 'devise-jwt'
Gem 'rack-cors'
gem 'dotenv-rails' // for hiding variables
Then run
bundle Install or bundle
config/initializers/cors.rb is generated by default when using the--api to create an app. Otherwise you need to create it.
Update it like this. The key here is allowing all origins to make requests:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', headers: %w(Authorization),
methods: :any,
expose: %w[Authorization]
end
end
Using origins ‘*’ is for our convenience. When deploying to production, set origins to the URL of your front-end app. Otherwise the whole internet will be able to hit your API. Though in some cases that’s desirable.
Lets install Devise
run this command:
Rails g devise:install
Now create a model and migration with devise, and migrate to generate tables.
rails g devise User
rails db:setup
Update the generated User model with the following code:
class User < ApplicationRecord
devise :database_authenticatable,
:jwt_authenticatable,
:registerable,
jwt_revocation_strategy: JwtDenylist
end
Create another model file called jwt_denylist.rb and paste in the following:
class JwtDenylist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Denylist
self.table_name = 'jwt_denylist'
end
The included module was previously called Devise::JWT::RevocationStrategies::Blacklist so you may see that in older tutorials.
Create a migration to go along with it.
$ rails g migration CreateJwtDenylist
Update it to this.
class CreateJwtDenylist < ActiveRecord::Migration[6.1]
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
end
And migrate.
$ rails db:migrate
This sets up a table which tracks JWT tokens that have been logged out and should no longer have access to the app.
Controllers
Create a sessions controller at /controllers/users/sessions_controller.rb. We want to override the default devise sessions controller so we can specify a custom response on log-in and log-out.
class Users::SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(resource, _opts = {})
render json: { message: 'You are logged in.' }, status: :ok
end
def respond_to_on_destroy
log_out_success && return if current_user
log_out_failure
end
def log_out_success
render json: { message: "You are logged out." }, status: :ok
end
def log_out_failure
render json: { message: "Hmm nothing happened."}, status: :unauthorized
end
end
Create a new registration controller, /controllers/users/registrations_controller.rb.
class Users::RegistrationsController < Devise::RegistrationsController
respond_to :json
private
def respond_with(resource, _opts = {})
register_success && return if resource.persisted?
register_failed
end
def register_success
render json: { message: 'Signed up sucessfully.' }
end
def register_failed
render json: { message: "Something went wrong." }
end
end
Add one more controller, controllers/members_controller.rb, so we can test logged-in VS logged-out behaviour on an endpoint that required authenticating.
class MembersController < ApplicationController
before_action :authenticate_user!
def show
render json: { message: "Yeppa you did it" }
end
end
More Devise Setup
Update config/initializers/devise.rb. Add this to the file inside the config block.
config.jwt do |jwt|
jwt.secret = ENV['RAILS-SECRET-KEY']
end
This tells Devise-JWT to use a secret key specified in our credentials file to build tokens.
Now generate a secret key. And note the output. We’ll add this into our .env file, so run:
rake secret
In the root of the project create a .env file and put this:
RAILS-SECRET-KEY = Your secret key from above command
Note: If you publish this website and upload it to heroku, do not forget to add a Config variable to your app in heroku, simply go to heroku, go to your app, click on setting, click on config vars then put your RAILS-SECRET-KEY then your secret key.
Routes
Update your routes so they point to your new controllers, rather than to the default devise controllers.
Rails.application.routes.draw do
devise_for :users,
controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations'
}
get '/member-data', to: 'members#show'
end
Now you will have these end-points:
http//localhost:3000/users/sign_in
Rooute ==> Sign in
Method ==> POST
Body ==> { "user": { "email": "test@example.com", "password": "12345678" } }
Response token ==> data.headers.authorization
http//localhost:3000/users
Route ==> Sign up
Method ==> POST
Body ==> { "user": { "email": "test@example.com", "password": "12345678" } }
Response token ==> data.headers.authorization
http//localhost:3000/member
Route ==> To know if user logged in?
Method ==> GET
headers ==> token: token you saved from log in or sign up user
Response ==> data.data.message=> 'yeppa you did it.'
http//localhost:3000/users/sign_out
Route ==> To know if user logged in?
Method ==> DELETE
headers ==> token: token you saved from log in or sign up user
Response ==> data.data.message=> 'You are logged out.'
Now you can use postman,Insomnia... or any other tool to test the endpoints, just don't forget that except signin and signup you need to send the token in the header like this:
Authorization: Bearer Your token here
You may ask what will happen to the token when the user signs out? do you remember the JWT_denylist model that we created!,
each time the user logs out rails puts his token inside that table and each time you log in and have a request with a token, rails compare your token with the tokens inside the deny_list table if it matches one of them it means you are logged out and you are not authorized.
This content originally appeared on DEV Community and was authored by Ari Karim
Ari Karim | Sciencx (2021-07-25T16:19:33+00:00) React-Rails authentication API with Devise and Devise-jwt(Back-end) part.. Retrieved from https://www.scien.cx/2021/07/25/react-rails-authentication-api-with-devise-and-devise-jwtback-end-part/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.