mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 14:04:18 +02:00
fix Azure token refresh issue
This commit is contained in:
parent
dd876b5833
commit
9e28da11f2
2 changed files with 68 additions and 6 deletions
|
|
@ -1273,6 +1273,23 @@ class OAuthProxy(OAuthProvider):
|
|||
# Refresh Token Flow
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _prepare_scopes_for_upstream_refresh(self, scopes: list[str]) -> list[str]:
|
||||
"""Prepare scopes for upstream token refresh request.
|
||||
|
||||
Override this method to transform scopes before sending to upstream provider.
|
||||
For example, Azure needs to prefix scopes and add additional Graph scopes.
|
||||
|
||||
The scopes parameter represents what should be stored in the RefreshToken.
|
||||
This method returns what should be sent to the upstream provider.
|
||||
|
||||
Args:
|
||||
scopes: Base scopes that will be stored in RefreshToken
|
||||
|
||||
Returns:
|
||||
Scopes to send to upstream provider (may be transformed/augmented)
|
||||
"""
|
||||
return scopes
|
||||
|
||||
async def load_refresh_token(
|
||||
self,
|
||||
client: OAuthClientInformationFull,
|
||||
|
|
@ -1333,12 +1350,17 @@ class OAuthProxy(OAuthProvider):
|
|||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
# Allow child classes to transform scopes before sending to upstream
|
||||
# This enables provider-specific scope formatting (e.g., Azure prefixing)
|
||||
# while keeping original scopes in storage
|
||||
upstream_scopes = self._prepare_scopes_for_upstream_refresh(scopes)
|
||||
|
||||
try:
|
||||
logger.debug("Refreshing upstream token (jti=%s)", refresh_jti[:8])
|
||||
token_response: dict[str, Any] = await oauth_client.refresh_token( # type: ignore[misc]
|
||||
url=self._upstream_token_endpoint,
|
||||
refresh_token=upstream_token_set.refresh_token,
|
||||
scope=" ".join(scopes) if scopes else None,
|
||||
scope=" ".join(upstream_scopes) if upstream_scopes else None,
|
||||
**self._extra_token_params,
|
||||
)
|
||||
logger.debug("Successfully refreshed upstream token")
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from key_value.aio.protocols import AsyncKeyValue
|
||||
from mcp.server.auth.provider import AuthorizationParams
|
||||
from mcp.shared.auth import OAuthClientInformationFull
|
||||
from pydantic import SecretStr, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
|
@ -19,10 +21,6 @@ from fastmcp.utilities.auth import parse_scopes
|
|||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import NotSet, NotSetT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mcp.server.auth.provider import AuthorizationParams
|
||||
from mcp.shared.auth import OAuthClientInformationFull
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
|
|
@ -358,3 +356,45 @@ class AzureProvider(OAuthProxy):
|
|||
|
||||
# Let parent build the URL with prefixed scopes
|
||||
return super()._build_upstream_authorize_url(txn_id, modified_transaction)
|
||||
|
||||
def _prepare_scopes_for_upstream_refresh(self, scopes: list[str]) -> list[str]:
|
||||
"""Prepare scopes for Azure token refresh.
|
||||
|
||||
Azure requires:
|
||||
1. Fully-qualified custom scopes (e.g., "api://xxx/read" not "read")
|
||||
2. Microsoft Graph scopes (e.g., "User.Read", "openid") sent as-is
|
||||
3. Additional scopes from provider config (additional_authorize_scopes)
|
||||
|
||||
This method transforms base client scopes for Azure while keeping them
|
||||
unprefixed in storage to prevent accumulation.
|
||||
|
||||
Args:
|
||||
scopes: Base scopes from RefreshToken (unprefixed, e.g., ["read"])
|
||||
|
||||
Returns:
|
||||
Scopes formatted for Azure token endpoint
|
||||
"""
|
||||
logger.debug(f"Base scopes from storage: {scopes}")
|
||||
|
||||
# Filter out any additional_authorize_scopes that may have been stored
|
||||
# (they shouldn't be in storage, but clean them up if they are)
|
||||
additional_scopes_set = set(self.additional_authorize_scopes or [])
|
||||
base_scopes = [s for s in scopes if s not in additional_scopes_set]
|
||||
|
||||
# Prefix base scopes with identifier_uri for Azure
|
||||
prefixed_scopes = []
|
||||
for scope in base_scopes:
|
||||
if "://" in scope or "/" in scope:
|
||||
# Already fully-qualified (e.g., "api://xxx/read")
|
||||
prefixed_scopes.append(scope)
|
||||
else:
|
||||
# Unprefixed client scope - prefix with identifier_uri
|
||||
prefixed_scopes.append(f"{self.identifier_uri}/{scope}")
|
||||
|
||||
# Add additional scopes (Graph + OIDC) for the Azure request
|
||||
# These are NOT stored in RefreshToken, only sent to Azure
|
||||
if self.additional_authorize_scopes:
|
||||
prefixed_scopes.extend(self.additional_authorize_scopes)
|
||||
|
||||
logger.debug(f"Scopes for Azure token endpoint: {prefixed_scopes}")
|
||||
return prefixed_scopes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue