[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()),
|
"iat": int(time.time()),
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
"aud": access_token.resource, # RFC 8707 audience claim
|
"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],
|
scopes=[self.settings.mcp_scope],
|
||||||
code_challenge=code_challenge,
|
code_challenge=code_challenge,
|
||||||
resource=resource, # RFC 8707
|
resource=resource, # RFC 8707
|
||||||
|
subject=username,
|
||||||
)
|
)
|
||||||
self.auth_codes[new_code] = auth_code
|
self.auth_codes[new_code] = auth_code
|
||||||
|
|
||||||
|
|
@ -224,6 +225,7 @@ class SimpleOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Re
|
||||||
scopes=authorization_code.scopes,
|
scopes=authorization_code.scopes,
|
||||||
expires_at=int(time.time()) + 3600,
|
expires_at=int(time.time()) + 3600,
|
||||||
resource=authorization_code.resource, # RFC 8707
|
resource=authorization_code.resource, # RFC 8707
|
||||||
|
subject=authorization_code.subject,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store user data mapping for this token
|
# Store user data mapping for this token
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ class IntrospectionTokenVerifier(TokenVerifier):
|
||||||
scopes=data.get("scope", "").split() if data.get("scope") else [],
|
scopes=data.get("scope", "").split() if data.get("scope") else [],
|
||||||
expires_at=data.get("exp"),
|
expires_at=data.get("exp"),
|
||||||
resource=data.get("aud"), # Include resource in token
|
resource=data.get("aud"), # Include resource in token
|
||||||
|
subject=data.get("sub"), # RFC 7662 subject (resource owner)
|
||||||
|
claims=data,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Token introspection failed: {e}")
|
logger.warning(f"Token introspection failed: {e}")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from dataclasses import dataclass
|
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 urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
from pydantic import AnyUrl, BaseModel
|
from pydantic import AnyUrl, BaseModel
|
||||||
|
|
@ -25,6 +25,7 @@ class AuthorizationCode(BaseModel):
|
||||||
redirect_uri: AnyUrl
|
redirect_uri: AnyUrl
|
||||||
redirect_uri_provided_explicitly: bool
|
redirect_uri_provided_explicitly: bool
|
||||||
resource: str | None = None # RFC 8707 resource indicator
|
resource: str | None = None # RFC 8707 resource indicator
|
||||||
|
subject: str | None = None # resource owner; propagate to the issued AccessToken
|
||||||
|
|
||||||
|
|
||||||
class RefreshToken(BaseModel):
|
class RefreshToken(BaseModel):
|
||||||
|
|
@ -32,6 +33,7 @@ class RefreshToken(BaseModel):
|
||||||
client_id: str
|
client_id: str
|
||||||
scopes: list[str]
|
scopes: list[str]
|
||||||
expires_at: int | None = None
|
expires_at: int | None = None
|
||||||
|
subject: str | None = None # resource owner; propagate to refreshed AccessTokens
|
||||||
|
|
||||||
|
|
||||||
class AccessToken(BaseModel):
|
class AccessToken(BaseModel):
|
||||||
|
|
@ -40,6 +42,8 @@ class AccessToken(BaseModel):
|
||||||
scopes: list[str]
|
scopes: list[str]
|
||||||
expires_at: int | None = None
|
expires_at: int | None = None
|
||||||
resource: str | None = None # RFC 8707 resource indicator
|
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[
|
RegistrationErrorCode = Literal[
|
||||||
|
|
|
||||||
|
|
@ -1282,9 +1282,14 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
|
||||||
related_request_id=self.request_id,
|
related_request_id=self.request_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO(maxisbey): see if this is needed otherwise remove
|
||||||
@property
|
@property
|
||||||
def client_id(self) -> str | None:
|
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 (
|
return (
|
||||||
getattr(self.request_context.meta, "client_id", None) if self.request_context.meta else None
|
getattr(self.request_context.meta, "client_id", None) if self.request_context.meta else None
|
||||||
) # pragma: no cover
|
) # pragma: no cover
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Refr
|
||||||
redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
|
redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
|
||||||
expires_at=time.time() + 300,
|
expires_at=time.time() + 300,
|
||||||
scopes=params.scopes or ["read", "write"],
|
scopes=params.scopes or ["read", "write"],
|
||||||
|
subject="test-user",
|
||||||
)
|
)
|
||||||
self.auth_codes[code.code] = code
|
self.auth_codes[code.code] = code
|
||||||
|
|
||||||
|
|
@ -80,6 +81,7 @@ class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Refr
|
||||||
client_id=client.client_id,
|
client_id=client.client_id,
|
||||||
scopes=authorization_code.scopes,
|
scopes=authorization_code.scopes,
|
||||||
expires_at=int(time.time()) + 3600,
|
expires_at=int(time.time()) + 3600,
|
||||||
|
subject=authorization_code.subject,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.refresh_tokens[refresh_token] = access_token
|
self.refresh_tokens[refresh_token] = access_token
|
||||||
|
|
@ -109,6 +111,7 @@ class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Refr
|
||||||
client_id=token_info.client_id,
|
client_id=token_info.client_id,
|
||||||
scopes=token_info.scopes,
|
scopes=token_info.scopes,
|
||||||
expires_at=token_info.expires_at,
|
expires_at=token_info.expires_at,
|
||||||
|
subject=token_info.subject,
|
||||||
)
|
)
|
||||||
|
|
||||||
return refresh_obj
|
return refresh_obj
|
||||||
|
|
@ -142,6 +145,7 @@ class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Refr
|
||||||
client_id=client.client_id,
|
client_id=client.client_id,
|
||||||
scopes=scopes or token_info.scopes,
|
scopes=scopes or token_info.scopes,
|
||||||
expires_at=int(time.time()) + 3600,
|
expires_at=int(time.time()) + 3600,
|
||||||
|
subject=refresh_token.subject,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.refresh_tokens[new_refresh_token] = new_access_token
|
self.refresh_tokens[new_refresh_token] = new_access_token
|
||||||
|
|
@ -170,6 +174,7 @@ class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, Refr
|
||||||
client_id=token_info.client_id,
|
client_id=token_info.client_id,
|
||||||
scopes=token_info.scopes,
|
scopes=token_info.scopes,
|
||||||
expires_at=token_info.expires_at,
|
expires_at=token_info.expires_at,
|
||||||
|
subject=token_info.subject,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def revoke_token(self, token: AccessToken | RefreshToken) -> None:
|
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 auth_info.client_id == client_info["client_id"]
|
||||||
assert "read" in auth_info.scopes
|
assert "read" in auth_info.scopes
|
||||||
assert "write" in auth_info.scopes
|
assert "write" in auth_info.scopes
|
||||||
|
assert auth_info.subject == "test-user"
|
||||||
|
|
||||||
# 6. Refresh the token
|
# 6. Refresh the token
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
|
|
@ -803,6 +809,10 @@ class TestAuthEndpoints:
|
||||||
assert new_token_response["access_token"] != access_token
|
assert new_token_response["access_token"] != access_token
|
||||||
assert new_token_response["refresh_token"] != refresh_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
|
# 7. Revoke the token
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
"/revoke",
|
"/revoke",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue