From 40ce687d0d62fdd042227cee442cf43fe8f6abc3 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:39:55 -0400 Subject: [PATCH 1/2] Fix JWT issuer validation to support string values per RFC 7519 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves issue where BearerAuthProvider rejected tokens with non-URL issuer claims. Works around the underlying SDK's URL validation while maintaining RFC 7519 compliance for JWT processing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/server/auth/providers/bearer.py | 12 ++++- tests/auth/providers/test_bearer.py | 53 +++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py index 763f90f4f..e544802f9 100644 --- a/src/fastmcp/server/auth/providers/bearer.py +++ b/src/fastmcp/server/auth/providers/bearer.py @@ -17,7 +17,7 @@ from mcp.shared.auth import ( OAuthClientInformationFull, OAuthToken, ) -from pydantic import SecretStr +from pydantic import AnyHttpUrl, SecretStr from fastmcp.server.auth.auth import ( ClientRegistrationOptions, @@ -179,8 +179,16 @@ class BearerAuthProvider(OAuthProvider): if public_key and jwks_uri: raise ValueError("Provide either public_key or jwks_uri, not both") + # Only pass issuer to parent if it's a valid URL, otherwise use default + # This allows the issuer claim validation to work with string issuers per RFC 7519 + try: + issuer_url = AnyHttpUrl(issuer) if issuer else "https://fastmcp.example.com" + except Exception: + # Issuer is not a valid URL, use default for parent class + issuer_url = "https://fastmcp.example.com" + super().__init__( - issuer_url=issuer or "https://fastmcp.example.com", + issuer_url=issuer_url, client_registration_options=ClientRegistrationOptions(enabled=False), revocation_options=RevocationOptions(enabled=False), required_scopes=required_scopes, diff --git a/tests/auth/providers/test_bearer.py b/tests/auth/providers/test_bearer.py index 6f59c96fe..31d623d2c 100644 --- a/tests/auth/providers/test_bearer.py +++ b/tests/auth/providers/test_bearer.py @@ -539,6 +539,59 @@ class TestBearerToken: assert access_token is not None assert access_token.client_id == "app456" # Should prefer client_id over sub + async def test_string_issuer_validation(self, rsa_key_pair: RSAKeyPair): + """Test that string (non-URL) issuers are supported per RFC 7519.""" + # Create provider with string issuer + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="my-service", # String issuer, not a URL + ) + + # Create token with matching string issuer + token = rsa_key_pair.create_token( + subject="test-user", + issuer="my-service", # Same string issuer + ) + + access_token = await provider.load_access_token(token) + assert access_token is not None + assert access_token.client_id == "test-user" + + async def test_string_issuer_mismatch_rejection(self, rsa_key_pair: RSAKeyPair): + """Test that mismatched string issuers are rejected.""" + # Create provider with one string issuer + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="my-service", + ) + + # Create token with different string issuer + token = rsa_key_pair.create_token( + subject="test-user", + issuer="other-service", # Different string issuer + ) + + access_token = await provider.load_access_token(token) + assert access_token is None + + async def test_url_issuer_still_works(self, rsa_key_pair: RSAKeyPair): + """Test that URL issuers still work after the fix.""" + # Create provider with URL issuer + provider = BearerAuthProvider( + public_key=rsa_key_pair.public_key, + issuer="https://my-auth-server.com", # URL issuer + ) + + # Create token with matching URL issuer + token = rsa_key_pair.create_token( + subject="test-user", + issuer="https://my-auth-server.com", # Same URL issuer + ) + + access_token = await provider.load_access_token(token) + assert access_token is not None + assert access_token.client_id == "test-user" + class TestFastMCPBearerAuth: def test_bearer_auth(self): From 7b5ef3e5e5e0316b90ddabb25d3879908bbed75b Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:50:16 -0400 Subject: [PATCH 2/2] Use specific ValidationError instead of broad Exception catch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PR feedback to use more specific exception handling for URL validation failures. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/server/auth/providers/bearer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py index e544802f9..a4f3a8e48 100644 --- a/src/fastmcp/server/auth/providers/bearer.py +++ b/src/fastmcp/server/auth/providers/bearer.py @@ -17,7 +17,7 @@ from mcp.shared.auth import ( OAuthClientInformationFull, OAuthToken, ) -from pydantic import AnyHttpUrl, SecretStr +from pydantic import AnyHttpUrl, SecretStr, ValidationError from fastmcp.server.auth.auth import ( ClientRegistrationOptions, @@ -183,7 +183,7 @@ class BearerAuthProvider(OAuthProvider): # This allows the issuer claim validation to work with string issuers per RFC 7519 try: issuer_url = AnyHttpUrl(issuer) if issuer else "https://fastmcp.example.com" - except Exception: + except ValidationError: # Issuer is not a valid URL, use default for parent class issuer_url = "https://fastmcp.example.com"