Jeremiah Lowin commited on
Commit
5f72fab
·
unverified ·
2 Parent(s): bbf015c029001b

Merge branch 'main' into claude-wt-20250625-195151

Browse files
src/fastmcp/server/auth/providers/bearer.py CHANGED
@@ -24,6 +24,7 @@ from fastmcp.server.auth.auth import (
24
  OAuthProvider,
25
  RevocationOptions,
26
  )
 
27
 
28
 
29
  class JWKData(TypedDict, total=False):
@@ -199,6 +200,7 @@ class BearerAuthProvider(OAuthProvider):
199
  self.public_key = public_key
200
  self.jwks_uri = jwks_uri
201
  self.jwt = JsonWebToken(["RS256"])
 
202
 
203
  # Simple JWKS cache
204
  self._jwks_cache: dict[str, str] = {}
@@ -265,6 +267,9 @@ class BearerAuthProvider(OAuthProvider):
265
  # Select the appropriate key
266
  if kid:
267
  if kid not in self._jwks_cache:
 
 
 
268
  raise ValueError(f"Key ID '{kid}' not found in JWKS")
269
  return self._jwks_cache[kid]
270
  else:
@@ -279,6 +284,7 @@ class BearerAuthProvider(OAuthProvider):
279
  raise ValueError("No keys found in JWKS")
280
 
281
  except Exception as e:
 
282
  raise ValueError(f"Failed to fetch JWKS: {e}")
283
 
284
  async def load_access_token(self, token: str) -> AccessToken | None:
@@ -298,15 +304,27 @@ class BearerAuthProvider(OAuthProvider):
298
  # Decode and verify the JWT token
299
  claims = self.jwt.decode(token, verification_key)
300
 
 
 
 
301
  # Validate expiration
302
  exp = claims.get("exp")
303
  if exp and exp < time.time():
 
 
 
 
304
  return None
305
 
306
  # Validate issuer - note we use issuer instead of issuer_url here because
307
  # issuer is optional, allowing users to make this check optional
308
  if self.issuer:
309
  if claims.get("iss") != self.issuer:
 
 
 
 
 
310
  return None
311
 
312
  # Validate audience if configured
@@ -314,26 +332,33 @@ class BearerAuthProvider(OAuthProvider):
314
  aud = claims.get("aud")
315
 
316
  # Handle different combinations of audience types
 
317
  if isinstance(self.audience, list):
318
  # self.audience is a list - check if any expected audience is present
319
  if isinstance(aud, list):
320
  # Both are lists - check for intersection
321
- if not any(expected in aud for expected in self.audience):
322
- return None
 
323
  else:
324
  # aud is a string - check if it's in our expected list
325
- if aud not in self.audience:
326
- return None
327
  else:
328
  # self.audience is a string - use original logic
329
  if isinstance(aud, list):
330
- if self.audience not in aud:
331
- return None
332
- elif aud != self.audience:
333
- return None
334
 
335
- # Extract claims - prefer client_id over sub for OAuth application identification
336
- client_id = claims.get("client_id") or claims.get("sub") or "unknown"
 
 
 
 
 
 
 
337
  scopes = self._extract_scopes(claims)
338
 
339
  return AccessToken(
@@ -344,8 +369,10 @@ class BearerAuthProvider(OAuthProvider):
344
  )
345
 
346
  except JoseError:
 
347
  return None
348
- except Exception:
 
349
  return None
350
 
351
  def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
 
24
  OAuthProvider,
25
  RevocationOptions,
26
  )
27
+ from fastmcp.utilities.logging import get_logger
28
 
29
 
30
  class JWKData(TypedDict, total=False):
 
200
  self.public_key = public_key
201
  self.jwks_uri = jwks_uri
202
  self.jwt = JsonWebToken(["RS256"])
203
+ self.logger = get_logger(__name__)
204
 
205
  # Simple JWKS cache
206
  self._jwks_cache: dict[str, str] = {}
 
267
  # Select the appropriate key
268
  if kid:
269
  if kid not in self._jwks_cache:
270
+ self.logger.debug(
271
+ "JWKS key lookup failed: key ID '%s' not found", kid
272
+ )
273
  raise ValueError(f"Key ID '{kid}' not found in JWKS")
274
  return self._jwks_cache[kid]
275
  else:
 
284
  raise ValueError("No keys found in JWKS")
285
 
286
  except Exception as e:
287
+ self.logger.debug("JWKS fetch failed: %s", str(e))
288
  raise ValueError(f"Failed to fetch JWKS: {e}")
289
 
290
  async def load_access_token(self, token: str) -> AccessToken | None:
 
304
  # Decode and verify the JWT token
305
  claims = self.jwt.decode(token, verification_key)
306
 
307
+ # Extract client ID early for logging
308
+ client_id = claims.get("client_id") or claims.get("sub") or "unknown"
309
+
310
  # Validate expiration
311
  exp = claims.get("exp")
312
  if exp and exp < time.time():
313
+ self.logger.debug(
314
+ "Token validation failed: expired token for client %s", client_id
315
+ )
316
+ self.logger.info("Bearer token rejected for client %s", client_id)
317
  return None
318
 
319
  # Validate issuer - note we use issuer instead of issuer_url here because
320
  # issuer is optional, allowing users to make this check optional
321
  if self.issuer:
322
  if claims.get("iss") != self.issuer:
323
+ self.logger.debug(
324
+ "Token validation failed: issuer mismatch for client %s",
325
+ client_id,
326
+ )
327
+ self.logger.info("Bearer token rejected for client %s", client_id)
328
  return None
329
 
330
  # Validate audience if configured
 
332
  aud = claims.get("aud")
333
 
334
  # Handle different combinations of audience types
335
+ audience_valid = False
336
  if isinstance(self.audience, list):
337
  # self.audience is a list - check if any expected audience is present
338
  if isinstance(aud, list):
339
  # Both are lists - check for intersection
340
+ audience_valid = any(
341
+ expected in aud for expected in self.audience
342
+ )
343
  else:
344
  # aud is a string - check if it's in our expected list
345
+ audience_valid = aud in self.audience
 
346
  else:
347
  # self.audience is a string - use original logic
348
  if isinstance(aud, list):
349
+ audience_valid = self.audience in aud
350
+ else:
351
+ audience_valid = aud == self.audience
 
352
 
353
+ if not audience_valid:
354
+ self.logger.debug(
355
+ "Token validation failed: audience mismatch for client %s",
356
+ client_id,
357
+ )
358
+ self.logger.info("Bearer token rejected for client %s", client_id)
359
+ return None
360
+
361
+ # Extract scopes
362
  scopes = self._extract_scopes(claims)
363
 
364
  return AccessToken(
 
369
  )
370
 
371
  except JoseError:
372
+ self.logger.debug("Token validation failed: JWT signature/format invalid")
373
  return None
374
+ except Exception as e:
375
+ self.logger.debug("Token validation failed: %s", str(e))
376
  return None
377
 
378
  def _extract_scopes(self, claims: dict[str, Any]) -> list[str]: