OAuth 2.0 Authorization Code Flow UI Implementation DesignΒΆ
Version: 1.2 Status: Design + implementation notes Date: February 2026 Related: OAuth Design Document
Executive SummaryΒΆ
This document describes how the Admin UI initiates OAuth 2.0 Authorization Code flow for MCP gateways and how the backend stores and uses user-delegated tokens. It reflects current behavior in code and calls out remaining gaps.
Scope of This Document
This document covers the UI implementation for gateway OAuth token delegation - specifically how administrators configure OAuth settings and authorize access to upstream MCP servers.
For information about user authentication to ContextForge (SSO, JWT tokens, RBAC), see:
- RBAC Configuration - Token scoping, permissions, and access control
- Multi-Tenancy Architecture - User authentication flows and team management
Current Implementation SnapshotΒΆ
Implemented todayΒΆ
- Admin UI exposes OAuth configuration fields for gateways and an "Authorize" action.
- Authorization Code flow uses PKCE (S256) and an HMAC-signed state value with a 300-second TTL.
- OAuth state is stored in Redis when
CACHE_TYPE=redis, in the database whenCACHE_TYPE=database, and in memory otherwise. - Tokens are stored per gateway and app user (email) in
oauth_tokens, encrypted withAUTH_ENCRYPTION_SECRET. - Refresh tokens are used when access tokens are near expiry; invalid refresh tokens are cleared.
- DCR auto-registration can run during
/oauth/authorize/{gateway_id}whenissueris set andclient_idis missing.
Known gaps and constraintsΒΆ
oauth_store_tokensandoauth_auto_refreshcheckboxes exist in the UI but are not persisted or enforced by the backend.OAUTH_PREFERRED_CODE_CHALLENGE_METHODis defined but PKCE is always S256 today.- No admin UI to list or revoke stored OAuth tokens per user.
- Token cleanup is a helper method only; there is no scheduler invoking it.
Architecture OverviewΒΆ
graph TD
subgraph "Admin UI"
A[Gateway OAuth Configuration]
B[Authorize Button]
end
subgraph "Backend"
C[OAuth Router]
D[OAuth Manager]
E[Token Storage Service]
end
subgraph "State Store"
S[Redis / Database / Memory]
end
subgraph "Database"
G[gateways.oauth_config]
T[oauth_tokens]
ST[oauth_states when CACHE_TYPE=database]
end
subgraph "External"
U[User Browser]
O[OAuth Provider]
end
A --> G
B --> C
C --> D
D --> S
D --> O
O --> C
C --> E
E --> T
E --> ST
B --> U
U --> O Authorize redirects to /oauth/authorize/{gateway_id}. The OAuth Manager performs PKCE and state handling.
Data ModelΒΆ
Gateway OAuth configuration (gateways.oauth_config)ΒΆ
Stored as JSON and assembled from Admin UI fields or API payloads.
grant_type:authorization_code,client_credentials, orpasswordissuer: OAuth Authorization Server issuer URL (required for DCR)authorization_url: OAuth authorization endpointtoken_url: OAuth token endpointredirect_uri: Must match the OAuth client registrationclient_id: OAuth client identifierclient_secret: Encrypted before storageusername/password: Used only for password grantscopes: Array of requested scopesresource(optional): RFC 8707 resource parameter; when omitted,/oauth/authorizederives it from the gateway URL
oauth_tokensΒΆ
One token record per gateway and app user (email).
gateway_id,app_user_email(unique pair)user_id(OAuth provider user identifier)access_token,refresh_token(encrypted at rest)token_type,expires_at,scopescreated_at,updated_at
oauth_states (database-backed state storage)ΒΆ
Used only when CACHE_TYPE=database.
gateway_id,state(unique)code_verifier(PKCE)expires_at,used,created_at- TTL is enforced in logic (300 seconds)
registered_oauth_clients (DCR, optional)ΒΆ
Stored when Dynamic Client Registration succeeds.
issuer,client_id,client_secret_encryptedredirect_uris,grant_types,response_types,scopetoken_endpoint_auth_method,registration_client_uricreated_at,expires_at,is_active
UI and FlowΒΆ
Admin UI touchpointsΒΆ
The gateway form maps these inputs into oauth_config:
oauth_grant_typeoauth_issueroauth_client_idoauth_client_secretoauth_authorization_urloauth_token_urloauth_redirect_urioauth_scopesoauth_username/oauth_password(password grant only)
The gateway list shows an Authorize button for OAuth gateways, which sends the current user through /oauth/authorize/{gateway_id}.
Authorization Code flow (current)ΒΆ
sequenceDiagram
participant Admin
participant UI as Admin UI
participant Gateway as OAuth Router (/oauth/authorize)
participant OAuthMgr as OAuth Manager
participant StateStore as State Store
participant Provider as OAuth Provider
participant TokenStore as Token Storage Service
participant DB as Database
Admin->>UI: Configure gateway OAuth settings
UI->>DB: Save oauth_config in gateways
Admin->>UI: Click "Authorize"
UI->>Gateway: GET /oauth/authorize/{gateway_id}
Gateway->>OAuthMgr: Initiate auth code flow (PKCE)
OAuthMgr->>StateStore: Store state + code_verifier (TTL 300s)
OAuthMgr-->>Gateway: Authorization URL
Gateway-->>Admin: Redirect to provider
Admin->>Provider: Login + consent
Provider-->>Gateway: Redirect /oauth/callback?code&state
Gateway->>OAuthMgr: Validate state + exchange code
OAuthMgr->>Provider: POST /token (code + code_verifier)
Provider-->>OAuthMgr: access_token + refresh_token
OAuthMgr->>TokenStore: Store tokens for app_user_email
TokenStore->>DB: Upsert oauth_tokens
Gateway-->>Admin: Success page Tool invocation using stored tokensΒΆ
sequenceDiagram
participant Client as MCP Client (authenticated user)
participant Gateway
participant TokenStore as Token Storage Service
participant Provider as OAuth Provider
participant MCP as MCP Server
Client->>Gateway: Invoke tool
Gateway->>TokenStore: Get token for gateway + app_user_email
TokenStore->>TokenStore: Check expiration
alt Token valid
TokenStore-->>Gateway: Decrypted access token
Gateway->>MCP: Tool request + Bearer token
MCP-->>Gateway: Tool response
Gateway-->>Client: Result
else Token expired
TokenStore->>Provider: Refresh token request
Provider-->>TokenStore: New access token
TokenStore->>DB: Update oauth_tokens
TokenStore-->>Gateway: New access token
Gateway->>MCP: Tool request + Bearer token
MCP-->>Gateway: Tool response
Gateway-->>Client: Result
end Security and Operational NotesΒΆ
- Tokens are encrypted at rest using
AUTH_ENCRYPTION_SECRET(via the encryption service). - State is HMAC-signed with
AUTH_ENCRYPTION_SECRET, single-use, and expires after 300 seconds. - Tokens are scoped per gateway and app user (email); cross-user reuse is blocked.
- The gateway derives an RFC 8707
resourcevalue from the gateway URL whenoauth_config.resourceis absent. - HTTPS is recommended in production; the gateway does not enforce it automatically.
Relationship to Gateway AuthenticationΒΆ
This OAuth flow is for upstream server authentication and is separate from user authentication to ContextForge:
| Aspect | Gateway OAuth (this doc) | User Auth to Gateway |
|---|---|---|
| Purpose | Authenticate to upstream MCP servers | Authenticate users to the gateway |
| Initiated by | Admin UI "Authorize" button | Login flow (SSO, email, etc.) |
| Token storage | oauth_tokens table (encrypted) | JWT in client, session in browser |
| User involvement | User consents at OAuth provider | User authenticates to gateway |
For user authentication and RBAC configuration, see RBAC Configuration.
ConfigurationΒΆ
| Setting | Default | Notes |
|---|---|---|
OAUTH_REQUEST_TIMEOUT | 30 | Timeout for OAuth HTTP requests |
OAUTH_MAX_RETRIES | 3 | Retry count for token exchanges |
OAUTH_DEFAULT_TIMEOUT | 3600 | Default expires_in when provider omits it |
AUTH_ENCRYPTION_SECRET | my-test-salt | Encrypts OAuth tokens and signs state |
CACHE_TYPE | database | redis, database, memory, or none |
REDIS_URL | redis://localhost:6379 | Required when CACHE_TYPE=redis |
DATABASE_URL | sqlite:///./mcp.db | Required when CACHE_TYPE=database |
DCR_ENABLED | true | Enable Dynamic Client Registration |
DCR_AUTO_REGISTER_ON_MISSING_CREDENTIALS | true | Auto-register when issuer is set but client_id is missing |
DCR_DEFAULT_SCOPES | ["mcp:read"] | Default scopes for DCR |
DCR_ALLOWED_ISSUERS | [] | Allowlist of issuer URLs |
DCR_TOKEN_ENDPOINT_AUTH_METHOD | client_secret_basic | Token endpoint auth method |
DCR_METADATA_CACHE_TTL | 3600 | Cache TTL for AS metadata discovery |
DCR_CLIENT_NAME_TEMPLATE | ContextForge ({gateway_name}) | DCR client_name template |
DCR_REQUEST_REFRESH_TOKEN_WHEN_UNSUPPORTED | false | Request refresh token even if AS metadata omits support |
OAUTH_DISCOVERY_ENABLED | true | Enable RFC 8414 discovery |
OAUTH_PREFERRED_CODE_CHALLENGE_METHOD | S256 | Defined but currently unused; PKCE is always S256 |
Validation ChecklistΒΆ
- Create an OAuth gateway with
authorization_codegrant and complete provider consent via Authorize. - Confirm an
oauth_tokensrecord exists for your user and gateway. - Invoke a tool to verify the stored token is used.
- Let the access token expire and confirm refresh happens when a refresh token is available.
- Switch
CACHE_TYPEbetweendatabaseandredisto confirm state storage works in both modes.
Future EnhancementsΒΆ
- Wire
oauth_store_tokensandoauth_auto_refreshto backend behavior. - Make PKCE method configurable using
OAUTH_PREFERRED_CODE_CHALLENGE_METHOD. - Add admin UI for token status and revocation.
- Schedule periodic cleanup of expired OAuth tokens.