🐞 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:
🔁 Steps to Reproduce
-
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).
-
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.
-
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.
-
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",
...
}
-
The MCP client sends the token to https://mcp.example.com/servers/{server_id}/mcp.
-
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]
-
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.
🐞 Bug Summary
OAuth access token audience verification fails with
Audience doesn't matchwhen 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 fromAPP_DOMAIN, but IdPs without RFC 8707 support set the tokenaudclaim to the OAuthclient_idinstead of the resource URL. This makes it impossible to pass audience verification whenAPP_DOMAINdiffers 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- APImcpgateway- UI (admin panel)mcpgateway.wrapper- stdio wrapper🔁 Steps to Reproduce
Configure a Virtual Server with
oauth_enabled=trueand an authorization server pointing to an IdP that does not support RFC 8707 Resource Indicators (e.g. Authentik, or any OIDC provider whereresource_indicators_supportedis absent from/.well-known/openid-configuration).Set
APP_DOMAINto 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.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, sendingresource=https://mcp.example.com/servers/{server_id}/mcpin the authorization request.The IdP ignores the
resourceparameter (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", ... }The MCP client sends the token to
https://mcp.example.com/servers/{server_id}/mcp.ContextForge builds
expected_audiencesfromAPP_DOMAIN:Token verification fails:
🤔 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:
include_client_id_in_audienceinoauth_config) that automatically adds the SSOclient_idto theexpected_audienceslist.expected_audienceswould be[resource_url, sso_client_id], allowing tokens withaud=client_idto pass verification.oauth_config.resourceworkaround 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
This repeats for every MCP request. The token signature and issuer are valid — only the
audclaim fails to match.IdP OIDC Discovery (
/.well-known/openid-configuration):resource_indicators_supportedfield is absent (confirming no RFC 8707 support)audclaim is set to the provider's client_id by default"Audience verification code path (
streamablehttp_transport.py~L3999):The
resource_urlis always{APP_DOMAIN}/servers/{id}/mcp. WhenAPP_DOMAINdiffers from the public domain AND the IdP setsaud=client_id(not the resource URL), there is no matching audience.Current workaround: Manually insert
client_idintooauth_config.resourcevia direct SQL:This works but requires DB access and is not documented.
🧠 Environment Info
You can retrieve most of this from the
/versionendpoint.main🧩 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:
aud=client_idalways.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_DOMAINcannot serve both purposes.