🔐 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
💬 Get in Touch

📌 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

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' }

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

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

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 <token> 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

🧭 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

You can now protect endpoints like this:

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

🎯 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

🧽 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

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

🧱 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

Leave a comment