mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 14:04:18 +02:00
Add base_authority parameter to AzureProvider for Azure Government support (#2306)
This commit is contained in:
parent
463b336941
commit
1ca53b4134
3 changed files with 138 additions and 5 deletions
|
|
@ -128,6 +128,7 @@ auth_provider = AzureProvider(
|
|||
# Optional: request additional upstream scopes in the authorize request
|
||||
# additional_authorize_scopes=["User.Read", "offline_access", "openid", "email"],
|
||||
# redirect_path="/auth/callback" # Default value, customize if needed
|
||||
# base_authority="login.microsoftonline.us" # For Azure Government (default: login.microsoftonline.com)
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Azure Secured App", auth=auth_provider)
|
||||
|
|
@ -315,6 +316,15 @@ Comma-, space-, or JSON-separated list of additional scopes to include in the au
|
|||
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_IDENTIFIER_URI" default="api://{client_id}">
|
||||
Application ID URI used to prefix scopes during authorization.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="FASTMCP_SERVER_AUTH_AZURE_BASE_AUTHORITY" default="login.microsoftonline.com">
|
||||
Azure authority base URL. Override this to use Azure Government:
|
||||
|
||||
- `login.microsoftonline.com` - Azure Public Cloud (default)
|
||||
- `login.microsoftonline.us` - Azure Government
|
||||
|
||||
This setting affects all Azure OAuth endpoints (authorization, token, issuer, JWKS).
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
||||
Example `.env` file:
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class AzureProviderSettings(BaseSettings):
|
|||
additional_authorize_scopes: list[str] | None = None
|
||||
allowed_client_redirect_uris: list[str] | None = None
|
||||
jwt_signing_key: str | None = None
|
||||
base_authority: str = "login.microsoftonline.com"
|
||||
|
||||
@field_validator("required_scopes", mode="before")
|
||||
@classmethod
|
||||
|
|
@ -93,6 +94,7 @@ class AzureProvider(OAuthProxy):
|
|||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.azure import AzureProvider
|
||||
|
||||
# Standard Azure (Public Cloud)
|
||||
auth = AzureProvider(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
|
|
@ -103,6 +105,16 @@ class AzureProvider(OAuthProxy):
|
|||
# identifier_uri defaults to api://{client_id}
|
||||
)
|
||||
|
||||
# Azure Government
|
||||
auth_gov = AzureProvider(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
tenant_id="your-tenant-id",
|
||||
required_scopes=["read", "write"],
|
||||
base_authority="login.microsoftonline.us", # Override for Azure Gov
|
||||
base_url="http://localhost:8000",
|
||||
)
|
||||
|
||||
mcp = FastMCP("My App", auth=auth)
|
||||
```
|
||||
"""
|
||||
|
|
@ -123,6 +135,7 @@ class AzureProvider(OAuthProxy):
|
|||
client_storage: AsyncKeyValue | None = None,
|
||||
jwt_signing_key: str | bytes | NotSetT = NotSet,
|
||||
require_authorization_consent: bool = True,
|
||||
base_authority: str | NotSetT = NotSet,
|
||||
) -> None:
|
||||
"""Initialize Azure OAuth provider.
|
||||
|
||||
|
|
@ -138,6 +151,8 @@ class AzureProvider(OAuthProxy):
|
|||
issuer_url: Issuer URL for OAuth metadata (defaults to base_url). Use root-level URL
|
||||
to avoid 404s during discovery when mounting under a path.
|
||||
redirect_path: Redirect path configured in Azure App registration (defaults to "/auth/callback")
|
||||
base_authority: Azure authority base URL (defaults to "login.microsoftonline.com").
|
||||
For Azure Government, use "login.microsoftonline.us".
|
||||
required_scopes: Custom API scope names WITHOUT prefix (e.g., ["read", "write"]).
|
||||
- Automatically prefixed with identifier_uri during initialization
|
||||
- Validated on all tokens
|
||||
|
|
@ -180,6 +195,7 @@ class AzureProvider(OAuthProxy):
|
|||
"additional_authorize_scopes": additional_authorize_scopes,
|
||||
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
||||
"jwt_signing_key": jwt_signing_key,
|
||||
"base_authority": base_authority,
|
||||
}.items()
|
||||
if v is not NotSet
|
||||
}
|
||||
|
|
@ -218,9 +234,10 @@ class AzureProvider(OAuthProxy):
|
|||
tenant_id_final = settings.tenant_id
|
||||
|
||||
# Always validate tokens against the app's API client ID using JWT
|
||||
issuer = f"https://login.microsoftonline.com/{tenant_id_final}/v2.0"
|
||||
base_authority_final = settings.base_authority
|
||||
issuer = f"https://{base_authority_final}/{tenant_id_final}/v2.0"
|
||||
jwks_uri = (
|
||||
f"https://login.microsoftonline.com/{tenant_id_final}/discovery/v2.0/keys"
|
||||
f"https://{base_authority_final}/{tenant_id_final}/discovery/v2.0/keys"
|
||||
)
|
||||
|
||||
# Azure returns unprefixed scopes in JWT tokens, so validate against unprefixed scopes
|
||||
|
|
@ -239,10 +256,10 @@ class AzureProvider(OAuthProxy):
|
|||
|
||||
# Build Azure OAuth endpoints with tenant
|
||||
authorization_endpoint = (
|
||||
f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/authorize"
|
||||
f"https://{base_authority_final}/{tenant_id_final}/oauth2/v2.0/authorize"
|
||||
)
|
||||
token_endpoint = (
|
||||
f"https://login.microsoftonline.com/{tenant_id_final}/oauth2/v2.0/token"
|
||||
f"https://{base_authority_final}/{tenant_id_final}/oauth2/v2.0/token"
|
||||
)
|
||||
|
||||
# Initialize OAuth proxy with Azure endpoints
|
||||
|
|
@ -262,11 +279,15 @@ class AzureProvider(OAuthProxy):
|
|||
require_authorization_consent=require_authorization_consent,
|
||||
)
|
||||
|
||||
authority_info = ""
|
||||
if base_authority_final != "login.microsoftonline.com":
|
||||
authority_info = f" using authority {base_authority_final}"
|
||||
logger.info(
|
||||
"Initialized Azure OAuth provider for client %s with tenant %s%s",
|
||||
"Initialized Azure OAuth provider for client %s with tenant %s%s%s",
|
||||
settings.client_id,
|
||||
tenant_id_final,
|
||||
f" and identifier_uri {self.identifier_uri}" if self.identifier_uri else "",
|
||||
authority_info,
|
||||
)
|
||||
|
||||
async def authorize(
|
||||
|
|
|
|||
|
|
@ -385,3 +385,105 @@ class TestAzureProvider:
|
|||
)
|
||||
assert "Mail.Read" in upstream_url
|
||||
assert "User.Read" in upstream_url
|
||||
|
||||
def test_base_authority_defaults_to_public_cloud(self):
|
||||
"""Test that base_authority defaults to login.microsoftonline.com."""
|
||||
provider = AzureProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="test-tenant",
|
||||
required_scopes=["read"],
|
||||
jwt_signing_key="test-secret",
|
||||
)
|
||||
|
||||
assert (
|
||||
provider._upstream_authorization_endpoint
|
||||
== "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/authorize"
|
||||
)
|
||||
assert (
|
||||
provider._upstream_token_endpoint
|
||||
== "https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token"
|
||||
)
|
||||
assert (
|
||||
provider._token_validator.issuer # type: ignore[attr-defined]
|
||||
== "https://login.microsoftonline.com/test-tenant/v2.0"
|
||||
)
|
||||
assert (
|
||||
provider._token_validator.jwks_uri # type: ignore[attr-defined]
|
||||
== "https://login.microsoftonline.com/test-tenant/discovery/v2.0/keys"
|
||||
)
|
||||
|
||||
def test_base_authority_azure_government(self):
|
||||
"""Test Azure Government endpoints with login.microsoftonline.us."""
|
||||
provider = AzureProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="gov-tenant-id",
|
||||
required_scopes=["read"],
|
||||
base_authority="login.microsoftonline.us",
|
||||
jwt_signing_key="test-secret",
|
||||
)
|
||||
|
||||
assert (
|
||||
provider._upstream_authorization_endpoint
|
||||
== "https://login.microsoftonline.us/gov-tenant-id/oauth2/v2.0/authorize"
|
||||
)
|
||||
assert (
|
||||
provider._upstream_token_endpoint
|
||||
== "https://login.microsoftonline.us/gov-tenant-id/oauth2/v2.0/token"
|
||||
)
|
||||
assert (
|
||||
provider._token_validator.issuer # type: ignore[attr-defined]
|
||||
== "https://login.microsoftonline.us/gov-tenant-id/v2.0"
|
||||
)
|
||||
assert (
|
||||
provider._token_validator.jwks_uri # type: ignore[attr-defined]
|
||||
== "https://login.microsoftonline.us/gov-tenant-id/discovery/v2.0/keys"
|
||||
)
|
||||
|
||||
def test_base_authority_from_environment_variable(self):
|
||||
"""Test that base_authority can be set via environment variable."""
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID": "env-client-id",
|
||||
"FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET": "env-secret",
|
||||
"FASTMCP_SERVER_AUTH_AZURE_TENANT_ID": "env-tenant-id",
|
||||
"FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES": "read",
|
||||
"FASTMCP_SERVER_AUTH_AZURE_BASE_AUTHORITY": "login.microsoftonline.us",
|
||||
"FASTMCP_SERVER_AUTH_AZURE_JWT_SIGNING_KEY": "test-secret",
|
||||
},
|
||||
):
|
||||
provider = AzureProvider()
|
||||
|
||||
assert (
|
||||
provider._upstream_authorization_endpoint
|
||||
== "https://login.microsoftonline.us/env-tenant-id/oauth2/v2.0/authorize"
|
||||
)
|
||||
assert (
|
||||
provider._upstream_token_endpoint
|
||||
== "https://login.microsoftonline.us/env-tenant-id/oauth2/v2.0/token"
|
||||
)
|
||||
assert (
|
||||
provider._token_validator.issuer # type: ignore[attr-defined]
|
||||
== "https://login.microsoftonline.us/env-tenant-id/v2.0"
|
||||
)
|
||||
assert (
|
||||
provider._token_validator.jwks_uri # type: ignore[attr-defined]
|
||||
== "https://login.microsoftonline.us/env-tenant-id/discovery/v2.0/keys"
|
||||
)
|
||||
|
||||
def test_base_authority_with_special_tenant_values(self):
|
||||
"""Test that base_authority works with special tenant values like 'organizations'."""
|
||||
provider = AzureProvider(
|
||||
client_id="test_client",
|
||||
client_secret="test_secret",
|
||||
tenant_id="organizations",
|
||||
required_scopes=["read"],
|
||||
base_authority="login.microsoftonline.us",
|
||||
jwt_signing_key="test-secret",
|
||||
)
|
||||
|
||||
parsed = urlparse(provider._upstream_authorization_endpoint)
|
||||
assert parsed.netloc == "login.microsoftonline.us"
|
||||
assert "/organizations/" in parsed.path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue