mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 21:44:18 +02:00
Merge branch 'main' into claude-wt-20250625-195151
This commit is contained in:
commit
cf72bbf94f
1 changed files with 38 additions and 11 deletions
|
|
@ -24,6 +24,7 @@ from fastmcp.server.auth.auth import (
|
|||
OAuthProvider,
|
||||
RevocationOptions,
|
||||
)
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
|
||||
class JWKData(TypedDict, total=False):
|
||||
|
|
@ -199,6 +200,7 @@ class BearerAuthProvider(OAuthProvider):
|
|||
self.public_key = public_key
|
||||
self.jwks_uri = jwks_uri
|
||||
self.jwt = JsonWebToken(["RS256"])
|
||||
self.logger = get_logger(__name__)
|
||||
|
||||
# Simple JWKS cache
|
||||
self._jwks_cache: dict[str, str] = {}
|
||||
|
|
@ -265,6 +267,9 @@ class BearerAuthProvider(OAuthProvider):
|
|||
# Select the appropriate key
|
||||
if kid:
|
||||
if kid not in self._jwks_cache:
|
||||
self.logger.debug(
|
||||
"JWKS key lookup failed: key ID '%s' not found", kid
|
||||
)
|
||||
raise ValueError(f"Key ID '{kid}' not found in JWKS")
|
||||
return self._jwks_cache[kid]
|
||||
else:
|
||||
|
|
@ -279,6 +284,7 @@ class BearerAuthProvider(OAuthProvider):
|
|||
raise ValueError("No keys found in JWKS")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug("JWKS fetch failed: %s", str(e))
|
||||
raise ValueError(f"Failed to fetch JWKS: {e}")
|
||||
|
||||
async def load_access_token(self, token: str) -> AccessToken | None:
|
||||
|
|
@ -298,15 +304,27 @@ class BearerAuthProvider(OAuthProvider):
|
|||
# Decode and verify the JWT token
|
||||
claims = self.jwt.decode(token, verification_key)
|
||||
|
||||
# Extract client ID early for logging
|
||||
client_id = claims.get("client_id") or claims.get("sub") or "unknown"
|
||||
|
||||
# Validate expiration
|
||||
exp = claims.get("exp")
|
||||
if exp and exp < time.time():
|
||||
self.logger.debug(
|
||||
"Token validation failed: expired token for client %s", client_id
|
||||
)
|
||||
self.logger.info("Bearer token rejected for client %s", client_id)
|
||||
return None
|
||||
|
||||
# Validate issuer - note we use issuer instead of issuer_url here because
|
||||
# issuer is optional, allowing users to make this check optional
|
||||
if self.issuer:
|
||||
if claims.get("iss") != self.issuer:
|
||||
self.logger.debug(
|
||||
"Token validation failed: issuer mismatch for client %s",
|
||||
client_id,
|
||||
)
|
||||
self.logger.info("Bearer token rejected for client %s", client_id)
|
||||
return None
|
||||
|
||||
# Validate audience if configured
|
||||
|
|
@ -314,26 +332,33 @@ class BearerAuthProvider(OAuthProvider):
|
|||
aud = claims.get("aud")
|
||||
|
||||
# Handle different combinations of audience types
|
||||
audience_valid = False
|
||||
if isinstance(self.audience, list):
|
||||
# self.audience is a list - check if any expected audience is present
|
||||
if isinstance(aud, list):
|
||||
# Both are lists - check for intersection
|
||||
if not any(expected in aud for expected in self.audience):
|
||||
return None
|
||||
audience_valid = any(
|
||||
expected in aud for expected in self.audience
|
||||
)
|
||||
else:
|
||||
# aud is a string - check if it's in our expected list
|
||||
if aud not in self.audience:
|
||||
return None
|
||||
audience_valid = aud in self.audience
|
||||
else:
|
||||
# self.audience is a string - use original logic
|
||||
if isinstance(aud, list):
|
||||
if self.audience not in aud:
|
||||
return None
|
||||
elif aud != self.audience:
|
||||
return None
|
||||
audience_valid = self.audience in aud
|
||||
else:
|
||||
audience_valid = aud == self.audience
|
||||
|
||||
# Extract claims - prefer client_id over sub for OAuth application identification
|
||||
client_id = claims.get("client_id") or claims.get("sub") or "unknown"
|
||||
if not audience_valid:
|
||||
self.logger.debug(
|
||||
"Token validation failed: audience mismatch for client %s",
|
||||
client_id,
|
||||
)
|
||||
self.logger.info("Bearer token rejected for client %s", client_id)
|
||||
return None
|
||||
|
||||
# Extract scopes
|
||||
scopes = self._extract_scopes(claims)
|
||||
|
||||
return AccessToken(
|
||||
|
|
@ -344,8 +369,10 @@ class BearerAuthProvider(OAuthProvider):
|
|||
)
|
||||
|
||||
except JoseError:
|
||||
self.logger.debug("Token validation failed: JWT signature/format invalid")
|
||||
return None
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
self.logger.debug("Token validation failed: %s", str(e))
|
||||
return None
|
||||
|
||||
def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue