πŸ›‘οΈ RackJwtAegis: Rack Middleware for Multi-Tenant JWT Authentication (Part 1)

πŸ›‘οΈ RackJwtAegis: Rack Middleware for Multi-Tenant JWT Authentication (Part 1)

Publish Date: Aug 14
0 0

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:


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

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

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

What this advanced configuration enables:

  • Tenant Header Validation: The tenant_id_header_name setting requires API requests to include an X-Company-Group-Id header that must match the company_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's company_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's company_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
Enter fullscreen mode Exit fullscreen mode

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

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

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

This payload structure enables three layers of validation:

  1. Tenant ID validation via company_group_id
  2. Subdomain validation via company_group_domain
  3. 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)
Enter fullscreen mode Exit fullscreen mode

Middleware Security Validation

Before your Rails application receives the request, RackJwtAegis middleware validates:

  1. JWT Token Validity βœ“ - Signature and expiration check
  2. Tenant Header Match βœ“ - X-Company-Group-Id must match JWT company_group_id
  3. Subdomain Validation βœ“ - acme-corp.localhost.local must match JWT company_group_domain
  4. Path Authorization βœ“ - acme-manufacturing must exist in JWT company_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'
Enter fullscreen mode Exit fullscreen mode

πŸ”„ 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"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

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

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

πŸ§ͺ 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"
Enter fullscreen mode Exit fullscreen mode

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

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

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

πŸŽ‰ 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
}
Enter fullscreen mode Exit fullscreen mode

πŸ” 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"
Enter fullscreen mode Exit fullscreen mode

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


Found this helpful? Give it a ⭐ on GitHub and watch for Part 2!

Tags: #ruby #rails #jwt #authentication #middleware #multitenant #security #api #saas #enterprise

Comments 0 total

    Add comment