Class: RailsAiBridge::Mcp::Authenticator

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_ai_bridge/mcp/authenticator.rb

Overview

Service object that authenticates HTTP MCP requests.

Consolidates strategy resolution, static-token lookup, and configuration predicates into a single entry point — previously split across +McpHttpAuth+ (utility helpers) and +Mcp::HttpAuth+ (strategy orchestrator), both removed in v2.0.0.

== Strategy priority (highest → lowest)

  1. Configuration#mcp_jwt_decoder → RailsAiBridge::Mcp::Auth::Strategies::Jwt
  2. Configuration#mcp_token_resolver → RailsAiBridge::Mcp::Auth::Strategies::BearerToken (resolver mode)
  3. +config.http_mcp_token+ / +ENV["RAILS_AI_BRIDGE_MCP_TOKEN"]+ → RailsAiBridge::Mcp::Auth::Strategies::BearerToken (static mode)
  4. None configured → open access (RailsAiBridge::Mcp::AuthResult.ok)

== Security design notes

  • Static-token comparison uses +Digest::SHA256+ pre-hashing + +ActiveSupport::SecurityUtils.secure_compare+ (constant-time over fixed-length digests). This prevents token-length leakage but does NOT protect against brute-force guessing. Use +config.mcp.rate_limit_max_requests+ for built-in per-IP rate limiting, or +rack-attack+ for distributed/stricter quotas.

  • The JWT strategy is decoder-only — this gem carries no JWT dependency. The host supplies a lambda; any +StandardError+ it raises is caught and surfaced as +:decode_error+, never propagated.

Examples:

Authenticating a Rack request

result = Mcp::Authenticator.call(request)
if result.success?
  # proceed — result.context may contain user/JWT payload
else
  Mcp::Authenticator.unauthorized_rack_response
end

See Also:

Constant Summary collapse

TOKEN_ENV_KEY =
'RAILS_AI_BRIDGE_MCP_TOKEN'

Class Method Summary collapse

Class Method Details

.any_configured?Boolean

Returns +true+ when any auth mechanism is configured — static token, resolver, or JWT decoder. Used by production-safety validators to confirm the MCP endpoint is protected.

Checks configuration presence, not runtime correctness. A resolver that always returns +nil+ still counts as "configured."

Returns:

  • (Boolean)


65
66
67
# File 'lib/rails_ai_bridge/mcp/authenticator.rb', line 65

def any_configured?
  resolve_strategy.present?
end

.call(request) ⇒ AuthResult

Authenticate an incoming Rack request against the configured strategy.

Parameters:

  • request (Rack::Request)

Returns:

  • (AuthResult)

    +.success?+ when authorized, +.failure?+ otherwise



50
51
52
53
54
55
# File 'lib/rails_ai_bridge/mcp/authenticator.rb', line 50

def call(request)
  strategy = resolve_strategy
  return AuthResult.ok(nil) if strategy.nil?

  strategy.authenticate(request)
end

.unauthorized_rack_responseArray<(Integer, Hash{String => String}, Array<String>)>

Builds a Rack 401 response for unauthenticated HTTP MCP requests.

Returns:

  • (Array<(Integer, Hash{String => String}, Array<String>)>)

    Rack response tuple



72
73
74
75
76
77
78
79
80
81
# File 'lib/rails_ai_bridge/mcp/authenticator.rb', line 72

def unauthorized_rack_response
  [
    401,
    {
      'Content-Type' => 'application/json',
      'WWW-Authenticate' => 'Bearer realm="rails-ai-bridge-mcp"'
    },
    ['{"error":"Unauthorized"}']
  ]
end