Skip to content

[EPIC][SECURITY]: OAuth 2.0 Token Exchange (RFC 8693) β€” User Delegation for Virtual ServersΒ #3385

Description

@jonpspri

🧭 Type of Feature

  • New feature or capability
  • Enhancement to existing functionality

🧭 Epic

Title: OAuth 2.0 Token Exchange (RFC 8693) β€” User Delegation for Virtual Servers
Goal: Allow ContextForge Virtual Servers to forward MCP tool/resource requests to upstream MCP servers using OAuth 2.0 Token Exchange (on-behalf-of), so that downstream actions carry the identity and scoped permissions of the original authenticated user β€” not a shared service account.
Why now: Multi-user MCP clients like LibreChat authenticate end-users via OAuth against ContextForge Virtual Servers (using the existing RFC 9728 + Authorization Code flow). When ContextForge then fans out to upstream MCP servers that also require OAuth, the current implementation has no way to exchange the user's inbound token for an audience-restricted token scoped to the upstream server. This forces operators to either share a single service credential (losing per-user auditability) or bypass OAuth on upstream servers entirely.


Background & Standards

Standard Role
RFC 8693 β€” OAuth 2.0 Token Exchange Defines urn:ietf:params:oauth:grant-type:token-exchange grant type for trading one token for another
RFC 8707 β€” Resource Indicators Audience-scoping the exchanged token to a specific upstream resource
RFC 9728 β€” OAuth Protected Resource Metadata Already implemented in ContextForge for advertising OAuth requirements
MCP Authorization Spec (2025-11-25) MCP's use of OAuth 2.1, resource indicators, and protected resource metadata

Delegation vs. Impersonation (RFC 8693 Β§1.1):

  • Delegation (preferred): The exchanged token carries both a sub (original user) and act (ContextForge service) claim, creating a clear audit trail.
  • Impersonation: The exchanged token appears as if the original user issued it directly. Simpler but weaker auditability.

ContextForge should support delegation as the default and allow impersonation as an opt-in for IDPs that don't support the act claim.


Proposed Design

sequenceDiagram
    participant Client as LibreChat / MCP Client
    participant VS as ContextForge Virtual Server
    participant AS as Authorization Server (IDP)
    participant US as Upstream MCP Server

    Client->>VS: MCP request (Bearer user_token)
    VS->>VS: Validate user_token (existing JWT verification)
    VS->>AS: POST /token (grant_type=token-exchange,<br/>subject_token=user_token,<br/>resource=upstream_server_url,<br/>audience=upstream_client_id)
    AS-->>VS: exchanged_token (scoped to upstream, act claim)
    VS->>US: MCP request (Bearer exchanged_token)
    US-->>VS: MCP response
    VS-->>Client: MCP response
Loading

Gateway oauth_config Extension

{
  "grant_type": "token-exchange",
  "token_endpoint": "https://idp.example.com/oauth/token",
  "client_id": "contextforge-gateway",
  "client_secret": "encrypted:...",
  "resource": "https://upstream.example.com/mcp",
  "audience": "upstream-mcp-client-id",
  "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "scopes": ["tools:read", "tools:execute"]
}

πŸ™‹β€β™‚οΈ User Story 1 β€” Per-User Token Exchange on Tool Invocation

As a: platform administrator
I want: upstream MCP servers to receive a token scoped to the calling user's identity
So that: per-user audit trails, rate limits, and access policies are enforced end-to-end through the gateway

βœ… Acceptance Criteria

Scenario: Successful token exchange on tool call
  Given a Virtual Server with an upstream gateway configured for token-exchange
  And the upstream gateway's IDP supports RFC 8693
  When an authenticated user calls a tool via the Virtual Server
  Then ContextForge exchanges the user's inbound token for an upstream-scoped token
  And the upstream request carries the exchanged token with "act" claim identifying ContextForge
  And the tool response is returned to the user

Scenario: Token exchange failure falls back to error
  Given a Virtual Server with token-exchange configured
  When the IDP rejects the exchange (invalid_grant, scope mismatch)
  Then the tool call returns an error to the client with a clear message
  And the failure is logged with correlation ID

Scenario: Cached exchanged token is reused within validity window
  Given a previously exchanged token that is still valid
  When the same user calls another tool on the same upstream
  Then the cached exchanged token is reused without a new exchange request

πŸ™‹β€β™‚οΈ User Story 2 β€” Admin Configuration of Token Exchange

As a: platform administrator
I want: to configure token exchange parameters per upstream gateway via the Admin UI and API
So that: I can enable user delegation without modifying upstream server configurations

βœ… Acceptance Criteria

Scenario: Configure token exchange via Admin UI
  Given the gateway edit form in the Admin UI
  When the admin selects "Token Exchange" as the grant type
  Then the form shows fields for token_endpoint, client_id, client_secret, resource, audience
  And the client_secret is encrypted before storage

Scenario: Configure token exchange via API
  Given a PATCH /gateways/{id} request with oauth_config.grant_type = "token-exchange"
  When the request includes valid token_endpoint and client credentials
  Then the gateway configuration is updated
  And the oauth_config is validated for required fields

πŸ™‹β€β™‚οΈ User Story 3 β€” LibreChat Multi-User Delegation

As a: LibreChat user authenticating via OAuth to a ContextForge Virtual Server
I want: my identity to be preserved when ContextForge calls upstream MCP servers on my behalf
So that: upstream servers can enforce per-user policies and I see only my authorized tools/resources

βœ… Acceptance Criteria

Scenario: LibreChat user identity preserved through gateway
  Given LibreChat authenticates user via OAuth against a ContextForge Virtual Server
  And the Virtual Server fans out to upstream MCP servers with token-exchange
  When the user calls a tool
  Then the upstream server receives a token with sub=user and act=contextforge
  And team-scoped filtering applies based on the user's identity, not a service account

πŸ“ Implementation Considerations

  1. OAuthManager Extension: Add token-exchange grant type alongside existing authorization_code, client_credentials, and password flows in mcpgateway/services/oauth_manager.py.
  2. Token Caching: Cache exchanged tokens per (user, upstream_gateway) pair in the existing oauth_tokens table with appropriate TTL.
  3. Subject Token Extraction: Extract the inbound Bearer token from the MCP request context and pass it as subject_token to the exchange endpoint.
  4. Fallback Behavior: If token exchange fails and a client_credentials fallback is configured, optionally fall back to M2M token (see companion epic).
  5. Encryption: Reuse existing protect_oauth_config_for_storage() / decrypt_oauth_config_for_runtime() for client secrets.

πŸ”— MCP Standards Check

  • Change adheres to current MCP specifications β€” MCP Authorization spec mandates OAuth 2.1 with resource indicators
  • No breaking changes to existing MCP-compliant integrations β€” new grant type is additive
  • RFC 9728 metadata endpoints remain unchanged

πŸ”„ Alternatives Considered

Alternative Why not
Pass inbound token directly to upstream Audience mismatch; upstream server may reject tokens not issued for it
Use client_credentials for all upstream calls Loses per-user identity; no audit trail of which user triggered which action
Require each user to separately OAuth into each upstream Poor UX; ContextForge's value is transparent federation

πŸ““ Additional Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    apiREST API Related itemenhancementNew feature or requestepicLarge feature spanning multiple issuesmcp-protocolAlignment with MCP protocol or specificationsecurityImproves security

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions