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.
Secure token storage & expiration
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.
Cross-platform (mobile, JS, IoT)
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 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
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)
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?
Top comments (0)