diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py index 763f90f4f..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 SecretStr +from pydantic import AnyHttpUrl, SecretStr, ValidationError 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 ValidationError: + # 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):