Spaces:
Running
Running
Jeremiah Lowin Claude commited on
Commit ·
b163829
1
Parent(s): 8e4c972
Fix JWT issuer validation to support string values per RFC 7519
Browse filesResolves 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 <noreply@anthropic.com>
src/fastmcp/server/auth/providers/bearer.py
CHANGED
|
@@ -17,7 +17,7 @@ from mcp.shared.auth import (
|
|
| 17 |
OAuthClientInformationFull,
|
| 18 |
OAuthToken,
|
| 19 |
)
|
| 20 |
-
from pydantic import SecretStr
|
| 21 |
|
| 22 |
from fastmcp.server.auth.auth import (
|
| 23 |
ClientRegistrationOptions,
|
|
@@ -179,8 +179,16 @@ class BearerAuthProvider(OAuthProvider):
|
|
| 179 |
if public_key and jwks_uri:
|
| 180 |
raise ValueError("Provide either public_key or jwks_uri, not both")
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
super().__init__(
|
| 183 |
-
issuer_url=
|
| 184 |
client_registration_options=ClientRegistrationOptions(enabled=False),
|
| 185 |
revocation_options=RevocationOptions(enabled=False),
|
| 186 |
required_scopes=required_scopes,
|
|
|
|
| 17 |
OAuthClientInformationFull,
|
| 18 |
OAuthToken,
|
| 19 |
)
|
| 20 |
+
from pydantic import AnyHttpUrl, SecretStr
|
| 21 |
|
| 22 |
from fastmcp.server.auth.auth import (
|
| 23 |
ClientRegistrationOptions,
|
|
|
|
| 179 |
if public_key and jwks_uri:
|
| 180 |
raise ValueError("Provide either public_key or jwks_uri, not both")
|
| 181 |
|
| 182 |
+
# Only pass issuer to parent if it's a valid URL, otherwise use default
|
| 183 |
+
# This allows the issuer claim validation to work with string issuers per RFC 7519
|
| 184 |
+
try:
|
| 185 |
+
issuer_url = AnyHttpUrl(issuer) if issuer else "https://fastmcp.example.com"
|
| 186 |
+
except Exception:
|
| 187 |
+
# Issuer is not a valid URL, use default for parent class
|
| 188 |
+
issuer_url = "https://fastmcp.example.com"
|
| 189 |
+
|
| 190 |
super().__init__(
|
| 191 |
+
issuer_url=issuer_url,
|
| 192 |
client_registration_options=ClientRegistrationOptions(enabled=False),
|
| 193 |
revocation_options=RevocationOptions(enabled=False),
|
| 194 |
required_scopes=required_scopes,
|
tests/auth/providers/test_bearer.py
CHANGED
|
@@ -539,6 +539,59 @@ class TestBearerToken:
|
|
| 539 |
assert access_token is not None
|
| 540 |
assert access_token.client_id == "app456" # Should prefer client_id over sub
|
| 541 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
|
| 543 |
class TestFastMCPBearerAuth:
|
| 544 |
def test_bearer_auth(self):
|
|
|
|
| 539 |
assert access_token is not None
|
| 540 |
assert access_token.client_id == "app456" # Should prefer client_id over sub
|
| 541 |
|
| 542 |
+
async def test_string_issuer_validation(self, rsa_key_pair: RSAKeyPair):
|
| 543 |
+
"""Test that string (non-URL) issuers are supported per RFC 7519."""
|
| 544 |
+
# Create provider with string issuer
|
| 545 |
+
provider = BearerAuthProvider(
|
| 546 |
+
public_key=rsa_key_pair.public_key,
|
| 547 |
+
issuer="my-service", # String issuer, not a URL
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
# Create token with matching string issuer
|
| 551 |
+
token = rsa_key_pair.create_token(
|
| 552 |
+
subject="test-user",
|
| 553 |
+
issuer="my-service", # Same string issuer
|
| 554 |
+
)
|
| 555 |
+
|
| 556 |
+
access_token = await provider.load_access_token(token)
|
| 557 |
+
assert access_token is not None
|
| 558 |
+
assert access_token.client_id == "test-user"
|
| 559 |
+
|
| 560 |
+
async def test_string_issuer_mismatch_rejection(self, rsa_key_pair: RSAKeyPair):
|
| 561 |
+
"""Test that mismatched string issuers are rejected."""
|
| 562 |
+
# Create provider with one string issuer
|
| 563 |
+
provider = BearerAuthProvider(
|
| 564 |
+
public_key=rsa_key_pair.public_key,
|
| 565 |
+
issuer="my-service",
|
| 566 |
+
)
|
| 567 |
+
|
| 568 |
+
# Create token with different string issuer
|
| 569 |
+
token = rsa_key_pair.create_token(
|
| 570 |
+
subject="test-user",
|
| 571 |
+
issuer="other-service", # Different string issuer
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
access_token = await provider.load_access_token(token)
|
| 575 |
+
assert access_token is None
|
| 576 |
+
|
| 577 |
+
async def test_url_issuer_still_works(self, rsa_key_pair: RSAKeyPair):
|
| 578 |
+
"""Test that URL issuers still work after the fix."""
|
| 579 |
+
# Create provider with URL issuer
|
| 580 |
+
provider = BearerAuthProvider(
|
| 581 |
+
public_key=rsa_key_pair.public_key,
|
| 582 |
+
issuer="https://my-auth-server.com", # URL issuer
|
| 583 |
+
)
|
| 584 |
+
|
| 585 |
+
# Create token with matching URL issuer
|
| 586 |
+
token = rsa_key_pair.create_token(
|
| 587 |
+
subject="test-user",
|
| 588 |
+
issuer="https://my-auth-server.com", # Same URL issuer
|
| 589 |
+
)
|
| 590 |
+
|
| 591 |
+
access_token = await provider.load_access_token(token)
|
| 592 |
+
assert access_token is not None
|
| 593 |
+
assert access_token.client_id == "test-user"
|
| 594 |
+
|
| 595 |
|
| 596 |
class TestFastMCPBearerAuth:
|
| 597 |
def test_bearer_auth(self):
|