Class: RackJwtAegis::Middleware

Inherits:
Object
  • Object
show all
Includes:
DebugLogger
Defined in:
lib/rack_jwt_aegis/middleware.rb

Overview

Main Rack middleware for JWT authentication and authorization

This middleware handles the complete JWT authentication flow including:

  • JWT token extraction and validation
  • Multi-tenant validation (subdomain/pathname slug)
  • RBAC permission checking
  • Custom payload validation
  • Request context setting

Examples:

Basic usage

use RackJwtAegis::Middleware, jwt_secret: ENV['JWT_SECRET']

Advanced usage

use RackJwtAegis::Middleware, {
  jwt_secret: ENV['JWT_SECRET'],
  validate_tenant_id: true,
  validate_pathname_slug: true,
  validate_subdomain: true,
  rbac_enabled: true,
  cache_store: :redis,
  skip_paths: ['/health', '/api/public/*']
}

Author:

  • Ken Camajalan Demanawa

Since:

  • 0.1.0

Instance Method Summary collapse

Methods included from DebugLogger

#debug_log

Constructor Details

#initialize(app, options = {}) ⇒ Middleware

Initialize the middleware

Parameters:

  • app (#call)

    the Rack application

  • options (Hash) (defaults to: {})

    configuration options (see Configuration#initialize)

Since:

  • 0.1.0



36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/rack_jwt_aegis/middleware.rb', line 36

def initialize(app, options = {})
  @app = app
  @config = Configuration.new(options)

  # Initialize components
  @jwt_validator = JwtValidator.new(@config)
  @multi_tenant_validator = MultiTenantValidator.new(@config) if multi_tenant_enabled?
  @rbac_manager = RbacManager.new(@config) if @config.rbac_enabled?
  @response_builder = ResponseBuilder.new(@config)
  @request_context = RequestContext.new(@config)

  debug_log("Middleware initialized with features: #{enabled_features}")
end

Instance Method Details

#call(env) ⇒ Array

Process the Rack request

Parameters:

  • env (Hash)

    the Rack environment

Returns:

  • (Array)

    Rack response array [status, headers, body]

Raises:

Since:

  • 0.1.0



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/rack_jwt_aegis/middleware.rb', line 56

def call(env)
  request = Rack::Request.new(env)

  debug_log("Processing request: #{request.request_method} #{request.path}")

  # Step 1: Check if path should be skipped
  if @config.skip_path?(request.path)
    debug_log("Skipping authentication for path: #{request.path}")
    return @app.call(env)
  end

  begin
    # Step 2: Extract and validate JWT token
    token = extract_jwt_token(request)
    payload = @jwt_validator.validate(token)

    debug_log("JWT validation successful for user: #{payload[@config.payload_key(:user_id).to_s]}")

    # Step 3: Multi-tenant validation (if enabled)
    if multi_tenant_enabled?
      @multi_tenant_validator.validate(request, payload)
      debug_log('Multi-tenant validation successful')
    end

    # Step 4: RBAC permission check (if enabled)
    if @config.rbac_enabled?
      # Extract and store user roles in request environment for RBAC manager
      user_roles = extract_user_roles(payload)
      request.env['rack_jwt_aegis.user_roles'] = user_roles

      @rbac_manager.authorize(request, payload)
      debug_log('RBAC authorization successful')
    end

    # Step 5: Custom payload validation (if configured)
    if @config.custom_payload_validator
      unless @config.custom_payload_validator.call(payload, request)
        debug_log('Custom payload validation failed')
        raise AuthorizationError, 'Custom validation failed'
      end
      debug_log('Custom payload validation successful')
    end

    # Step 6: Set request context for application
    @request_context.set_context(env, payload)
    debug_log('Request context set successfully')

    # Continue to application
    @app.call(env)
  rescue AuthenticationError => e
    debug_log("Authentication failed: #{e.message}")
    @response_builder.unauthorized_response(e.message)
  rescue AuthorizationError => e
    debug_log("Authorization failed: #{e.message}")
    @response_builder.forbidden_response(e.message)
  rescue StandardError => e
    debug_log("Unexpected error: #{e.message}")
    if @config.debug_mode?
      @response_builder.error_response("Internal error: #{e.message}", 500)
    else
      @response_builder.error_response('Internal server error', 500)
    end
  end
end

#enabled_featuresString (private)

Generate a string describing enabled features for logging

Returns:

  • (String)

    comma-separated list of enabled features

Since:

  • 0.1.0



153
154
155
156
157
158
159
160
# File 'lib/rack_jwt_aegis/middleware.rb', line 153

def enabled_features
  features = ['JWT']
  features << 'TenantId' if @config.validate_tenant_id?
  features << 'Subdomain' if @config.validate_subdomain?
  features << 'PathnameSlug' if @config.validate_pathname_slug?
  features << 'RBAC' if @config.rbac_enabled?
  features.join(', ')
end

#extract_jwt_token(request) ⇒ String (private)

Extract JWT token from the Authorization header

Parameters:

  • request (Rack::Request)

    the Rack request object

Returns:

  • (String)

    the extracted JWT token

Raises:

Since:

  • 0.1.0



128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/rack_jwt_aegis/middleware.rb', line 128

def extract_jwt_token(request)
  auth_header = request.get_header('HTTP_AUTHORIZATION')

  raise AuthenticationError, 'Authorization header missing' if auth_header.nil? || auth_header.empty?

  # Extract Bearer token
  match = auth_header.match(/\ABearer\s+(.+)\z/)
  raise AuthenticationError, 'Invalid authorization header format' if match.nil?

  token = match[1]
  raise AuthenticationError, 'JWT token missing' if token.nil? || token.empty?

  token
end

#extract_user_roles(payload) ⇒ Array (private)

Extract user roles from JWT payload for RBAC authorization

Parameters:

  • payload (Hash)

    the JWT payload

Returns:

  • (Array)

    array of user role IDs

Since:

  • 0.1.0



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/rack_jwt_aegis/middleware.rb', line 166

def extract_user_roles(payload)
  # Use configured payload mapping for role_ids, with fallback to common field names
  role_key = @config.payload_key(:role_ids).to_s
  roles = payload[role_key]

  # If mapped key doesn't exist, try common fallback field names
  roles = payload['roles'] || payload['role'] || payload['user_roles'] || payload['role_ids'] if roles.nil?

  case roles
  when Array
    roles.map(&:to_s) # Ensure all roles are strings for consistent lookup
  when String, Integer
    [roles.to_s] # Single role as array
  else
    debug_log("Warning: No valid roles found in JWT payload. Looking for '#{role_key}' field. \
               Available fields: #{payload.keys}".squeeze)
    []
  end
end

#multi_tenant_enabled?Boolean (private)

Check if multi-tenant validation is enabled

Returns:

  • (Boolean)

    true if subdomain or pathname slug validation is enabled

Since:

  • 0.1.0



146
147
148
# File 'lib/rack_jwt_aegis/middleware.rb', line 146

def multi_tenant_enabled?
  @config.validate_tenant_id? || @config.validate_subdomain? || @config.validate_pathname_slug?
end