Implementing middleware-level JWT authentication and authorization before requests reach your Rails application
TL;DR
RackJwtAegis is a Rack middleware that provides JWT authentication and authorization before requests reach your Rails or Rack application. It validates JWT claims at the middleware layer, enabling multi-tenancy and security without touching your application code. This is Part 1 of a 2-part series focusing on basic setup and multi-tenant configuration. Part 2 will cover advanced RBAC (Role-Based Access Control) and caching strategies.
π Links:
- RubyGems: rack_jwt_aegis
- GitHub: RackJwtAegis Repository
- Demo App: Multi-Tenant Demo (Proof-of-Concept)
Why Another JWT Middleware?
Most JWT gems handle basic authentication, but many applications need additional features:
- Multi-tenant isolation with header-based tenant id and request host subdomain validation
- Path-based authorization using company slugs from JWT payloads
- Middleware-level validation that occurs before requests reach your application
- Built-in caching strategies for performance optimization
- Support for organizational hierarchies without application code changes
RackJwtAegis was built to handle these scenarios at the Rack middleware layer, intercepting and validating requests before they reach your Rails controllers.
ποΈ The Demo: Multi-Tenant Proof-of-Concept
To demonstrate RackJwtAegis features, I built a basic proof-of-concept application with a multi-tenant ERP structure:
β οΈ Note: This is an unfinished demo application that only returns success messages - no actual business logic is implemented. It exists solely to showcase middleware authentication patterns.
Architecture Overview
Company Group (Tenant)
βββ Company A (Manufacturing)
βββ Company B (Retail)
βββ Company C (Logistics)
βββ Company D (Technology)
βββ Company E (Wholesale)
Each company has 7 ERP modules (demo structure only):
π Accounting π¦ Inventory π Procurement πΌ Sales
πͺ Retail π Warehouse π Wholesale
Note: These are placeholder modules for demonstrating multi-tenant routing - no actual ERP features are implemented.
Demo Permission Matrix
The application demonstrates these access patterns (without actual business logic):
- Super Admin: Full access across all companies and modules
- Group CFO: Financial access across multiple companies
- Company Admin: Full access within their assigned companies
- Department Manager: Module-specific access (e.g., Sales Manager)
- Employee: Limited access based on role and assignment
Note: All endpoints return simple success messages like {"message": "Reports retrieved successfully"}
to demonstrate authentication flow.
β‘ Quick Setup & Configuration
1. Installation
# Gemfile
gem 'rack_jwt_aegis', '~> 1.1.0'
bundle install
2. Basic Configuration
# config/application.rb
config.middleware.insert_before 0, RackJwtAegis::Middleware, {
jwt_secret: ENV['JWT_SECRET'],
skip_paths: ['/api/v1/auth/login', '/health']
}
What this configuration enables:
- JWT Token Validation: The middleware validates JWT signatures using your secret key before any request reaches your Rails API-only application
-
Authentication Layer: Automatically extracts and validates JWT tokens from the
Authorization: Bearer <token>
header for API endpoints -
Public Endpoints: The
skip_paths
array allows specific API routes (like login and health checks) to bypass JWT validation -
Middleware Positioning:
insert_before 0
ensures JWT validation happens first, before any other middleware or Rails API routing - API-First Security: Perfect for Rails API-only applications that serve mobile apps, SPAs, or microservices
Result: Your Rails API-only application now has middleware-level JWT authentication. Invalid tokens are rejected with HTTP 401 before reaching your API controllers.
3. Multi-Tenant Configuration
# config/application.rb - Multi-tenant configuration
config.middleware.insert_before 0, RackJwtAegis::Middleware, {
jwt_secret: ENV['JWT_SECRET'] || 'your-super-secret-jwt-key-for-development',
tenant_id_header_name: 'X-Company-Group-Id',
validate_tenant_id: true,
validate_pathname_slug: true,
validate_subdomain: true,
skip_paths: ['/api/v1/auth/login', '/up', '/api/v1/health'],
# Custom JWT payload mapping for multi-tenant access control
payload_mapping: {
user_id: :sub,
tenant_id: :company_group_id,
subdomain: :company_group_domain,
pathname_slugs: :company_slugs,
role_ids: :role_ids,
},
}
What this advanced configuration enables:
-
Tenant Header Validation: The
tenant_id_header_name
setting requires API requests to include anX-Company-Group-Id
header that must match thecompany_group_id
in the JWT payload, preventing cross-tenant access -
Subdomain Security:
validate_subdomain: true
ensures the request subdomain (e.g.,acme-corp.localhost.local
) matches the JWT'scompany_group_domain
field for API-only applications -
Path-Based Authorization:
validate_pathname_slug: true
validates that company slugs in API URLs (e.g.,/api/v1/acme-manufacturing/...
) exist in the JWT'scompany_slugs
array -
Custom Payload Mapping: The
payload_mapping
hash tells the middleware where to find tenant isolation data in your JWT structure for API-only authentication -
Role Integration: Maps
role_ids
from JWT payload for downstream RBAC processing in API controllers - Multi-Tenant API Security: Ideal for Rails API-only applications serving multiple tenants through mobile apps, SPAs, or B2B integrations
Result: Three-layer middleware security validation (JWT β Tenant β Path) that enforces multi-tenant isolation before requests reach your Rails API-only application. Users can only access companies they're authorized for, and tenant isolation is guaranteed at the middleware level for all API endpoints.
π― Consuming Validated Data in Rails Controllers
After RackJwtAegis middleware validates the JWT and enforces multi-tenant security, your Rails API controllers can access the validated user context through RequestContext
:
Basic Controller Usage
# app/controllers/api/v1/accounting/reports_controller.rb
class Api::V1::Accounting::ReportsController < ApplicationController
def index
# Check if request passed middleware authentication
return unauthorized unless RackJwtAegis::RequestContext.authenticated?(request.env)
# Get validated user information from middleware
user_id = RackJwtAegis::RequestContext.user_id(request.env)
tenant_id = RackJwtAegis::RequestContext.tenant_id(request.env)
# Access full JWT payload validated by middleware
payload = RackJwtAegis::RequestContext.payload(request.env)
user_roles = payload['role_ids']
# Your Rails API business logic here
reports = Report.where(company_group_id: tenant_id)
render json: { message: "Reports retrieved successfully", count: reports.count }
end
end
Multi-Tenant Context Access
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
protected
def current_user_id
RackJwtAegis::RequestContext.current_user_id(request)
end
def current_tenant_id
RackJwtAegis::RequestContext.current_tenant_id(request)
end
def accessible_companies
RackJwtAegis::RequestContext.pathname_slugs(request.env)
end
def has_company_access?(company_slug)
RackJwtAegis::RequestContext.has_pathname_slug_access?(request.env, company_slug)
end
def jwt_payload
RackJwtAegis::RequestContext.payload(request.env)
end
end
Available RequestContext Methods
-
authenticated?(env)
- Check if middleware validated the request -
payload(env)
- Get full JWT payload hash validated by middleware -
user_id(env)
- Get authenticated user ID -
tenant_id(env)
- Get tenant/company group ID -
subdomain(env)
- Get subdomain from JWT -
pathname_slugs(env)
- Get array of accessible company slugs -
current_user_id(request)
- Helper for request objects -
current_tenant_id(request)
- Helper for request objects -
has_pathname_slug_access?(env, slug)
- Check company access
Key Benefits: Your Rails API controllers receive pre-validated data from the middleware layer, eliminating the need for JWT parsing or multi-tenant validation in your application code.
π Building API Gateway-Like Protection
For production Rails API-only applications, combine RackJwtAegis with Rack::Attack for comprehensive API gateway-like middleware protection:
# config/application.rb - Production API security stack
config.middleware.insert_before 0, RackJwtAegis::Middleware, {
jwt_secret: ENV['JWT_SECRET'],
tenant_id_header_name: 'X-Company-Group-Id',
validate_tenant_id: true,
validate_pathname_slug: true,
validate_subdomain: true,
skip_paths: ['/api/v1/auth/login', '/up', '/api/v1/health']
}
# Add rate limiting and request filtering after JWT validation
config.middleware.insert_after RackJwtAegis::Middleware, Rack::Attack
# or config.middleware.insert_before RackJwtAegis::Middleware, Rack::Attack
Result: Your Rails API-only application now has enterprise-grade middleware protection:
- Layer 1: JWT authentication and multi-tenant authorization (RackJwtAegis)
- Layer 2: Rate limiting, IP blocking, and request filtering (Rack::Attack)
- Layer 3: Your Rails API controllers with clean, validated data
This middleware stack provides API gateway-like security without the complexity of external infrastructure, perfect for Rails API applications serving mobile apps, SPAs, and microservices.
π JWT Payload Design for Multi-Tenancy
The key to effective multi-tenant authentication is a well-structured JWT payload:
# app/models/user.rb
def jwt_payload_data
{
sub: id, # Standard JWT subject claim
email: email,
first_name: first_name,
last_name: last_name,
company_group_id: company_group.id, # Tenant isolation
company_group_domain: company_group.domain_name, # Subdomain validation
company_slugs: companies.pluck(:slug), # Path-based authorization
iat: Time.current.to_i, # Issued at
exp: 24.hours.from_now.to_i # Expires at
}
end
This payload structure enables three layers of validation:
-
Tenant ID validation via
company_group_id
-
Subdomain validation via
company_group_domain
-
Path-based authorization via
company_slugs
π’ Middleware-Level Multi-Tenant Authorization
RackJwtAegis operates at the Rack middleware layer, validating requests before they reach your application. This provides three layers of security validation:
URL Structure & Validation
Request: GET https://acme-corp.localhost.local:3000/api/v1/acme-manufacturing/accounting/reports
Headers: Authorization: Bearer <JWT_TOKEN>
X-Company-Group-Id: 1
URL Components:
βββ Subdomain: acme-corp (maps to company_group_domain in JWT)
βββ Path Slug: acme-manufacturing (must be in company_slugs array)
βββ Module: accounting (validated in Part 2: RBAC)
Middleware Security Validation
Before your Rails application receives the request, RackJwtAegis middleware validates:
- JWT Token Validity β - Signature and expiration check
-
Tenant Header Match β -
X-Company-Group-Id
must match JWTcompany_group_id
-
Subdomain Validation β -
acme-corp.localhost.local
must match JWTcompany_group_domain
-
Path Authorization β -
acme-manufacturing
must exist in JWTcompany_slugs
array
Only valid requests reach your Rails controllers - invalid requests are rejected at the middleware layer with appropriate HTTP status codes.
Development Setup
# Update /etc/hosts for subdomain testing
echo "127.0.0.1 acme-corp.localhost.local" >> /etc/hosts
# In config/environments/development.rb
config.hosts << '.localhost.local'
π Complete Authentication Flow
1. User Login
curl -X POST http://localhost:3000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"auth": {
"email": "owner@acme-corp.com",
"password": "password123"
}
}'
Response:
{
"message": "Login successful",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"user": {
"id": 1,
"email": "owner@acme-corp.com",
"role": "super_admin",
"accessible_companies": [
"acme-manufacturing",
"acme-retail",
"acme-logistics",
"acme-technology",
"acme-wholesale"
]
},
"expires_at": "2025-08-15T00:36:15Z"
}
2. Multi-Tenant API Access
# β
SUCCESS: Valid token + tenant header + authorized company
curl -X GET http://localhost:3000/api/v1/acme-manufacturing/accounting/reports \
-H "Authorization: Bearer $TOKEN" \
-H "X-Company-Group-Id: 1"
# β FORBIDDEN: Wrong tenant header
curl -X GET http://localhost:3000/api/v1/acme-manufacturing/accounting/reports \
-H "Authorization: Bearer $TOKEN" \
-H "X-Company-Group-Id: 999"
# β FORBIDDEN: Unauthorized company slug
curl -X GET http://localhost:3000/api/v1/unauthorized-company/accounting/reports \
-H "Authorization: Bearer $TOKEN" \
-H "X-Company-Group-Id: 1"
π§ͺ Testing Middleware Security
The demo includes curl tests that demonstrate how the middleware rejects requests before they reach your Rails application:
JWT Token Validation
# β No token provided
curl -X GET http://localhost:3000/api/v1/acme-retail/accounting/reports
# β Invalid token
curl -X GET http://localhost:3000/api/v1/acme-retail/accounting/reports \
-H "Authorization: Bearer invalid.jwt.token"
# β Malformed token
curl -X GET http://localhost:3000/api/v1/acme-retail/accounting/reports \
-H "Authorization: Bearer notjwttoken"
Tenant Isolation Tests
# β Wrong tenant header
curl -X GET http://localhost:3000/api/v1/acme-retail/accounting/reports \
-H "Authorization: Bearer $VALID_TOKEN" \
-H "X-Company-Group-Id: 999"
# β Missing tenant header
curl -X GET http://localhost:3000/api/v1/acme-retail/accounting/reports \
-H "Authorization: Bearer $VALID_TOKEN"
Path-Based Authorization Tests
# β Unauthorized company slug (not in JWT company_slugs array)
curl -X GET http://localhost:3000/api/v1/unauthorized-company/accounting/reports \
-H "Authorization: Bearer $VALID_TOKEN" \
-H "X-Company-Group-Id: 1"
# β
Authorized company slug (in JWT company_slugs array)
curl -X GET http://localhost:3000/api/v1/acme-manufacturing/accounting/reports \
-H "Authorization: Bearer $VALID_TOKEN" \
-H "X-Company-Group-Id: 1"
π― What's Coming in Part 2
Part 2 of this series will cover advanced RBAC and caching strategies:
Advanced Features Preview
- π Role-Based Access Control (RBAC): Complex permission hierarchies with role inheritance
- β‘ Solid Cache Integration: High-performance permission caching strategies
- π Permission Matrix: Role-based access patterns across modules and companies
- ποΈ Dynamic Permissions: Runtime permission validation and caching
- π Cache Invalidation: Smart cache invalidation strategies for role changes
RBAC Permissions Hash Structure
# Coming in Part 2: Cached permissions hash
{
"permissions" => {
"last_update" => 1755141074,
"1" => ["accounting/*:*", "inventory/*:*", "sales/*:*"], # Owner role
"11" => ["sales/leads:get", "sales/customers:get"], # Sales Rep role
"14" => ["procurement/*:*"] # Procurement Manager
}
}
π Key Features Covered in Part 1
β Middleware-Level Security Foundation
- π JWT Authentication: Token validation with HS256 algorithm at middleware layer
-
π’ Multi-Tenancy: Header-based tenant isolation (
X-Company-Group-Id
) before Rails - π Subdomain Validation: Request host validation against JWT claims
- π Path Authorization: Company slug validation from JWT payload
- π‘οΈ Pre-Application Security: Three-tier validation before requests reach controllers
- π§ Configurable: Custom payload mapping and skip paths
- π§ͺ Tested: Curl test suite demonstrating middleware rejection patterns
π Production-Ready Configuration
# Production multi-tenant setup
config.middleware.insert_before 0, RackJwtAegis::Middleware, {
jwt_secret: ENV['JWT_SECRET'], # Strong secret required
tenant_id_header_name: 'X-Company-Group-Id', # Consistent header naming
validate_tenant_id: true, # Enable tenant isolation
validate_subdomain: true, # Enable subdomain validation
validate_pathname_slug: true, # Enable path authorization
skip_paths: ['/api/v1/auth/*', '/health'], # Public endpoints
debug: false # Disable debug in production
}
π Real-World Multi-Tenant Use Cases
The middleware-level authentication patterns covered in Part 1 work well for:
Enterprise SaaS Applications
- Multi-tenant CRM/ERP systems with company isolation
- Team collaboration platforms with workspace separation
- Project management tools with client segregation
- E-commerce marketplaces with vendor separation
API-First Applications
- Microservices authentication with tenant-aware routing
- Mobile app backends with organization-based access
- Third-party integrations with client-specific endpoints
- Multi-brand platforms with subdomain-based routing
π― Conclusion
In Part 1, we've covered middleware-level multi-tenant JWT authentication with RackJwtAegis. You now have:
β
Three-layer middleware security validation (JWT β Tenant β Path)
β
Pre-application request filtering that protects your Rails controllers
β
Production-ready multi-tenant configuration
β
Testing patterns that demonstrate middleware behavior
This middleware foundation provides security for multi-tenant applications before requests reach your application code, but enterprise applications need more sophisticated authorization patterns.
Part 2 will cover RBAC systems, caching strategies, and permission hierarchies for multi-organizational applications.
Try the Demo Today
Prerequisites: Ruby 3.3+, Rails 8.0+
# Clone and run the proof-of-concept demo
git clone https://github.com/kanutocd/rack-jwt-aegis-example
cd rack-jwt-aegis-example
# Install dependencies
bundle install
# Setup database with test users and permissions cache
rails db:setup
# Start the server
rails server
# Test multi-tenant authentication (returns demo success messages)
curl -X POST http://localhost:3000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"auth": {"email": "owner@acme-corp.com", "password": "password123"}}'
# Use the returned JWT token to test middleware security
TOKEN="<paste-token-here>"
curl -X GET http://localhost:3000/api/v1/acme-manufacturing/accounting/reports \
-H "Authorization: Bearer $TOKEN" \
-H "X-Company-Group-Id: 1"
What you'll see:
- Authentication endpoint returns a JWT token with multi-tenant claims
- Protected endpoints return success messages like
{"message": "Reports retrieved successfully"}
- Invalid requests are rejected by middleware before reaching Rails controllers
- Comprehensive test users with different permission levels (see
CURL_TESTING_GUIDE.md
)
Remember: This demo only returns success messages to show middleware authentication patterns - no actual ERP functionality is implemented.
π Watch for Part 2: Advanced RBAC and Caching Strategies with RackJwtAegis
π Resources
- π¦ RubyGems: rack_jwt_aegis
- π GitHub Repository: RackJwtAegis
- π» Demo App: Multi-Tenant Demo (Proof-of-Concept)
- π§ͺ Testing Guide: Complete curl Testing Suite
Found this helpful? Give it a β on GitHub and watch for Part 2!
Tags: #ruby #rails #jwt #authentication #middleware #multitenant #security #api #saas #enterprise