SEP-2207: Refresh token guidance (#2039)
This commit is contained in:
parent
5cbd259c3b
commit
437d15aa71
2 changed files with 392 additions and 17 deletions
|
|
@ -320,7 +320,7 @@ class OAuthClientProvider(httpx.Auth):
|
|||
raise OAuthFlowError("No callback handler provided for authorization code grant") # pragma: no cover
|
||||
|
||||
if self.context.oauth_metadata and self.context.oauth_metadata.authorization_endpoint:
|
||||
auth_endpoint = str(self.context.oauth_metadata.authorization_endpoint) # pragma: no cover
|
||||
auth_endpoint = str(self.context.oauth_metadata.authorization_endpoint)
|
||||
else:
|
||||
auth_base_url = self.context.get_authorization_base_url(self.context.server_url)
|
||||
auth_endpoint = urljoin(auth_base_url, "/authorize")
|
||||
|
|
@ -343,11 +343,16 @@ class OAuthClientProvider(httpx.Auth):
|
|||
|
||||
# Only include resource param if conditions are met
|
||||
if self.context.should_include_resource_param(self.context.protocol_version):
|
||||
auth_params["resource"] = self.context.get_resource_url() # RFC 8707 # pragma: no cover
|
||||
auth_params["resource"] = self.context.get_resource_url() # RFC 8707
|
||||
|
||||
if self.context.client_metadata.scope: # pragma: no branch
|
||||
auth_params["scope"] = self.context.client_metadata.scope
|
||||
|
||||
# OIDC requires prompt=consent when offline_access is requested
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
if "offline_access" in self.context.client_metadata.scope.split():
|
||||
auth_params["prompt"] = "consent"
|
||||
|
||||
authorization_url = f"{auth_endpoint}?{urlencode(auth_params)}"
|
||||
await self.context.redirect_handler(authorization_url)
|
||||
|
||||
|
|
@ -576,6 +581,7 @@ class OAuthClientProvider(httpx.Auth):
|
|||
extract_scope_from_www_auth(response),
|
||||
self.context.protected_resource_metadata,
|
||||
self.context.oauth_metadata,
|
||||
self.context.client_metadata.grant_types,
|
||||
)
|
||||
|
||||
# Step 4: Register client or use URL-based client ID (CIMD)
|
||||
|
|
@ -622,7 +628,10 @@ class OAuthClientProvider(httpx.Auth):
|
|||
try:
|
||||
# Step 2a: Update the required scopes
|
||||
self.context.client_metadata.scope = get_client_metadata_scopes(
|
||||
extract_scope_from_www_auth(response), self.context.protected_resource_metadata
|
||||
extract_scope_from_www_auth(response),
|
||||
self.context.protected_resource_metadata,
|
||||
self.context.oauth_metadata,
|
||||
self.context.client_metadata.grant_types,
|
||||
)
|
||||
|
||||
# Step 2b: Perform (re-)authorization and token exchange
|
||||
|
|
|
|||
|
|
@ -99,24 +99,36 @@ def get_client_metadata_scopes(
|
|||
www_authenticate_scope: str | None,
|
||||
protected_resource_metadata: ProtectedResourceMetadata | None,
|
||||
authorization_server_metadata: OAuthMetadata | None = None,
|
||||
client_grant_types: list[str] | None = None,
|
||||
) -> str | None:
|
||||
"""Select scopes as outlined in the 'Scope Selection Strategy' in the MCP spec."""
|
||||
# Per MCP spec, scope selection priority order:
|
||||
# 1. Use scope from WWW-Authenticate header (if provided)
|
||||
# 2. Use all scopes from PRM scopes_supported (if available)
|
||||
# 3. Omit scope parameter if neither is available
|
||||
"""Select effective scopes and augment for refresh token support."""
|
||||
selected_scope: str | None = None
|
||||
|
||||
# MCP spec scope selection priority:
|
||||
# 1. WWW-Authenticate header scope
|
||||
# 2. PRM scopes_supported
|
||||
# 3. AS scopes_supported (SDK fallback)
|
||||
# 4. Omit scope parameter
|
||||
if www_authenticate_scope is not None:
|
||||
# Priority 1: WWW-Authenticate header scope
|
||||
return www_authenticate_scope
|
||||
selected_scope = www_authenticate_scope
|
||||
elif protected_resource_metadata is not None and protected_resource_metadata.scopes_supported is not None:
|
||||
# Priority 2: PRM scopes_supported
|
||||
return " ".join(protected_resource_metadata.scopes_supported)
|
||||
selected_scope = " ".join(protected_resource_metadata.scopes_supported)
|
||||
elif authorization_server_metadata is not None and authorization_server_metadata.scopes_supported is not None:
|
||||
return " ".join(authorization_server_metadata.scopes_supported) # pragma: no cover
|
||||
else:
|
||||
# Priority 3: Omit scope parameter
|
||||
return None
|
||||
selected_scope = " ".join(authorization_server_metadata.scopes_supported)
|
||||
|
||||
# SEP-2207: append offline_access when the AS supports it and the client can use refresh tokens
|
||||
if (
|
||||
selected_scope is not None
|
||||
and authorization_server_metadata is not None
|
||||
and authorization_server_metadata.scopes_supported is not None
|
||||
and "offline_access" in authorization_server_metadata.scopes_supported
|
||||
and client_grant_types is not None
|
||||
and "refresh_token" in client_grant_types
|
||||
and "offline_access" not in selected_scope.split()
|
||||
):
|
||||
selected_scope = f"{selected_scope} offline_access"
|
||||
|
||||
return selected_scope
|
||||
|
||||
|
||||
def build_oauth_authorization_server_metadata_discovery_urls(auth_server_url: str | None, server_url: str) -> list[str]:
|
||||
|
|
|
|||
Loading…
Reference in a new issue