DEV Community

Cover image for [Part 1] Rails 8 Authentication but with JWT
ruwhan
ruwhan

Posted on

[Part 1] Rails 8 Authentication but with JWT

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
Enter fullscreen mode Exit fullscreen mode

navigate to the project root directory:

cd rails_jwt
Enter fullscreen mode Exit fullscreen mode
  • Add Procfile, touch Procfile (optional).
web: bin/rails server -p $PORT
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

The model:

class User < ApplicationRecord
  has_secure_password

  validates :email_address, presence: true, uniqueness: true
  normalizes :email_address, with: -> (e) { e.strip.downcase }
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Make the Authentication Available

Add them into ApplicationController,

class ApplicationController < ActionController::API 
  include Authentication
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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\"}"
Enter fullscreen mode Exit fullscreen mode

we should get the token in the response body:

{"token":"<AUTH_TOKEN>"}
Enter fullscreen mode Exit fullscreen mode

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)