[v1.x] Add subject and claims to AccessToken (#2690)
This commit is contained in:
parent
9773a3f75e
commit
1abcca2408
6 changed files with 27 additions and 2 deletions
|
|
@ -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),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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[
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue