mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 04:54:17 +02:00
Block insecure HS* JWT verification with JWKS/public keys (#3430)
* Block HS* JWT verification with public keys/JWKS 🤖 Generated with GPT-5.2-Codex * Fix ruff format violations 🤖 Generated with Claude Code * Handle bytes public_key in HS* algorithm PEM check
This commit is contained in:
parent
ad517d09fa
commit
5ed14650ab
4 changed files with 58 additions and 13 deletions
|
|
@ -139,6 +139,20 @@ class RSAKeyPair:
|
|||
return token_bytes.decode("utf-8")
|
||||
|
||||
|
||||
def _looks_like_pem_public_key(key: str | bytes) -> bool:
|
||||
"""Return True when key text appears to be PEM-encoded asymmetric key material."""
|
||||
if isinstance(key, bytes):
|
||||
key = key.decode("utf-8", errors="replace")
|
||||
key_text = key.strip()
|
||||
pem_markers = (
|
||||
"-----BEGIN PUBLIC KEY-----",
|
||||
"-----BEGIN RSA PUBLIC KEY-----",
|
||||
"-----BEGIN EC PUBLIC KEY-----",
|
||||
"-----BEGIN CERTIFICATE-----",
|
||||
)
|
||||
return any(marker in key_text for marker in pem_markers)
|
||||
|
||||
|
||||
class JWTVerifier(TokenVerifier):
|
||||
"""
|
||||
JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
|
||||
|
|
@ -161,7 +175,7 @@ class JWTVerifier(TokenVerifier):
|
|||
def __init__(
|
||||
self,
|
||||
*,
|
||||
public_key: str | None = None,
|
||||
public_key: str | bytes | None = None,
|
||||
jwks_uri: str | None = None,
|
||||
issuer: str | list[str] | None = None,
|
||||
audience: str | list[str] | None = None,
|
||||
|
|
@ -225,11 +239,16 @@ class JWTVerifier(TokenVerifier):
|
|||
}:
|
||||
raise ValueError(f"Unsupported algorithm: {algorithm}.")
|
||||
|
||||
if jwks_uri and algorithm.startswith("HS"):
|
||||
raise ValueError(
|
||||
"HMAC algorithms (HS256/HS384/HS512) require a shared secret via "
|
||||
"public_key and cannot be used with jwks_uri"
|
||||
)
|
||||
if algorithm.startswith("HS"):
|
||||
if jwks_uri:
|
||||
raise ValueError(
|
||||
"Symmetric HS* algorithms cannot be used with jwks_uri; "
|
||||
"configure a shared secret via public_key instead."
|
||||
)
|
||||
if public_key and _looks_like_pem_public_key(public_key):
|
||||
raise ValueError(
|
||||
"Symmetric HS* algorithms require a shared secret, not a public key."
|
||||
)
|
||||
|
||||
# Parse scopes if provided as string
|
||||
parsed_required_scopes = (
|
||||
|
|
@ -257,7 +276,7 @@ class JWTVerifier(TokenVerifier):
|
|||
self._jwks_cache_time: float = 0
|
||||
self._cache_ttl = 3600 # 1 hour
|
||||
|
||||
async def _get_verification_key(self, token: str) -> str:
|
||||
async def _get_verification_key(self, token: str) -> str | bytes:
|
||||
"""Get the verification key for the token."""
|
||||
if self.public_key:
|
||||
return self.public_key
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class SupabaseProvider(RemoteAuthProvider):
|
|||
1. Supabase Project Setup:
|
||||
- Create a Supabase project at https://supabase.com
|
||||
- Note your project URL (e.g., "https://abc123.supabase.co")
|
||||
- Configure your JWT algorithm in Supabase Auth settings (HS256, RS256, or ES256)
|
||||
- Configure your JWT algorithm in Supabase Auth settings (RS256 or ES256)
|
||||
- Asymmetric keys (RS256/ES256) are recommended for production
|
||||
|
||||
2. JWT Verification:
|
||||
|
|
@ -74,7 +74,7 @@ class SupabaseProvider(RemoteAuthProvider):
|
|||
project_url: AnyHttpUrl | str,
|
||||
base_url: AnyHttpUrl | str,
|
||||
auth_route: str = "/auth/v1",
|
||||
algorithm: Literal["HS256", "RS256", "ES256"] = "ES256",
|
||||
algorithm: Literal["RS256", "ES256"] = "ES256",
|
||||
required_scopes: list[str] | None = None,
|
||||
scopes_supported: list[str] | None = None,
|
||||
resource_name: str | None = None,
|
||||
|
|
@ -88,7 +88,7 @@ class SupabaseProvider(RemoteAuthProvider):
|
|||
base_url: Public URL of this FastMCP server
|
||||
auth_route: Supabase Auth route. Defaults to "/auth/v1". Can be customized
|
||||
for self-hosted Supabase Auth setups using custom routes.
|
||||
algorithm: JWT signing algorithm (HS256, RS256, or ES256). Must match your
|
||||
algorithm: JWT signing algorithm (RS256 or ES256). Must match your
|
||||
Supabase Auth configuration. Defaults to ES256.
|
||||
required_scopes: Optional list of scopes to require for all requests.
|
||||
Note: Supabase currently uses RLS policies for authorization. OAuth-level
|
||||
|
|
|
|||
|
|
@ -106,13 +106,13 @@ class TestSupabaseProvider:
|
|||
assert isinstance(provider.token_verifier, JWTVerifier)
|
||||
assert provider.token_verifier.algorithm == algorithm
|
||||
|
||||
def test_algorithm_hs256_rejected(self):
|
||||
"""Test that HS256 is rejected with Supabase JWKS verification."""
|
||||
def test_algorithm_rejects_hs256(self):
|
||||
"""Test that HS256 is rejected for Supabase's JWKS-based verifier."""
|
||||
with pytest.raises(ValueError, match="cannot be used with jwks_uri"):
|
||||
SupabaseProvider(
|
||||
project_url="https://abc123.supabase.co",
|
||||
base_url="https://myserver.com",
|
||||
algorithm="HS256",
|
||||
algorithm="HS256", # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
def test_algorithm_default_es256(self):
|
||||
|
|
|
|||
|
|
@ -224,6 +224,32 @@ class TestSymmetricKeyJWT:
|
|||
)
|
||||
assert provider.algorithm == algorithm
|
||||
|
||||
def test_symmetric_algorithm_rejects_jwks_uri(self):
|
||||
"""HS* algorithms must not be configured with JWKS/public key endpoints."""
|
||||
with pytest.raises(ValueError, match="cannot be used with jwks_uri"):
|
||||
JWTVerifier(
|
||||
jwks_uri="https://test.example.com/.well-known/jwks.json",
|
||||
issuer="https://test.example.com",
|
||||
algorithm="HS256",
|
||||
)
|
||||
|
||||
def test_symmetric_algorithm_rejects_pem_public_key(self, rsa_key_pair: RSAKeyPair):
|
||||
"""HS* algorithms must use a shared secret, not PEM public key material."""
|
||||
with pytest.raises(ValueError, match="require a shared secret"):
|
||||
JWTVerifier(
|
||||
public_key=rsa_key_pair.public_key,
|
||||
issuer="https://test.example.com",
|
||||
algorithm="HS256",
|
||||
)
|
||||
|
||||
def test_symmetric_algorithm_accepts_bytes_secret(self):
|
||||
"""HS* algorithms accept bytes secrets without TypeError."""
|
||||
verifier = JWTVerifier(
|
||||
public_key=b"secret",
|
||||
algorithm="HS256",
|
||||
)
|
||||
assert verifier.algorithm == "HS256"
|
||||
|
||||
async def test_valid_symmetric_token_validation(
|
||||
self, symmetric_key_helper: SymmetricKeyHelper, symmetric_provider: JWTVerifier
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue