From 1ca53b4134a9047a5872ebdf71f83af34107a180 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Wed, 29 Oct 2025 11:06:21 -0700
Subject: [PATCH] Add base_authority parameter to AzureProvider for Azure
Government support (#2306)
---
docs/integrations/azure.mdx | 10 ++
src/fastmcp/server/auth/providers/azure.py | 31 ++++++-
tests/server/auth/providers/test_azure.py | 102 +++++++++++++++++++++
3 files changed, 138 insertions(+), 5 deletions(-)
diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx
index 1c6730f87..90dcfd1bc 100644
--- a/docs/integrations/azure.mdx
+++ b/docs/integrations/azure.mdx
@@ -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
Application ID URI used to prefix scopes during authorization.
+
+
+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).
+
Example `.env` file:
diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py
index 163585046..4c7cb8359 100644
--- a/src/fastmcp/server/auth/providers/azure.py
+++ b/src/fastmcp/server/auth/providers/azure.py
@@ -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(
diff --git a/tests/server/auth/providers/test_azure.py b/tests/server/auth/providers/test_azure.py
index b4b7428de..168384eb0 100644
--- a/tests/server/auth/providers/test_azure.py
+++ b/tests/server/auth/providers/test_azure.py
@@ -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