feat: harden Claude MCP OAuth transport
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user