From e3a1cd1f6781e967ea1f51c9e4c372fc26af94d0 Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Wed, 15 Apr 2026 08:35:54 +0530 Subject: [PATCH] feat(auth): add subject field to AccessToken for user identification Add an optional `subject` field to `AccessToken` to store the JWT `sub` claim (user ID). This allows token verifiers to pass through user identity without requiring consumers to re-decode the JWT. Backward compatible - field defaults to None. Fixes #1038 --- src/mcp/server/auth/provider.py | 1 + .../auth/middleware/test_auth_context.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/mcp/server/auth/provider.py b/src/mcp/server/auth/provider.py index 957082a85..89032ab0e 100644 --- a/src/mcp/server/auth/provider.py +++ b/src/mcp/server/auth/provider.py @@ -40,6 +40,7 @@ class AccessToken(BaseModel): scopes: list[str] expires_at: int | None = None resource: str | None = None # RFC 8707 resource indicator + subject: str | None = None # Subject identifier (typically the "sub" JWT claim / user ID) RegistrationErrorCode = Literal[ diff --git a/tests/server/auth/middleware/test_auth_context.py b/tests/server/auth/middleware/test_auth_context.py index 66481bcf7..722863867 100644 --- a/tests/server/auth/middleware/test_auth_context.py +++ b/tests/server/auth/middleware/test_auth_context.py @@ -117,3 +117,23 @@ async def send(message: Message) -> None: # pragma: no cover # Verify context is still empty after middleware assert auth_context_var.get() is None assert get_access_token() is None + + +def test_access_token_subject_field(): + """Test that AccessToken supports the optional subject field.""" + # Without subject (backward compatible) + token_no_sub = AccessToken( + token="token1", + client_id="client1", + scopes=["read"], + ) + assert token_no_sub.subject is None + + # With subject + token_with_sub = AccessToken( + token="token2", + client_id="client2", + scopes=["read", "write"], + subject="user-123", + ) + assert token_with_sub.subject == "user-123"