Rails 8 includes its own authentication generator. However, for API-only or RESTful APIs, this authentication scheme is not compatible because APIs are expected to be stateless.
As a result, many developers prefer to modify the default setup to make it stateless and integrate JWT for authentication. One of the key advantages of JWT is its efficiency—it eliminates the need to access the database on each request to authenticate users.
Setting Up the Project
After creating an API-only Rails project with the command:
rails new rails_jwt --api
navigate to the project root directory:
cd rails_jwt
- Add Procfile,
touch Procfile
(optional).
web: bin/rails server -p $PORT
- Create the required gems,
bundle add bcrypt ruby-jwt
. - Create the database,
bin/rails db:create
Building The App
Adding The Model
Adding the model, in this case User model, bin/rails g model User
.
The migration:
class CreateUsers < ActiveRecord::Migration[8.0]
def change
create_table :users do |t|
t.string :email_address, null: false
t.string :password_digest, null: false
t.timestamps
end
end
end
The model:
class User < ApplicationRecord
has_secure_password
validates :email_address, presence: true, uniqueness: true
normalizes :email_address, with: -> (e) { e.strip.downcase }
end
Migrate, bin/rails db:migrate
. It will create the users table.
Adding the Authentication Module
app/controllers/concern/Authentication.rb
module Authentication
extend ActiveSupport::Concern
included do
before_action :authenticate
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :authenticate, options
end
end
def encode(payload)
now = Time.now.to_i
JWT.encode(
{
data: {
id: payload.id,
email_address: payload.email_address
},
exp: now + 3.minutes.to_i,
iat: now,
iss: "rails_jwt_api",
aud: "rails_jwt_client",
sub: "User",
jti: SecureRandom.uuid,
nbf: now + 1.second.to_i,
},
Rails.application.credentials.jwt_secret,
"HS256",
{
typ: "JWT",
alg: "HS256"
})
end
def decode
token = get_token
JWT.decode(token, Rails.application.credentials.jwt_secret, 'HS256')
end
private
def get_token
request.headers["Authorization"].split(" ").last
end
def current_user
decoded = decode
decoded.first["data"]
end
def authenticate
begin
if current_user
current_user
else
render json: { error: "Unauthorized" }, status: :unauthorized
end
rescue JWT::ExpiredSignature
render json: { error: "Token has expired" }, status: :unauthorized
end
end
end
Here, the authenticate function will handle error thrown when the token is already expired, and respond with 401.
Next, let's add jwt_secret into our app credentials, since it is required to encode and decode the JWT. Without it, the JWT.encode and JWT.decode will throw an error.
Adding the JWT Secret
in the console, run EDITOR=vim bin/rails credentials:edit
, and add the jwt_secret, e.g,
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 32b1ae8aa8953eba641519aba932dc5edf03fe1c9d5b12ca379d7eabe69fde9f4da02717159f2d69ea2ad47eb467edadb40c45160822a2bfe5d55a3ace8fd096
jwt_secret: jwt_secret_jwt_secret
Make the Authentication Available
Add them into ApplicationController,
class ApplicationController < ActionController::API
include Authentication
end
The API Endpoint
Let's create the AuthController, bin/rails g controller V1/Auth,
module V1
class AuthController < ApplicationController
allow_unauthenticated_access only: [:create]
def create
@current_user = User.find_by(email_address: params[:email_address])
if @current_user && @current_user.authenticate(params[:password])
encoded_token = encode(@current_user)
render json: { token: encoded_token }, status: :ok
else
render json: { error: "Invalid email or password" }, status: :unauthorized
end
end
end
end
Now, let's add the route in app/config/routes.rb, so that we will have POST /v1/auth
endpoint.
Rails.application.routes.draw do
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
namespace :v1 do
resources :auth, only: [:create]
end
get "up" => "rails/health#show", as: :rails_health_check
# Defines the root path route ("/")
# root "posts#index"
end
We can test them by:
curl -X POST "http://localhost:5000/v1/auth" -H "Content-Type: application/json" -d "{\"email_address\": \"two@example.com\", \"password\": \"password\"}"
we should get the token in the response body:
{"token":"<AUTH_TOKEN>"}
Conclusion
In this first part, we explored how to implement JWT authentication in a Rails application. However, some functions, such as decode and current_user from the Authentication module, have not yet been fully covered. We will address these and other features in the next part.
Top comments (0)