Skip to content

[BUG]: OAuth audience verification always fails with IdPs that do not support RFC 8707 Resource Indicators (e.g. Authentik) #4171

@kimsehwan96

Description

@kimsehwan96

🐞 Bug Summary

OAuth access token audience verification fails with Audience doesn't match when using an IdP that does not support RFC 8707 Resource Indicators (e.g. Authentik). The hardened audience enforcement introduced in PR #3715 always derives the expected audience from APP_DOMAIN, but IdPs without RFC 8707 support set the token aud claim to the OAuth client_id instead of the resource URL. This makes it impossible to pass audience verification when APP_DOMAIN differs from the public-facing domain (e.g. reverse proxy or split internal/external endpoints), even though the token is otherwise valid.


🧩 Affected Component

Select the area of the project impacted:

  • mcpgateway - API
  • mcpgateway - UI (admin panel)
  • mcpgateway.wrapper - stdio wrapper
  • Federation or Transports
  • CLI, Makefiles, or shell scripts
  • Container setup (Docker/Podman/Compose)
  • Other (explain below)

🔁 Steps to Reproduce

  1. Configure a Virtual Server with oauth_enabled=true and an authorization server pointing to an IdP that does not support RFC 8707 Resource Indicators (e.g. Authentik, or any OIDC provider where resource_indicators_supported is absent from /.well-known/openid-configuration).

  2. Set APP_DOMAIN to an internal domain (e.g. https://internal-gateway.example.com) while exposing the MCP endpoint on a different public domain (e.g. https://mcp.example.com) via reverse proxy, separate ingress, or domain splitting.

  3. Have an MCP client (e.g. Claude.ai custom connector) connect to https://mcp.example.com/servers/{server_id}/mcp. The client completes the OAuth 2.1 flow with the IdP, sending resource=https://mcp.example.com/servers/{server_id}/mcp in the authorization request.

  4. The IdP ignores the resource parameter (no RFC 8707 support) and issues an access token with:

    {
      "aud": "my-oauth-client-id",
      "iss": "https://idp.example.com/application/o/my-app/",
      "sub": "user@example.com",
      ...
    }
  5. The MCP client sends the token to https://mcp.example.com/servers/{server_id}/mcp.

  6. ContextForge builds expected_audiences from APP_DOMAIN:

    resource_url = f"{settings.app_domain}/servers/{server_id}/mcp"
    # → "https://internal-gateway.example.com/servers/{server_id}/mcp"
    expected_audiences = [resource_url]
  7. Token verification fails:

    expected_audiences = ["https://internal-gateway.example.com/servers/{server_id}/mcp"]
    actual token aud   = "my-oauth-client-id"
    → Audience doesn't match → 401 Unauthorized
    

🤔 Expected Behavior

The operator should be able to configure audience verification behavior per Virtual Server to accommodate IdPs that do not support RFC 8707 Resource Indicators. Specifically:

  • A per-server toggle (e.g. include_client_id_in_audience in oauth_config) that automatically adds the SSO client_id to the expected_audiences list.
  • When enabled, expected_audiences would be [resource_url, sso_client_id], allowing tokens with aud=client_id to pass verification.
  • The existing oauth_config.resource workaround requires direct DB manipulation since the field is not exposed in the admin UI. A UI-accessible toggle would be the proper solution.

📓 Logs / Error Output

WARN [mcpgateway.utils.verify_credentials] OAuth access token verification failed
  (issuer=https://idp.example.com/application/o/my-app/): Audience doesn't match

This repeats for every MCP request. The token signature and issuer are valid — only the aud claim fails to match.

IdP OIDC Discovery (/.well-known/openid-configuration):

  • resource_indicators_supported field is absent (confirming no RFC 8707 support)
  • IdP developer confirmed: "the aud claim is set to the provider's client_id by default"

Audience verification code path (streamablehttp_transport.py ~L3999):

resource_url = _build_server_resource_url(self.scope, server_id)
# → Always derived from settings.app_domain, not the inbound Host header

expected_audiences: list[str] = [resource_url]
extra_audience = server.oauth_config.get("resource") or server.oauth_config.get("client_id")
# ... extend expected_audiences ...

claims = await verify_oauth_access_token(token, authorization_servers, expected_audience=expected_audiences)

The resource_url is always {APP_DOMAIN}/servers/{id}/mcp. When APP_DOMAIN differs from the public domain AND the IdP sets aud=client_id (not the resource URL), there is no matching audience.

Current workaround: Manually insert client_id into oauth_config.resource via direct SQL:

UPDATE servers SET oauth_config = '{"authorization_servers": [...], "resource": ["https://mcp.example.com/...", "my-oauth-client-id"]}'::json WHERE id = '...';

This works but requires DB access and is not documented.


🧠 Environment Info

You can retrieve most of this from the /version endpoint.

Key Value
Version or commit main
Runtime Python 3.12, Uvicorn
Platform / OS EKS (Amazon Linux 2, arm64)
Container Custom Containerfile build from upstream main
IdP Authentik (no RFC 8707 support)

🧩 Additional Context (optional)

Why this matters: The MCP specification (2025-11-25) requires clients to implement RFC 8707 Resource Indicators, but many widely-used IdPs do not support it on the server side:

  • Authentik: No RFC 8707 support. aud = client_id always.
  • Keycloak: RFC 8707 support is still in discussion.
  • Many smaller OIDC providers also lack RFC 8707 support.

The domain-split scenario is common in production: organizations often expose an internal admin UI on one domain (behind VPN) and a public MCP API endpoint on a different domain (with IP allowlisting). APP_DOMAIN cannot serve both purposes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriageIssues / Features awaiting triage

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions