OAuth 2.1 Authentication Architecture
Implementation: mcp-trino uses oauth-mcp-proxy - a standalone, production-ready OAuth library for Go MCP servers.
For implementation details, provider setup, and security best practices, see the oauth-mcp-proxy documentation.
This document outlines the OAuth 2.1 authentication architecture for mcp-trino server, providing secure access control for AI assistants accessing Trino databases.
Important Security Notes
⚠️ Critical Requirements:
Fixed Redirect Mode: ONLY accepts localhost redirect URIs (development/testing only)
Allowlist Mode: Requires exact URI matches (production deployments)
JWT_SECRET: Must be configured for multi-pod deployments to ensure state verification consistency
PKCE: Optional but strongly recommended per OAuth 2.1 standard
HTTPS: Required for all non-localhost redirect URIs
✅ Security Guarantees:
HMAC-SHA256 signed state prevents tampering
Localhost-only restriction prevents open redirect attacks in fixed mode
Defense-in-depth: Multiple independent validation layers
Constant-time comparison prevents timing attacks
Architecture Overview
The mcp-trino server implements OAuth 2.0 as a resource server, validating JWT tokens from clients while maintaining existing Trino authentication methods.
OAuth Operational Modes
The MCP server supports two distinct operational modes:
Native Mode (Direct OAuth)
How it works:
Client authenticates directly with OAuth provider (Okta, Google, Azure)
Client receives JWT access token from provider
Client sends Bearer token to MCP server with each request
MCP server validates token using JWKS from OAuth provider
MCP server grants access to Trino resources
Configuration Requirements:
Server Side:
OIDC_ISSUER,OIDC_AUDIENCEonlyClient Side: Must configure OAuth client_id and provider endpoints
Security Model:
✅ Zero OAuth secrets stored in MCP server
✅ Most secure - direct trust relationship
✅ Simplified server deployment
⚠️ Requires OAuth-capable clients (Claude.ai, etc.)
Proxy Mode (OAuth Proxy)
How it works:
Client makes request to MCP server without any OAuth configuration
MCP server returns 401 with OAuth discovery information
Client discovers OAuth endpoints from MCP server metadata
MCP server proxies entire OAuth flow to upstream provider
Client receives token through MCP server proxy
Client uses token for subsequent API calls
Configuration Requirements:
Server Side: Full OAuth configuration (client_id, client_secret, issuer, audience, redirect URIs)
Client Side: Zero OAuth configuration needed
Security Model:
✅ Centralized credential management
✅ Works with any MCP client
✅ No client-side OAuth configuration
⚠️ Requires OAuth secrets in server environment
⚠️ Fixed mode limited to localhost callbacks (development only)
✅ Allowlist mode for production deployments
OAuth Configuration Guide
Environment Variables
OAUTH_ENABLED
Required
Required
Enable OAuth authentication
OAUTH_MODE
native
proxy
Operational mode
OAUTH_PROVIDER
okta/google/azure/hmac
okta/google/azure/hmac
Provider selection
JWT_SECRET
HMAC: Token validation
HMAC: Tokens All providers: State signing
HMAC signing key
OIDC_ISSUER
Required
Required
Provider issuer URL
OIDC_AUDIENCE
Required
Required
Token audience
OIDC_CLIENT_ID
❌ Not used
✅ Required
OAuth app client ID
OIDC_CLIENT_SECRET
❌ Not used
⚠️ Public: No Confidential: Yes
OAuth app secret
OAUTH_REDIRECT_URI
❌ Not used
✅ Required
Fixed or allowlist URIs
Redirect URI Configuration Modes
Fixed Redirect Mode (Single URI):
Configuration:
OAUTH_REDIRECT_URI=https://mcp-server.com/oauth/callback(no commas)Behavior: Server uses fixed URI with OAuth provider, proxies callback to client
Client URIs: MUST be localhost only (localhost, 127.0.0.1, ::1)
State Handling: HMAC-signed to prevent tampering
Use Case: Development tools (MCP Inspector, mcp-remote on localhost)
Security: Localhost-only prevents open redirect attacks
Allowlist Mode (Multiple URIs):
Configuration:
OAUTH_REDIRECT_URI=https://app1.com/cb,https://app2.com/cb(comma-separated)Behavior: Direct OAuth flow, no proxy
Client URIs: Must exactly match one URI in allowlist
State Handling: Standard OAuth state (no signing needed)
Use Case: Production deployments with known redirect URIs
Security: Exact match prevents open redirect attacks
Security Default (Empty):
Configuration:
OAUTH_REDIRECT_URI=(empty or not set)Behavior: Rejects all redirect URIs
Use Case: Fail-closed security when OAuth not properly configured
Security Architecture
Defense-in-Depth Model
The implementation uses four independent security layers. Even if one layer is compromised, the others prevent attacks.
Layer 1: Request Validation
Redirect URI format validation (URL parsing, scheme check)
HTTPS enforcement for non-localhost URIs
Fragment rejection per OAuth 2.0 specification
Localhost detection (hostname parsing to prevent subdomain attacks)
Layer 2: State Integrity Protection
HMAC-SHA256 signature using JWT_SECRET
Deterministic signing algorithm (consistent field ordering)
Constant-time signature comparison
Automatic key generation with warnings if not configured
Layer 3: Authorization Code Protection (PKCE)
Code challenge/verifier mechanism
Custom HTTP transport adds code_verifier to token requests
Prevents code theft even if authorization code is intercepted
Supported but optional (strongly recommended)
Layer 4: Token Validation
JWT signature verification using JWKS
Audience claim validation
Expiration timestamp checks
Token caching with SHA256 hashing
Fixed Redirect Mode Security Flow
This mode is designed for development tools and enforces strict localhost-only security:
Authorization Phase:
Validate redirect URI is well-formed URL
Check scheme is http or https
Reject if fragment present (OAuth 2.0 spec)
Critical: Verify hostname is localhost/127.0.0.1/::1
If not localhost → Reject with error
If localhost → Sign state with HMAC
Forward to OAuth provider using server's fixed redirect URI
Callback Phase:
Receive callback from OAuth provider
Decode and verify HMAC signature
Extract client redirect URI from signed state
Defense in depth: Re-validate client URI is localhost
If signature invalid or not localhost → Reject
If valid → Proxy to client's localhost callback
Allowlist Mode Security Flow
This mode is for production and enforces strict exact-match validation:
Process:
Parse client's redirect URI
Compare against allowlist using exact string matching
If no match → Reject request
If match → Use client's URI directly with OAuth provider
OAuth provider calls client directly (no proxy)
No state signing needed (standard OAuth flow)
Security Properties:
Fail-closed: Empty allowlist rejects all requests
No substring matching (prevents subdomain attacks)
No pattern matching (prevents bypass attempts)
Whitespace trimmed for comparison
Attack Prevention
State Tampering Attack
Attack Scenario: An attacker intercepts a valid signed state parameter and attempts to modify the redirect URI to point to their own server.
Prevention Mechanism:
State contains:
{state: "csrf-token", redirect: "http://localhost:6274", sig: "hmac..."}Attacker decodes base64 and changes redirect to "https://evil.com"
Attacker re-encodes and sends to callback endpoint
Server recalculates HMAC over original data
Signatures don't match → Request rejected
Why it works:
HMAC is cryptographically tied to the exact data
Any modification invalidates the signature
Attacker cannot forge signature without JWT_SECRET
Even with leaked JWT_SECRET, localhost validation prevents external redirects
Open Redirect Attack
Attack Scenario Fixed Mode: Attacker tries to use MCP server as open redirect by requesting authorization with redirect_uri=https://evil.com/steal.
Prevention:
Server validates redirect URI is localhost
evil.comis not localhost → Request rejected immediatelyAttack blocked before any OAuth flow begins
Attack Scenario Allowlist Mode: Attacker tries redirect to unauthorized URI.
Prevention:
Server checks exact string match against allowlist
No match → Request rejected
No wildcards or pattern matching prevents bypass
Authorization Code Theft
Attack Scenario: Attacker intercepts authorization code in transit (network sniffing, malware, etc.).
Prevention (PKCE):
Client generates random
code_verifierClient sends SHA256 hash (
code_challenge) in authorization requestOAuth provider stores the challenge
When exchanging code for token, client must provide original
code_verifierProvider verifies hash(code_verifier) == code_challenge
Without verifier, code is useless
Result: Even if attacker steals authorization code, they cannot exchange it for access token.
Metadata Endpoints
Discovery Endpoint Behavior
The server exposes multiple discovery endpoints that return different information based on operational mode:
/.well-known/oauth-authorization-server
Native Mode: Returns OAuth provider endpoints (Okta, Google, etc.)
Proxy Mode: Returns MCP server endpoints
Purpose: Tells clients where to find authorization, token, and registration endpoints
/.well-known/oauth-protected-resource
Native Mode:
authorization_servers: ["{oauth-provider-url}"]Proxy Mode:
authorization_servers: ["{mcp-server-url}"]Purpose: Critical for client routing - determines if client talks to provider directly or via proxy
/.well-known/jwks.json (Proxy mode only)
Proxies JWKS from upstream OAuth provider
Okta: Fetches from
{issuer}/oauth2/v1/keysGoogle: Fetches from
https://www.googleapis.com/oauth2/v3/certsReturns cached keys (5-minute cache)
Complete OAuth Flow - Proxy Mode with Fixed Redirect
This diagram shows the complete flow for development tools like MCP Inspector:
Configuration Examples
Development Setup - Fixed Redirect Mode
Helm Values:
trino:
oauth:
enabled: true
mode: "proxy"
provider: "okta"
jwtSecret: "your-256-bit-hex-key" # Required for state signing
redirectURIs: "https://mcp-server.com/oauth/callback" # Single URI
oidc:
issuer: "https://company.okta.com"
audience: "https://mcp-server.com"
clientId: "your-okta-app-client-id"
clientSecret: "your-okta-app-secret"What this enables:
MCP Inspector can use
http://localhost:6274/callbackmcp-remote can use any dynamic localhost port
All localhost callbacks are accepted and proxied securely
State signing ensures integrity across pod restarts
Security:
Localhost-only restriction prevents open redirect
HMAC signing prevents state tampering
Multi-pod safe with configured jwtSecret
Production Setup - Allowlist Mode
Helm Values:
trino:
oauth:
enabled: true
mode: "proxy"
provider: "okta"
jwtSecret: "your-256-bit-hex-key" # For HMAC provider or consistency
redirectURIs: "https://app1.company.com/callback,https://app2.company.com/callback"
oidc:
issuer: "https://company.okta.com"
audience: "https://api.company.com"
clientId: "production-client-id"
clientSecret: "production-client-secret"What this enables:
Only app1.company.com and app2.company.com callbacks allowed
Direct OAuth flow (no proxy)
Maximum security with exact matching
Production-grade deployment
Security Model Details
HMAC State Signing
Purpose: Prevent attackers from tampering with redirect URIs in the state parameter.
How it works:
Signing (Authorization):
Create data string:
state={csrf-token}&redirect={client-redirect-uri}Calculate:
signature = HMAC-SHA256(data, JWT_SECRET)Combine:
{state, redirect, sig}→ base64 encodeSend encoded state to OAuth provider
Verification (Callback):
Decode base64 → Extract signature
Recalculate:
expected = HMAC-SHA256(state + redirect, JWT_SECRET)Compare:
hmac.Equal(received_sig, expected_sig)(constant-time)If match → Extract original state and redirect
If mismatch → Reject as tampered
Key Properties:
Uses same JWT_SECRET across all pods (must be configured)
Deterministic algorithm ensures verification succeeds
Constant-time comparison prevents timing attacks
Defense in depth: Localhost also re-validated after verification
Localhost Detection
Purpose: Ensure fixed redirect mode only accepts localhost callbacks, preventing open redirect attacks.
Implementation:
Parse full URI to extract hostname
Convert hostname to lowercase
Check if hostname is one of:
localhost127.0.0.1(IPv4 loopback)::1(IPv6 loopback)
Attack Prevention:
localhost.evil.com→false(subdomain attack)evil-localhost.com→false(similar name attack)http://[email protected]→false(userinfo attack)
Validation Points:
Authorization request: Validate before signing state
Callback handler: Re-validate after HMAC verification (defense in depth)
Deployment Architecture
Kubernetes Production Deployment
Infrastructure Components:
Ingress: Terminates TLS, must set
X-Forwarded-Proto: httpsheaderMultiple Pods: Horizontal scaling with shared JWT_SECRET from Kubernetes Secret
Service: ClusterIP for internal load balancing
Secrets: Store jwtSecret and clientSecret securely
Network Flow:
Critical Configuration:
All pods must mount same jwtSecret for state verification
Ingress must set X-Forwarded-Proto header for HTTPS detection
OAuth credentials stored in Kubernetes Secrets, not ConfigMaps
Bug Fixes & Troubleshooting
Issue 1: Incorrect Okta JWKS URL
Problem:
JWKS endpoint was using
{issuer}/.well-known/jwks.jsonOkta returns 404 for this path
Correct Okta path is
{issuer}/oauth2/v1/keys
Symptoms:
mcp-remote fails with "JWKS endpoint error"
Claude Code shows 502 Bad Gateway when accessing JWKS
Solution:
Before: {issuer}/.well-known/jwks.json → 404
After: {issuer}/oauth2/v1/keys → 200 OKFiles Fixed:
internal/oauth/handlers.go:211internal/oauth/metadata.go:296
Issue 2: Protected Resource Metadata Mode Mismatch
Problem: The /.well-known/oauth-protected-resource endpoint always returned OAuth provider URL in authorization_servers, even when configured in proxy mode.
Impact:
mcp-remote received:
"authorization_servers": ["https://okta.com"]mcp-remote tried to register with Okta directly
Okta returned:
403 Invalid session(no valid session with Okta)Client unable to complete OAuth flow
Solution: Mode-aware response:
Proxy Mode:
"authorization_servers": ["{mcp-server-url}"]→ Client talks to MCP serverNative Mode:
"authorization_servers": ["{okta-url}"]→ Client talks to Okta directly
File Fixed: internal/oauth/metadata.go:126-136
Issue 3: Missing JWT_SECRET in Multi-Pod Deployment
Problem: Without configured jwtSecret, each pod generates its own random signing key:
Pod A signs state during authorization
Pod B receives callback, uses different key
Signature verification fails → "Invalid state parameter"
Symptoms:
Intermittent "Invalid state parameter" errors
Errors occur randomly (depends on which pod handles callback)
Error rate increases with more pod replicas
Solution: Configure jwtSecret in Helm values:
trino:
oauth:
jwtSecret: "$(openssl rand -hex 32)" # Same across all podsThis ensures all pods use the same HMAC signing key for state parameters.
Troubleshooting Guide
Common Error Messages
"Invalid state parameter"
Cause: JWT_SECRET not configured or differs across pods
Solution: Set jwtSecret in Helm values, redeploy
Verification: Check all pods have same JWT_SECRET env var
"403 Invalid session" from Okta
Cause: Protected resource metadata pointing to wrong authorization server
Solution: Verify OAUTH_MODE=proxy is set correctly
Verification: Check
/.well-known/oauth-protected-resourcereturns MCP server URL
"JWKS endpoint error" (502)
Cause: Incorrect Okta JWKS URL
Solution: Deploy version with fixed JWKS path
Verification: Test
/.well-known/jwks.jsonreturns public keys
"Fixed redirect mode only allows localhost"
Cause: Trying to use production redirect URI in fixed mode
Solution: Either use localhost callback OR switch to allowlist mode
Verification: Check OAUTH_REDIRECT_URI contains comma (allowlist) or is single URL (fixed)
"HTTPS required for OAuth endpoints"
Cause: Ingress not setting X-Forwarded-Proto header
Solution: Configure ingress to set
X-Forwarded-Proto: httpsVerification: Check request headers at pod level
Error Resolution Flowchart
OAuth 2.0 Compliance
Implemented Standards
RFC 6749
OAuth 2.0 Core
✅ Full
Authorization code flow
RFC 7636
PKCE
✅ Supported
Optional but recommended
RFC 8414
Metadata
✅ Full
Discovery endpoints
RFC 7591
Dynamic Registration
✅ Full
Client registration
RFC 9728
Protected Resource
✅ Full
Resource metadata
Security Best Practices Compliance
Exact redirect URI matching
✅
Allowlist mode
State parameter CSRF protection
✅
Required + HMAC-signed in fixed mode
PKCE for public clients
✅
Supported, recommended
TLS/HTTPS enforcement
✅
Non-localhost URIs
Constant-time comparisons
✅
HMAC verification
Input validation
✅
Length limits, format checks
Defense in depth
✅
Multiple validation layers
Client Compatibility
Tested Clients
MCP Inspector (Browser-based)
✅ OAuth discovery via 401 response
✅ Dynamic client registration
✅ Localhost callback (http://localhost:6274)
✅ PKCE flow
Status: Fully working
mcp-remote CLI
✅ Automatic port selection
✅ OAuth discovery
✅ Client registration
✅ Localhost callback with dynamic port
Status: Working after bug fixes
Claude Code
✅ IDE integration
✅ OAuth discovery
✅ mcp-remote transport
Status: Working after bug fixes
Generic OAuth 2.0 Clients
✅ Standard OAuth 2.0 flow
✅ PKCE support
⚠️ Must use localhost in fixed mode OR be in allowlist
Production Deployment Recommendations
Required Configuration Checklist
Pre-Deployment:
Runtime Monitoring:
Security Recommendations
High Priority:
Mandatory PKCE: Consider enforcing PKCE for all clients (OAuth 2.1 recommendation)
Rate Limiting: Add rate limiting to OAuth endpoints (prevent DoS)
JWT_SECRET Rotation: Implement key rotation strategy
Medium Priority:
Structured audit logging for security events
Metrics/monitoring dashboards for OAuth operations
Session timeouts for token exchange flows
Low Priority:
JWT client assertion support (public key/private key authentication)
Token introspection endpoint
Dynamic client registry with persistence
Last updated
Was this helpful?