feat: harden Claude MCP OAuth transport

This commit is contained in:
2026-06-13 21:05:11 +02:00
parent ed3130ef74
commit 541124e92a
11 changed files with 1377 additions and 68 deletions
+26 -6
View File
@@ -177,6 +177,29 @@ class GiteaOAuthValidator:
self._jwks_cache[jwks_uri] = (jwks, now + self.settings.oauth_cache_ttl_seconds)
return jwks
def _acceptable_audiences(self) -> list[str]:
"""Return the set of OIDC audiences this MCP server will accept.
Per the MCP authorization spec (RFC 8707 / RFC 9728) tokens are bound to
the MCP server's canonical resource URL, so the configured public base is
the primary accepted audience. The upstream Gitea OAuth client id is also
accepted because Gitea — the actual token issuer behind this proxy —
stamps ``aud`` with the client id rather than the MCP resource URL. An
operator may add a further required audience via OAUTH_EXPECTED_AUDIENCE.
"""
audiences: list[str] = []
canonical_resource = self.settings.public_base
if canonical_resource:
audiences.append(canonical_resource)
gitea_client_id = self.settings.gitea_oauth_client_id.strip()
if gitea_client_id:
audiences.append(gitea_client_id)
configured = self.settings.oauth_expected_audience.strip()
if configured:
audiences.append(configured)
# Preserve order while removing duplicates.
return list(dict.fromkeys(audiences))
async def _validate_jwt(self, token: str) -> dict[str, Any]:
"""Validate JWT access token using OIDC discovery and JWKS."""
discovery = await self._get_discovery_document()
@@ -216,19 +239,16 @@ class GiteaOAuthValidator:
"oauth_jwt_invalid_jwk",
) from exc
expected_audience = (
self.settings.oauth_expected_audience.strip()
or self.settings.gitea_oauth_client_id.strip()
)
accepted_audiences = self._acceptable_audiences()
decode_options = cast(Any, {"verify_aud": bool(expected_audience)})
decode_options = cast(Any, {"verify_aud": bool(accepted_audiences)})
try:
claims = jwt.decode(
token,
key=cast(Any, public_key),
algorithms=["RS256"],
issuer=issuer,
audience=expected_audience or None,
audience=accepted_audiences or None,
options=decode_options,
)
except InvalidTokenError as exc: