Skip to content

Commit 3eb5799

Browse files
authored
Add subject and claims to AccessToken (#2686)
1 parent e8e6484 commit 3eb5799

6 files changed

Lines changed: 27 additions & 2 deletions

File tree

examples/servers/simple-auth/mcp_simple_auth/auth_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ async def introspect_handler(request: Request) -> Response:
120120
"iat": int(time.time()),
121121
"token_type": "Bearer",
122122
"aud": access_token.resource, # RFC 8707 audience claim
123+
"sub": access_token.subject, # RFC 7662 subject
124+
"iss": str(server_settings.server_url),
123125
}
124126
)
125127

examples/servers/simple-auth/mcp_simple_auth/simple_auth_provider.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ async def handle_simple_callback(self, username: str, password: str, state: str)
181181
scopes=[self.settings.mcp_scope],
182182
code_challenge=code_challenge,
183183
resource=resource, # RFC 8707
184+
subject=username,
184185
)
185186
self.auth_codes[new_code] = auth_code
186187

@@ -219,6 +220,7 @@ async def exchange_authorization_code(
219220
scopes=authorization_code.scopes,
220221
expires_at=int(time.time()) + 3600,
221222
resource=authorization_code.resource, # RFC 8707
223+
subject=authorization_code.subject,
222224
)
223225

224226
# Store user data mapping for this token

examples/servers/simple-auth/mcp_simple_auth/token_verifier.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ async def verify_token(self, token: str) -> AccessToken | None:
7575
scopes=data.get("scope", "").split() if data.get("scope") else [],
7676
expires_at=data.get("exp"),
7777
resource=data.get("aud"), # Include resource in token
78+
subject=data.get("sub"), # RFC 7662 subject (resource owner)
79+
claims=data,
7880
)
7981
except Exception as e:
8082
logger.warning(f"Token introspection failed: {e}")

src/mcp/server/auth/provider.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from typing import Generic, Literal, Protocol, TypeVar
2+
from typing import Any, Generic, Literal, Protocol, TypeVar
33
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
44

55
from pydantic import AnyUrl, BaseModel
@@ -25,13 +25,15 @@ class AuthorizationCode(BaseModel):
2525
redirect_uri: AnyUrl
2626
redirect_uri_provided_explicitly: bool
2727
resource: str | None = None # RFC 8707 resource indicator
28+
subject: str | None = None # resource owner; propagate to the issued AccessToken
2829

2930

3031
class RefreshToken(BaseModel):
3132
token: str
3233
client_id: str
3334
scopes: list[str]
3435
expires_at: int | None = None
36+
subject: str | None = None # resource owner; propagate to refreshed AccessTokens
3537

3638

3739
class AccessToken(BaseModel):
@@ -40,6 +42,8 @@ class AccessToken(BaseModel):
4042
scopes: list[str]
4143
expires_at: int | None = None
4244
resource: str | None = None # RFC 8707 resource indicator
45+
subject: str | None = None # RFC 7662/9068 `sub`: resource owner; unique only per issuer
46+
claims: dict[str, Any] | None = None # additional claims (e.g. `iss`, `act`)
4347

4448

4549
RegistrationErrorCode = Literal[

src/mcp/server/mcpserver/context.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,14 @@ async def log(
208208
related_request_id=self.request_id,
209209
)
210210

211+
# TODO(maxisbey): see if this is needed otherwise remove
211212
@property
212213
def client_id(self) -> str | None:
213-
"""Get the client ID if available."""
214+
"""Get the client ID if available.
215+
216+
Note: this reads from the MCP request's `_meta` params, not the OAuth
217+
bearer token. For that, use `get_access_token().client_id`.
218+
"""
214219
return self.request_context.meta.get("client_id") if self.request_context.meta else None # pragma: no cover
215220

216221
@property

tests/server/mcpserver/auth/test_auth_integration.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ async def authorize(self, client: OAuthClientInformationFull, params: Authorizat
5353
redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
5454
expires_at=time.time() + 300,
5555
scopes=params.scopes or ["read", "write"],
56+
subject="test-user",
5657
)
5758
self.auth_codes[code.code] = code
5859

@@ -79,6 +80,7 @@ async def exchange_authorization_code(
7980
client_id=client.client_id,
8081
scopes=authorization_code.scopes,
8182
expires_at=int(time.time()) + 3600,
83+
subject=authorization_code.subject,
8284
)
8385

8486
self.refresh_tokens[refresh_token] = access_token
@@ -108,6 +110,7 @@ async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_t
108110
client_id=token_info.client_id,
109111
scopes=token_info.scopes,
110112
expires_at=token_info.expires_at,
113+
subject=token_info.subject,
111114
)
112115

113116
return refresh_obj
@@ -141,6 +144,7 @@ async def exchange_refresh_token(
141144
client_id=client.client_id,
142145
scopes=scopes or token_info.scopes,
143146
expires_at=int(time.time()) + 3600,
147+
subject=refresh_token.subject,
144148
)
145149

146150
self.refresh_tokens[new_refresh_token] = new_access_token
@@ -169,6 +173,7 @@ async def load_access_token(self, token: str) -> AccessToken | None:
169173
client_id=token_info.client_id,
170174
scopes=token_info.scopes,
171175
expires_at=token_info.expires_at,
176+
subject=token_info.subject,
172177
)
173178

174179
async def revoke_token(self, token: AccessToken | RefreshToken) -> None:
@@ -832,6 +837,7 @@ async def test_authorization_get(
832837
assert auth_info.client_id == client_info["client_id"]
833838
assert "read" in auth_info.scopes
834839
assert "write" in auth_info.scopes
840+
assert auth_info.subject == "test-user"
835841

836842
# 6. Refresh the token
837843
response = await test_client.post(
@@ -852,6 +858,10 @@ async def test_authorization_get(
852858
assert new_token_response["access_token"] != access_token
853859
assert new_token_response["refresh_token"] != refresh_token
854860

861+
refreshed_auth_info = await mock_oauth_provider.load_access_token(new_token_response["access_token"])
862+
assert refreshed_auth_info
863+
assert refreshed_auth_info.subject == "test-user"
864+
855865
# 7. Revoke the token
856866
response = await test_client.post(
857867
"/revoke",

0 commit comments

Comments
 (0)