[v1.x] Add subject and claims to AccessToken (#2690)

This commit is contained in:
Max 2026-05-26 15:49:44 +01:00 • committed by GitHub
commit 1abcca2408
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 27 additions and 2 deletions

View file

@ -123,6 +123,8 @@ def create_authorization_server(server_settings: AuthServerSettings, auth_settin
"iat": int(time.time()),
"token_type": "Bearer",
"aud": access_token.resource, # RFC 8707 audience claim
"sub": access_token.subject, # RFC 7662 subject
"iss": str(server_settings.server_url),
}
)

View file

@ -186,6 +186,7 @@ class SimpleOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Re
scopes=[self.settings.mcp_scope],
code_challenge=code_challenge,
resource=resource, # RFC 8707
subject=username,
)
self.auth_codes[new_code] = auth_code
@ -224,6 +225,7 @@ class SimpleOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Re
scopes=authorization_code.scopes,
expires_at=int(time.time()) + 3600,
resource=authorization_code.resource, # RFC 8707
subject=authorization_code.subject,
)
# Store user data mapping for this token

View file

@ -75,6 +75,8 @@ class IntrospectionTokenVerifier(TokenVerifier):
scopes=data.get("scope", "").split() if data.get("scope") else [],
expires_at=data.get("exp"),
resource=data.get("aud"), # Include resource in token
subject=data.get("sub"), # RFC 7662 subject (resource owner)
claims=data,
)
except Exception as e:
logger.warning(f"Token introspection failed: {e}")

View file

@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Generic, Literal, Protocol, TypeVar
from typing import Any, Generic, Literal, Protocol, TypeVar
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
from pydantic import AnyUrl, BaseModel
@ -25,6 +25,7 @@ class AuthorizationCode(BaseModel):
redirect_uri: AnyUrl
redirect_uri_provided_explicitly: bool
resource: str | None = None # RFC 8707 resource indicator
subject: str | None = None # resource owner; propagate to the issued AccessToken
class RefreshToken(BaseModel):
@ -32,6 +33,7 @@ class RefreshToken(BaseModel):
client_id: str
scopes: list[str]
expires_at: int | None = None
subject: str | None = None # resource owner; propagate to refreshed AccessTokens
class AccessToken(BaseModel):
@ -40,6 +42,8 @@ class AccessToken(BaseModel):
scopes: list[str]
expires_at: int | None = None
resource: str | None = None # RFC 8707 resource indicator
subject: str | None = None # RFC 7662/9068 `sub`: resource owner; unique only per issuer
claims: dict[str, Any] | None = None # additional claims (e.g. `iss`, `act`)
RegistrationErrorCode = Literal[

View file

@ -1282,9 +1282,14 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
related_request_id=self.request_id,
)
# TODO(maxisbey): see if this is needed otherwise remove
@property
def client_id(self) -> str | None:
"""Get the client ID if available."""
"""Get the client ID if available.
Note: this reads from the MCP request's `_meta` params, not the OAuth
bearer token. For that, use `get_access_token().client_id`.
"""
return (
getattr(self.request_context.meta, "client_id", None) if self.request_context.meta else None
) # pragma: no cover

View file

@ -54,6 +54,7 @@ class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Refr
redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
expires_at=time.time() + 300,
scopes=params.scopes or ["read", "write"],
subject="test-user",
)
self.auth_codes[code.code] = code
@ -80,6 +81,7 @@ class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Refr
client_id=client.client_id,
scopes=authorization_code.scopes,
expires_at=int(time.time()) + 3600,
subject=authorization_code.subject,
)
self.refresh_tokens[refresh_token] = access_token
@ -109,6 +111,7 @@ class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Refr
client_id=token_info.client_id,
scopes=token_info.scopes,
expires_at=token_info.expires_at,
subject=token_info.subject,
)
return refresh_obj
@ -142,6 +145,7 @@ class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Refr
client_id=client.client_id,
scopes=scopes or token_info.scopes,
expires_at=int(time.time()) + 3600,
subject=refresh_token.subject,
)
self.refresh_tokens[new_refresh_token] = new_access_token
@ -170,6 +174,7 @@ class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Refr
client_id=token_info.client_id,
scopes=token_info.scopes,
expires_at=token_info.expires_at,
subject=token_info.subject,
)
async def revoke_token(self, token: AccessToken | RefreshToken) -> None:
@ -783,6 +788,7 @@ class TestAuthEndpoints:
assert auth_info.client_id == client_info["client_id"]
assert "read" in auth_info.scopes
assert "write" in auth_info.scopes
assert auth_info.subject == "test-user"
# 6. Refresh the token
response = await test_client.post(
@ -803,6 +809,10 @@ class TestAuthEndpoints:
assert new_token_response["access_token"] != access_token
assert new_token_response["refresh_token"] != refresh_token
refreshed_auth_info = await mock_oauth_provider.load_access_token(new_token_response["access_token"])
assert refreshed_auth_info
assert refreshed_auth_info.subject == "test-user"
# 7. Revoke the token
response = await test_client.post(
"/revoke",