DEV Community

Germán Alberto Gimenez Silva
Germán Alberto Gimenez Silva

Posted on • Originally published at rubystacknews.com on

🔐 Building Token-Based Authentication in a Rails API with JWT

May 19, 2025

A Practical and Maintainable Approach to Securing Your Endpoints

by Giménez Silva Germán Alberto

Modern APIs require stateless, secure, and scalable authentication mechanisms. For many Rails developers, JWT (JSON Web Tokens) offer the ideal solution: they’re lightweight, portable, and perfectly suited to APIs where session-based auth doesn’t scale.

In this article, I’ll walk you through designing and implementing JWT authentication in a Rails API, with token expiration , revocation support , and role-based access control , while keeping the architecture clean and testable. Think Martin Fowler’s clarity, applied to Rails security.


🔐 Need Help with API Authentication?

Whether you’re working with JWT, OAuth2, or API key authentication in Rails, I can help you design a secure and scalable solution.

✔ JWT login & validation

✔ Secure token storage & expiration

✔ Token revocation strategies

✔ Doorkeeper for OAuth2

✔ API key generation & protection
💬


📌 1. Why JWT?

Stateless authentication allows you to scale your API horizontally—no session storage, no sticky sessions. Tokens are self-contained and verify identity and permissions in a single string.

✅ Scalable

✅ Cross-platform (mobile, JS, IoT)

✅ Fast & framework-agnostic

However, JWTs come with trade-offs. You must handle expiration , revocation , and permissions explicitly.


🛠 2. Installing JWT Support in Rails

Start with the essentials:


# Gemfile
gem 'jwt'
gem 'bcrypt'



bundle install

Enter fullscreen mode Exit fullscreen mode

Create your User model with password encryption:


rails g model User email:string password_digest:string role:string
rails db:migrate



# app/models/user.rb
has_secure_password
enum role: { user: 'user', admin: 'admin' }

Enter fullscreen mode Exit fullscreen mode

Now, let’s generate a model to track JWTs issued:


rails g model JwtToken user:references token:string exp:datetime
rails db:migrate

Enter fullscreen mode Exit fullscreen mode

We’ll store each token to allow revocation and later implement scheduled cleanup.


🔐 3. Designing the Auth Flow

Here’s the minimal contract we want for our clients:

  • POST /auth/login with credentials → receive JWT
  • Use Authorization: Bearer on protected endpoints
  • Token auto-expires and can be revoked

Login Controller


# app/controllers/auth_controller.rb
class AuthController < ApplicationController
  def login
    user = User.find_by(email: params[:email])
    if user&.authenticate(params[:password])
      payload = { user_id: user.id, role: user.role, exp: 1.hour.from_now.to_i }
      token = JWT.encode(payload, Rails.application.secret_key_base, 'HS256')
      JwtToken.create!(user: user, token: token, exp: Time.at(payload[:exp]))
      render json: { token: token }
    else
      render json: { error: "Invalid credentials" }, status: :unauthorized
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

🧭 4. Authenticating Requests

We’ll define a method in ApplicationController that can be reused across all protected controllers.


# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  before_action :authenticate_user!

  private

  def authenticate_user!
    header = request.headers['Authorization']
    return unauthorized unless header&.match(/^Bearer /)

    token = header.split(' ').last
    begin
      payload = JWT.decode(token, Rails.application.secret_key_base, true, algorithm: 'HS256')[0]
      raise if Time.at(payload['exp']) < Time.now

      JwtToken.find_by!(token: token) # Check revocation
      @current_user = User.find(payload['user_id'])
    rescue
      unauthorized
    end
  end

  def unauthorized
    render json: { error: 'Unauthorized' }, status: :unauthorized
  end
end

Enter fullscreen mode Exit fullscreen mode

You can now protect endpoints like this:


# app/controllers/api/v1/posts_controller.rb
before_action :authenticate_user!

Enter fullscreen mode Exit fullscreen mode

🎯 5. Role-Based Authorization

To go beyond identity, we add roles to define permissions:


# in a controller
def require_admin!
  return render json: { error: 'Forbidden' }, status: :forbidden unless @current_user&.admin?
end

Enter fullscreen mode Exit fullscreen mode

🧽 6. Expiration & Revocation

Article content

The JWT includes an exp claim. But what if a user logs out or you need to revoke a token?

✅ We already track issued tokens in a JwtToken model.

✅ Let’s clean expired tokens automatically:


# lib/tasks/jwt_cleanup.rake
namespace :jwt do
  task cleanup: :environment do
    JwtToken.where('exp < ?', Time.current).delete_all
  end
end

Enter fullscreen mode Exit fullscreen mode

Schedule this with cron or whenever.

You can also add a logout endpoint:


def logout
  token = request.headers['Authorization']&.split(' ')&.last
  JwtToken.find_by(token: token)&.destroy
  head :ok
end

Enter fullscreen mode Exit fullscreen mode

🧱 7. Security Principles

🚫 Never store JWTs in localStorage

✅ Prefer HTTP-only cookies (if CSRF isn’t a concern)

🔐 Always use HTTPS

🛑 Rate-limit login attempts (rack-attack)

🔁 Rotate secret_key_base periodically


🧵 In Summary

JWT is not a silver bullet—but when implemented cleanly, with attention to security and flexibility, it provides a robust, scalable auth strategy for Rails APIs.

This implementation offers:

  • Stateless auth with expirable tokens
  • Revocation support
  • Role-based permissions
  • Extendable logic (e.g., refresh tokens, scopes)

✉ Want the full repo example? Let me know in the comments or DM.

💬 What’s your preferred API authentication method in production?

🔁 Share if your team is building APIs in 2025.

Article content

Top comments (0)