Jeremiah Lowin commited on
Commit
d7899a9
·
unverified ·
2 Parent(s): a2b6ed4d215186

Merge pull request #1033 from jlowin/claude-wt-20250703-195810

Browse files

Support 'scp' claim for OAuth scopes in BearerAuthProvider

src/fastmcp/server/auth/providers/bearer.py CHANGED
@@ -399,12 +399,21 @@ class BearerAuthProvider(OAuthProvider):
399
  return None
400
 
401
  def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
402
- """Extract scopes from JWT claims."""
403
- scope_claim = claims.get("scope", "")
404
- if isinstance(scope_claim, str):
405
- return scope_claim.split()
406
- elif isinstance(scope_claim, list):
407
- return scope_claim
 
 
 
 
 
 
 
 
 
408
  return []
409
 
410
  async def verify_token(self, token: str) -> AccessToken | None:
 
399
  return None
400
 
401
  def _extract_scopes(self, claims: dict[str, Any]) -> list[str]:
402
+ """
403
+ Extract scopes from JWT claims. Supports both 'scope' and 'scp'
404
+ claims.
405
+
406
+ Checks the `scope` claim first (standard OAuth2 claim), then the `scp`
407
+ claim (used by some Identity Providers).
408
+ """
409
+
410
+ for claim in ["scope", "scp"]:
411
+ if claim in claims:
412
+ if isinstance(claims[claim], str):
413
+ return claims[claim].split()
414
+ elif isinstance(claims[claim], list):
415
+ return claims[claim]
416
+
417
  return []
418
 
419
  async def verify_token(self, token: str) -> AccessToken | None:
tests/auth/providers/test_bearer.py CHANGED
@@ -533,6 +533,59 @@ class TestBearerToken:
533
  assert access_token is not None
534
  assert access_token.scopes == []
535
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  async def test_malformed_token_rejection(self, bearer_provider: BearerAuthProvider):
537
  """Test rejection of malformed tokens."""
538
  malformed_tokens = [
 
533
  assert access_token is not None
534
  assert access_token.scopes == []
535
 
536
+ async def test_scp_claim_extraction_string(
537
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
538
+ ):
539
+ """Test scope extraction from 'scp' claim with space-separated string."""
540
+ token = rsa_key_pair.create_token(
541
+ subject="test-user",
542
+ issuer="https://test.example.com",
543
+ audience="https://api.example.com",
544
+ additional_claims={"scp": "read write admin"}, # 'scp' claim as string
545
+ )
546
+
547
+ access_token = await bearer_provider.load_access_token(token)
548
+
549
+ assert access_token is not None
550
+ assert set(access_token.scopes) == {"read", "write", "admin"}
551
+
552
+ async def test_scp_claim_extraction_list(
553
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
554
+ ):
555
+ """Test scope extraction from 'scp' claim with list format."""
556
+ token = rsa_key_pair.create_token(
557
+ subject="test-user",
558
+ issuer="https://test.example.com",
559
+ audience="https://api.example.com",
560
+ additional_claims={
561
+ "scp": ["read", "write", "admin"]
562
+ }, # 'scp' claim as list
563
+ )
564
+
565
+ access_token = await bearer_provider.load_access_token(token)
566
+
567
+ assert access_token is not None
568
+ assert set(access_token.scopes) == {"read", "write", "admin"}
569
+
570
+ async def test_scope_precedence_over_scp(
571
+ self, rsa_key_pair: RSAKeyPair, bearer_provider: BearerAuthProvider
572
+ ):
573
+ """Test that 'scope' claim takes precedence over 'scp' claim when both are present."""
574
+ token = rsa_key_pair.create_token(
575
+ subject="test-user",
576
+ issuer="https://test.example.com",
577
+ audience="https://api.example.com",
578
+ additional_claims={
579
+ "scope": "read write", # Standard OAuth2 claim
580
+ "scp": "admin delete", # Should be ignored when 'scope' is present
581
+ },
582
+ )
583
+
584
+ access_token = await bearer_provider.load_access_token(token)
585
+
586
+ assert access_token is not None
587
+ assert set(access_token.scopes) == {"read", "write"} # Only 'scope' claim used
588
+
589
  async def test_malformed_token_rejection(self, bearer_provider: BearerAuthProvider):
590
  """Test rejection of malformed tokens."""
591
  malformed_tokens = [