diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py index 6ffffa909..c6e9024bc 100644 --- a/src/fastmcp/server/auth/providers/bearer.py +++ b/src/fastmcp/server/auth/providers/bearer.py @@ -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]: