OAuth 2.0 IntegrationΒΆ
This guide explains how to configure and operate OAuth 2.0 authentication for ContextForge when connecting to MCP servers or downstream APIs on behalf of users or services.
Related design docs:
- Architecture: oauth-design.md
- UI Flow: oauth-authorization-code-ui-design.md
What You GetΒΆ
- Client Credentials and Authorization Code flows
- Per-gateway OAuth configuration with encrypted client secrets
- Fresh tokens on demand (no caching by default)
- Optional token storage and refresh for user flows (per design)
- Admin UI support for configuring providers
Tip
OAuth is configured per Gateway. Tools that route through a gateway configured for OAuth inherit the token behavior.
Supported FlowsΒΆ
-
Client Credentials (machine-to-machine)
-
Uses client ID/secret to fetch access tokens
-
Best for service integrations without user consent
-
Authorization Code (user delegation)
-
Redirects the user to the provider for consent
- Exchanges code for access token, with optional refresh tokens
See the flow details and security model in the architecture docs.
PrerequisitesΒΆ
- An OAuth 2.0 provider (e.g., GitHub, Google, custom OIDC)
-
A registered application with:
-
Client ID and Client Secret
- Authorization URL and Token URL
- Redirect URI pointing to the gateway callback (for Authorization Code)
Environment VariablesΒΆ
# OAuth HTTP behavior
OAUTH_REQUEST_TIMEOUT=30 # Seconds
OAUTH_MAX_RETRIES=3 # Retries for transient failures
# Secret encryption for stored OAuth client secrets (and tokens if enabled)
AUTH_ENCRYPTION_SECRET=<strong-random-key>
Important
Always run ContextForge over HTTPS when using OAuth. Never transmit client secrets or authorization codes over insecure channels.
Configure a Gateway (Admin UI)ΒΆ
- Open Admin UI β Gateways β New Gateway (or Edit).
- Set Authentication type = OAuth.
-
Choose Grant Type:
-
client_credentials
-
authorization_code
-
Fill fields:
-
Client ID
- Client Secret (stored encrypted at rest)
- Token URL
- Scopes (space-separated)
- Audience (optional, for Atlassian, Auth0, and other non-RFC-8707 providers that use an
audiencerequest parameter) - Resource (optional, RFC 8707 audience indicator β see OAuth Resource Configuration)
-
Authorization URL and Redirect URI (required for Authorization Code)
-
Save.
Field mapping follows the architecture proposal and is used by the OAuth Manager service to request tokens.
Resource (Audience) Configuration
The Resource field controls the OAuth 2.0 resource parameter (RFC 8707) and token audience validation. In most cases you can leave it empty and let ContextForge auto-derive (from the gateway URL origin) and auto-learn (from the first successful token) the correct value. See OAuth Resource Configuration for when to set it explicitly and how to force a re-learn.
Configure a Gateway (JSON/API)ΒΆ
Example OAuth-enabled gateway record:
{
"name": "GitHub MCP",
"url": "https://github-mcp.example.com/sse",
"auth_type": "oauth",
"oauth_config": {
"grant_type": "authorization_code",
"client_id": "your_github_app_id",
"client_secret": "your_github_app_secret",
"authorization_url": "https://github.com/login/oauth/authorize",
"token_url": "https://github.com/login/oauth/access_token",
"redirect_uri": "https://gateway.example.com/oauth/callback",
"scopes": ["repo", "read:user"]
}
}
For Client Credentials, omit authorization_url and redirect_uri and set grant_type to client_credentials.
Resource Parameter and omit_resourceΒΆ
By default ContextForge derives an RFC 8707 resource parameter from the gateway URL and includes it in token requests and refresh calls. Some IdPs (e.g. certain Atlassian configurations) reject requests that include the resource parameter. Set omit_resource: true to suppress it:
{
"oauth_config": {
"grant_type": "authorization_code",
"client_id": "your-client-id",
"token_url": "https://auth.example.com/token",
"omit_resource": true
}
}
When omit_resource is true, the resource parameter is removed before every token request and refresh call, regardless of whether a resource value was explicitly configured or auto-derived from the gateway URL.
Audience ParameterΒΆ
Some OAuth providers (Atlassian, Auth0, etc.) require an audience parameter instead of or in addition to the RFC 8707 resource parameter:
{
"name": "Atlassian MCP",
"url": "https://atlassian-mcp.example.com/sse",
"auth_type": "oauth",
"oauth_config": {
"grant_type": "authorization_code",
"client_id": "your_atlassian_app_id",
"client_secret": "your_atlassian_app_secret",
"authorization_url": "https://auth.atlassian.com/authorize",
"token_url": "https://auth.atlassian.com/oauth/token",
"redirect_uri": "https://gateway.example.com/oauth/callback",
"audience": "api.atlassian.com",
"scopes": ["read:jira-work", "write:jira-work"]
}
}
The audience parameter: - Is included in both authorization and token exchange requests - Can coexist with resource parameter for providers that accept both - When set without resource, the RFC 8707 resource parameter is automatically omitted
Resource Parameter (RFC 8707) β per-user audience learning, origin fallback, advisory validationΒΆ
See also
For a task-oriented guide (provider patterns, multi-resource configs, forcing a re-learn), see OAuth Resource Configuration. This section summarises the behaviour and links back to the implementation details.
The gateway resolves the expected token audience via a three-level precedence, with per-user isolation on the learned tier:
- Explicit
oauth_config.resourceβ admin sets it (single URI or list) via the Admin UI "Resource (Audience)" field or via the API. Authoritative and blocking. Global to the gateway. - Per-user
OAuthToken.learned_audβ on each user's OAuth callback, the gateway decodes the access token'saudandissclaims (best-effort, unverified) and stores them on that user's own token row. Subsequent validation for THAT USER checks their token against their own learned value. Authoritative and blocking for that user only. - Auto-derived gateway URL origin β if neither of the above applies, the gateway uses
scheme://netlocfrom the gateway URL as an advisory fallback. Most providers (Salesforce, Azure AD, Okta) issue origin-level audiences, so this maximises first-authentication success.
Why per-user (and not per-gateway)? The earlier design stored learned audience on shared gateway.oauth_config.resource. That created two problems: (a) a single gateway serving multiple IdP tenants with per-tenant aud values would have its resource pinned by whichever user authenticated first, locking out other tenants; and (b) the OAuth callback path only enforces gateway access, not gateways.update, so callback-driven writes to shared config let users without the update permission mutate global state. Per-user storage eliminates both.
Outbound resource parameter. The RFC 8707 resource value sent to the IdP is still sourced from oauth_config.resource (or the origin fallback if unset). It is not derived from any user's learned value β outbound requests are the same regardless of who initiated them.
Advisory vs. blocking audience validation.
| Expected audience source | Mismatch severity | Behavior |
|---|---|---|
Explicitly configured oauth_config.resource | Blocking | GatewayConnectionError raised, token not forwarded |
Per-user OAuthToken.learned_aud | Blocking (for this user) | GatewayConnectionError raised, token not forwarded |
| Auto-derived gateway URL origin (fallback) | Advisory (default) | Warning logged, token forwarded β upstream MCP server is the authority |
The advisory fallback only applies to users with no learned value (first authentication) and no admin-configured resource. If you cannot rely on the upstream MCP server to validate aud, enable strict mode to make even the fallback authoritative:
Triggering re-learning.
- For one user: they re-authenticate. The next OAuth callback overwrites their
OAuthToken.learned_audwith the current audience. No admin action needed. - For admin-configured
oauth_config.resource: edit the gateway in the Admin UI and blank the Resource field (or PUT{"oauth_config": {"resource": null}}via API).
Redirect URI and CallbackΒΆ
- Default callback path:
/oauth/callback - Your provider must whitelist the full redirect URI, e.g.
https://gateway.example.com/oauth/callback - The gateway handles exchanging the authorization code for an access token and applies it as
Authorization: Bearer <token>when contacting the MCP server
Popup OAuth Flow (React UI)ΒΆ
The React UI can initiate OAuth in a popup window instead of a full-page redirect:
Endpoint: GET /oauth/authorize/{gateway_id}?popup=true
Behavior: - Opens OAuth provider in a popup window - Callback responds with postMessage instead of HTML page - Parent window receives result and closes popup automatically
postMessage Payload Structure:
Success:
{
type: "oauth_callback",
status: "success",
gatewayId: "gateway-uuid",
gatewayName: "Gateway Name"
}
Error:
{
type: "oauth_callback",
status: "error",
error: "error_code",
errorDescription: "Human-readable description"
}
Error Codes: - access_denied - User denied authorization at provider - missing_code - Authorization code missing from callback - invalid_state - State parameter invalid or expired - oauth_error - OAuth protocol error (see errorDescription) - server_error - Unexpected server error
React Integration Pattern:
// Open popup
const authWindow = window.open(
`/oauth/authorize/${gatewayId}?popup=true`,
'oauth-popup',
'width=600,height=700'
);
// Listen for result
window.addEventListener('message', (event) => {
// Security: Validate event.source matches popup
if (event.source !== authWindow) return;
const { type, status, error, errorDescription } = event.data;
if (type === 'oauth_callback') {
if (status === 'success') {
// Handle success
console.log('OAuth successful');
} else {
// Handle error
console.error(`OAuth failed: ${error} - ${errorDescription}`);
}
}
});
Security Notes: - Callback uses postMessage(..., '*') for cross-origin compatibility - Receiver MUST validate event.source === authWindow (exact popup reference) - State token includes popup. prefix for callback mode detection - CSP nonce is embedded in callback script for strict CSP compliance
Sequence (Authorization Code):
sequenceDiagram
participant User
participant Gateway as ContextForge
participant OAuth as OAuth Provider
participant MCP as MCP Server
User->>Gateway: Request OAuth-enabled operation
Gateway->>Gateway: Generate authorization URL
Gateway-->>User: 302 Redirect to provider
User->>OAuth: Login and consent
OAuth-->>Gateway: Redirect (code, state)
Gateway->>OAuth: POST /token (code)
OAuth-->>Gateway: Access token (Β±refresh)
Gateway->>MCP: Request with Bearer token
MCP-->>Gateway: Response
Gateway-->>User: Result Token Storage and RefreshΒΆ
OAuth tokens are stored per gateway and user for the Authorization Code flow to ensure proper security isolation:
- User-Scoped Tokens: OAuth tokens are scoped per ContextForge user (using
app_user_emailfield) to prevent token sharing between users - Store tokens per gateway + user combination with unique constraints
- Auto-refresh using refresh tokens when near expiry
- Encrypt tokens at rest using
AUTH_ENCRYPTION_SECRET - Foreign key relationships ensure token cleanup when users are deleted
Security Enhancement
OAuth tokens are now user-scoped to prevent token sharing between users. Each Authorization Code flow token is tied to the specific ContextForge user who authorized it, providing better security isolation.
Token Deletion PolicyΒΆ
Stored tokens are deleted selectively β only when the Authorization Server explicitly signals that the refresh token is permanently invalid:
| Condition | Token action | RFC reference |
|---|---|---|
error: invalid_grant from token endpoint | Deleted β refresh token revoked or expired | RFC 6749 Β§5.2 |
error: invalid_client, invalid_request, etc. | Preserved β configuration error, not a token failure | RFC 6749 Β§5.2 |
client_secret decryption failure | Preserved β AUTH_ENCRYPTION_SECRET mismatch, not a token failure | β |
| Network / transient error | Preserved β retry on next cycle | β |
Tip
If a gateway's refresh loop is failing with invalid_client after rotating AUTH_ENCRYPTION_SECRET, the token is preserved but refresh cannot succeed until the secret is re-encrypted with the current key. Re-save the gateway in the Admin UI to re-encrypt the client_secret.
Health Checks for Authorization Code GatewaysΒΆ
Authorization Code gateways use per-user tokens β there is no platform-level service token. Health checks therefore verify service reachability, not token ownership:
- If no stored token is available for the health-check user, the probe is sent unauthenticated
- A 401 or 403 response is treated as "gateway reachable but unauthorized" β this is the expected response for an unauthenticated probe against a user-only upstream (e.g. Atlassian Remote MCP). The gateway is kept online.
- A connection failure (timeout, DNS error, TCP reset) is treated as a genuine outage and counts toward
UNHEALTHY_THRESHOLD - If a gateway was previously marked unreachable and then responds with 401/403, it is automatically reactivated
Note
PLATFORM_ADMIN_EMAIL is used as the default identity for health-check token lookups. If that user has not completed the OAuth consent flow for a given gateway, the probe proceeds unauthenticated β this is expected behaviour, not an error.
Provider ExamplesΒΆ
GitHub (Authorization Code)ΒΆ
- Authorization URL:
https://github.com/login/oauth/authorize - Token URL:
https://github.com/login/oauth/access_token - Scopes:
repo read:user - Redirect URI:
https://<your-domain>/oauth/callback
Atlassian (Authorization Code with Audience)ΒΆ
- Authorization URL:
https://auth.atlassian.com/authorize - Token URL:
https://auth.atlassian.com/oauth/token - Audience:
api.atlassian.com - Scopes:
read:jira-work write:jira-work read:confluence-content.all - Redirect URI:
https://<your-domain>/oauth/callback
Auth0 (Client Credentials with Audience)ΒΆ
- Token URL:
https://<your-tenant>.auth0.com/oauth/token - Audience:
https://your-api.example.com - Scopes: provider-specific
- No redirect required
Generic OIDC (Client Credentials)ΒΆ
- Token URL:
https://idp.example.com/oauth2/token - Scopes: provider-specific
- No redirect required
Security RecommendationsΒΆ
- Use least-privilege scopes
- Run behind HTTPS only (including callback)
- Rotate client secrets and avoid plaintext storage
- Restrict who can create/modify OAuth-configured gateways
- Monitor token fetch errors and rate limits from providers
AUTH_ENCRYPTION_SECRET and Stored SecretsΒΆ
- The
client_secretis always decrypted before use; there is no plaintext fallback when encryption is configured - If decryption fails (wrong key or corrupted ciphertext), the refresh attempt is aborted with an explicit error and the stored token is preserved for the next cycle
- Changing
AUTH_ENCRYPTION_SECRETinvalidates all stored encrypted secrets (client secrets, access tokens, refresh tokens) β after rotation, re-save every OAuth gateway in the Admin UI to re-encrypt with the new key
See also: securing.md for general hardening guidance and proxy.md for fronting the gateway with an auth proxy.
TroubleshootingΒΆ
Common issues and quick fixes:
- 401 from MCP after OAuth: Verify scopes and that token is attached as
Authorization: Bearerby the gateway - Provider denies callback: Check exact Redirect URI match and HTTPS
- Invalid client: Confirm Client ID/Secret and application registration
- State mismatch / "Invalid or expired state parameter": See the detailed OAuth Troubleshooting Guide
- Timeouts: Increase
OAUTH_REQUEST_TIMEOUTor investigate provider availability
Detailed Troubleshooting
For comprehensive debugging steps, error message explanations, and state storage configuration, see the OAuth Troubleshooting Guide.
PKCE SupportΒΆ
ContextForge implements PKCE (Proof Key for Code Exchange) as defined in RFC 7636 for all Authorization Code flows. This provides enhanced security, especially for:
- Public clients (mobile apps, SPAs, desktop apps)
- Environments where client secrets cannot be securely stored
- Protection against authorization code interception attacks
How it works:
- Gateway generates a random
code_verifier(43-128 characters) - Computes
code_challenge= BASE64URL(SHA256(code_verifier)) - Sends
code_challengeandcode_challenge_method=S256in authorization request - Stores
code_verifierin OAuth state (encrypted at rest) - Includes
code_verifierwhen exchanging authorization code for token
PKCE is automatically enabled for all Authorization Code flows - no configuration needed.
FAQΒΆ
- Can I use PKCE? Yes! PKCE is automatically enabled for all Authorization Code flows (RFC 7636).
- Can I configure per-tool OAuth? Roadmap considers multiple OAuth configs per tool; current design is per-gateway.
- Do you cache tokens? Default is no caching; tokens are fetched per operation. Optional storage/refresh is available for Authorization Code flows.