JWT Auth in a Rails API
In this article we will integrate a JWT authentication from the scratch in a Rails API.
If you are not familiarized with the JWT concept, the official site defines it as:
an open, industry standard RFC 7519 method for representing claims securely between two parties.
Basicamente vamos usar um token para verificar a autenticidade de um usuário para rotas protegidas em nossa API.
We will use a token to verify the authenticity of an user to access protected routes in our API.
You can find more information about JWT in the website.
Going forward, we will create a API Only Rails Project, for that you should run the command below on your terminal:
rails new jwt-auth-example --database=postgresql --api
With our project ready, let's stark working on it.
On your Gemfile, uncomment bcrypt and rack-cors gems. We gonna use them both.
You will also need to add the JWT gem. So add to your Gemfile:
gem 'jwt'
Don't forget to run bundle
on your terminal, to install the new gems.
The CORS initializer needs to be allowed on our project, fortunately it already comes pre-settled, all you need to do is uncomment it. You can find it in config/initializers/cors.rb
Uncommenting the content between the lines 8 and 16, it will looks like this:
# Be sure to restart your server when you modify this file.# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.# Read more: https://github.com/cyu/rack-corsRails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'example.com' resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
Awesome! Now our application allows Cross Origin Resources Sharing, the CORS.
Now let's start working on the routes.
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
post '/signup', to: 'users#signup'
post '/signin', to: 'users#signin'
get '/signedin', to: 'users#signedin'
end
end
end
The /signup route will be used for signup users (obviously 😆). The /signin route will be for signin existent users (obviously again). And finally, the /signedin route will be the one we check if there is a current user session.
With our routes file ready, we will create our User’s model.
rails g model User email:string password_digest:string
The Bcrypt gem we added in the beginning will be responsible for encrypt and ensure the safety of the passwords.
We need to add the Bcrypt’s has_secure_password
method in our User's model. We also will include a validation to make sure that we will haven't duplicated emails.
class User < ApplicationRecord
has_secure_password
validates :email, uniqueness: { case_sensitive: false }
end
Now we need to create our database and run the pending migrations, in this case is only the users table.
rails db:create db:migrate
With the last step ready, we will create the Users controller and add the JWT auth methods to the Application Controller, to finally test our application 🥳.
To create the Users Controller
rails g controller Api::V1::Users
The content should looks like:
module Api
module V1
class UsersController < ApplicationController
skip_before_action :set_auth_header, only: %i[signup signin]
before_action :authorize, only: %i[signedin] def signup
@user = User.create(user_params) if @user.valid?
token = encode_token({ user_id: @user.id })
render json: { user: @user, token: token }
else
render json: "\"Invalid Credentials\"" , status: :unauthorized
end
end def signin
@user = User.find_by(email: user_params[:email]) if @user && @user.authenticate(user_params[:password])
token = encode_token({ user_id: @user.id })
render json: { user: @user, token: token }
else
render json: "\"Invalid Credentials\"" , status: :unauthorized
end
end def signedin
render json: @user
end private
def user_params
params.require(:user).permit(:email, :password)
end
end
end
end
Still working with controllers, now we should define our auth methods in app/controllers/application_controller.rb:
class ApplicationController < ActionController::API
def encode_token(payload)
JWT.encode(payload, 'Secret Key')
end def auth_header
request.headers['Authorization'].split(' ')[1]
end def decoded_token
return unless auth_header.present? token = auth_header
begin
JWT.decode(token, 'Secret Key', true, algorithm: 'HS256')
rescue JWT::DecodeError
nil
end
end def set_auth_header
response.set_header('Auth-Token', auth_header)
end def logged_user
return unless decoded_token.present? id = decoded_token[0]['user_id']
@user = User.find(id)
end def is_logged?
logged_user
end def authorize
return if is_logged? render json: { message: 'You must signin before access this route' }, status: :unauthorized
end
end
Attention: in case you are working on a real project, where we have Secret Key you should replace with a safe key. This key should never be shared in your code, I deeply recommend to add it in the Rails Credentials file or in an ENV file. 😉
With all this stuff done, we are ready to test our authentication! 🥳
Up your local server:
rails s -p 3001
Test in your HTTP client the endpoints below.
Signup
Request: POST
Endpoint:
http://localhost:3001/api/v1/signup
Headers:
Content-Type: application/json
Body:
{
"user": {
"email": "bruno@teste.com",
"password": "123456"
}
}
Signin
Request: POST
Endpoint:
http://localhost:3001/api/v1/signin
Headers:
Content-Type: application/json
Body:
{
"user": {
"email": "bruno@teste.com",
"password": "123456"
}
}
Signedin
Request: GET
Headers:
Authorization: Bearer {{Token retornado na request de signup ou signin}}
We are all done here!!!
The code developed in this post will be available in a public repository on my GitHub: https://github.com/Hey-Feres/JwtAuthExample
Hope this post helped you in some way. 😄
Feel free to place suggestions or questions in the comment section below.