Compare commits

...

2 Commits

Author SHA1 Message Date
Evan
83952cf665 test(mcp): annotate types on unpinned-algorithm test
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:05:08 -07:00
Amin Ghadersohi
6dacfc14bd fix(mcp): fail closed when the JWT verifier has no pinned algorithm
DetailedJWTVerifier.load_access_token only rejected the "none" algorithm and
otherwise compared the token algorithm against self.algorithm — but only when
self.algorithm was truthy. The pinned value is currently always present because
the upstream JWTVerifier defaults it to RS256, so the verifier relied on that
upstream default for a security property.

Make the pinning explicit: reject tokens when no algorithm is pinned rather
than validating against an unconstrained algorithm family. This removes the
implicit dependency on the upstream default and fails closed if the pinned
algorithm is ever absent.

Adds a unit test asserting an unpinned verifier rejects signed tokens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-22 17:10:51 -07:00
2 changed files with 36 additions and 1 deletions

View File

@@ -511,7 +511,17 @@ class DetailedJWTVerifier(MCPJWTVerifier):
_sanitize_for_log(token_alg),
)
return None
if self.algorithm and token_alg != self.algorithm:
# Require a pinned signing algorithm. Without one, the accepted
# algorithm family would be whatever the verification key or the
# underlying library permits; refuse rather than validating against
# an unconstrained algorithm set. The production factory always
# pins an algorithm, so this guards the directly-constructed case.
if not self.algorithm:
reason = "No signing algorithm pinned"
_jwt_failure_reason.set(reason)
logger.debug("Rejected token: verifier has no pinned signing algorithm")
return None
if token_alg != self.algorithm:
reason = "Algorithm mismatch"
_jwt_failure_reason.set(reason)
logger.debug(

View File

@@ -83,6 +83,31 @@ async def test_algorithm_mismatch(hs256_verifier):
assert "HS256" not in reason
@pytest.mark.asyncio
async def test_unpinned_algorithm_is_rejected(
hs256_verifier: DetailedJWTVerifier,
) -> None:
"""A verifier with no pinned algorithm must reject signed tokens.
The upstream JWTVerifier currently always defaults the algorithm to RS256,
so this state is not reachable through normal construction. This asserts the
fail-closed guard so the verifier does not silently rely on that upstream
default: if the pinned algorithm is ever absent, tokens are rejected rather
than validated against an unconstrained algorithm family.
"""
# Simulate an unpinned verifier (e.g. a future upstream default change).
hs256_verifier.algorithm = None
token = _make_token(
{"alg": "HS256", "typ": "JWT"},
{"sub": "user1", "iss": "test-issuer", "aud": "test-audience"},
)
result = await hs256_verifier.load_access_token(token)
assert result is None
assert _jwt_failure_reason.get() == "No signing algorithm pinned"
@pytest.mark.asyncio
async def test_malformed_token_header(hs256_verifier):
"""Token with invalid header should report malformed header."""