fix Azure token refresh issue

This commit is contained in:
Nestor Qin 2025-11-19 19:02:31 +00:00
commit 9e28da11f2
2 changed files with 68 additions and 6 deletions

View file

@ -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")

View file

@ -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