diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py index b470ae488..1c9978432 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py @@ -175,6 +175,18 @@ def _resolve_authority( return f"https://login.microsoftonline.com/{tenant_id}" + @staticmethod + def _resolve_azure_region(config: AgentAuthConfiguration) -> str | None: + """Resolves the Azure regional token service (ESTS-R) to use, if configured. + + Returns the configured region only when it is populated and non-whitespace, + otherwise None so that MSAL falls back to the global token service. + """ + azure_region = getattr(config, "AZURE_REGION", None) + if azure_region and azure_region.strip(): + return azure_region + return None + @staticmethod def _resolve_tenant_id( config: AgentAuthConfiguration, tenant_id: str | None = None @@ -253,6 +265,7 @@ def get_assertion() -> str: client_id=self._msal_configuration.CLIENT_ID, authority=authority, client_credential=client_credential, + azure_region=MsalAuth._resolve_azure_region(self._msal_configuration), ) def _client_rep( @@ -379,6 +392,7 @@ async def get_agentic_instance_token( client_id=agent_app_instance_id, authority=authority, client_credential={"client_assertion": agent_token_result}, + azure_region=MsalAuth._resolve_azure_region(self._msal_configuration), # token_cache=self._token_cache, ) @@ -474,6 +488,7 @@ async def get_agentic_user_token( client_id=agent_app_instance_id, authority=authority, client_credential={"client_assertion": agent_token}, + azure_region=MsalAuth._resolve_azure_region(self._msal_configuration), # token_cache=self._token_cache, ) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py index ee5798d7a..f98fcca1e 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py @@ -20,6 +20,8 @@ class AgentAuthConfiguration: SCOPES: The scopes to request AUTHORITY: The authority URL for the Azure AD (if different from the default). ALT_BLUEPRINT_ID: An optional alternative blueprint ID used when constructing a connector client. + AZURE_REGION: The Azure regional token service to use for token acquisition (ESTS-R). + This feature is currently available to first-party applications only. """ TENANT_ID: str | None @@ -32,6 +34,7 @@ class AgentAuthConfiguration: SCOPES: list[str] | None AUTHORITY: str | None ALT_BLUEPRINT_ID: str | None + AZURE_REGION: str | None ANONYMOUS_ALLOWED: bool = False # Multi-connection support: Maintains a map of all configured connections @@ -54,6 +57,7 @@ def __init__( federated_client_id: str | None = None, authority: str | None = None, scopes: list[str] | None = None, + azure_region: str | None = None, anonymous_allowed: bool = False, **kwargs: str, ): @@ -69,6 +73,13 @@ def __init__( "FEDERATEDCLIENTID", None ) self.SCOPES = scopes or kwargs.get("SCOPES", None) + # Azure regional token service. Falls back to the legacy "REGIONALAUTHORITY" + # configuration key when "AZUREREGION" is not provided. + self.AZURE_REGION = ( + azure_region + or kwargs.get("AZUREREGION", None) + or kwargs.get("REGIONALAUTHORITY", None) + ) self.ALT_BLUEPRINT_ID = kwargs.get("ALT_BLUEPRINT_NAME", None) self.ANONYMOUS_ALLOWED = anonymous_allowed or kwargs.get( "ANONYMOUS_ALLOWED", False diff --git a/tests/authentication_msal/test_msal_auth.py b/tests/authentication_msal/test_msal_auth.py index e9e5f8ec8..f163424f9 100644 --- a/tests/authentication_msal/test_msal_auth.py +++ b/tests/authentication_msal/test_msal_auth.py @@ -213,6 +213,53 @@ def test_resolve_authority_regex_preserves_path(self): ) +class TestMsalAuthAzureRegion: + """ + Test suite for resolving the Azure regional token service (ESTS-R). + """ + + def test_resolve_azure_region_when_configured(self): + config = AgentAuthConfiguration(azure_region="westus") + assert MsalAuth._resolve_azure_region(config) == "westus" + + def test_resolve_azure_region_none_when_unset(self): + config = AgentAuthConfiguration() + assert MsalAuth._resolve_azure_region(config) is None + + def test_resolve_azure_region_none_when_whitespace(self): + config = AgentAuthConfiguration(azure_region=" ") + assert MsalAuth._resolve_azure_region(config) is None + + def test_create_client_application_passes_azure_region(self, mocker): + config = AgentAuthConfiguration( + auth_type=AuthTypes.client_secret, + tenant_id="12345678-1234-1234-1234-123456789abc", + client_id="test-client-id", + client_secret="test-client-secret", + azure_region="westus", + ) + mock_cca = mocker.patch( + "microsoft_agents.authentication.msal.msal_auth.ConfidentialClientApplication" + ) + auth = MsalAuth(config) + auth._create_client_application() + assert mock_cca.call_args.kwargs["azure_region"] == "westus" + + def test_create_client_application_azure_region_defaults_none(self, mocker): + config = AgentAuthConfiguration( + auth_type=AuthTypes.client_secret, + tenant_id="12345678-1234-1234-1234-123456789abc", + client_id="test-client-id", + client_secret="test-client-secret", + ) + mock_cca = mocker.patch( + "microsoft_agents.authentication.msal.msal_auth.ConfidentialClientApplication" + ) + auth = MsalAuth(config) + auth._create_client_application() + assert mock_cca.call_args.kwargs["azure_region"] is None + + # class TestMsalAuthAgentic: # @pytest.mark.asyncio diff --git a/tests/hosting_core/test_auth_configuration.py b/tests/hosting_core/test_auth_configuration.py index ca77a9c9d..64c64796b 100644 --- a/tests/hosting_core/test_auth_configuration.py +++ b/tests/hosting_core/test_auth_configuration.py @@ -75,3 +75,24 @@ def test_empty_settings(self): assert auth_config.CONNECTION_NAME is None assert auth_config.AUTHORITY is None assert auth_config.SCOPES is None + assert auth_config.AZURE_REGION is None + + def test_azure_region_from_parameter(self): + auth_config = AgentAuthConfiguration(azure_region="westus") + assert auth_config.AZURE_REGION == "westus" + + def test_azure_region_from_kwargs(self): + auth_config = AgentAuthConfiguration(AZUREREGION="eastus") + assert auth_config.AZURE_REGION == "eastus" + + def test_azure_region_legacy_regional_authority_fallback(self): + # When AZUREREGION is not provided, fall back to the legacy + # RegionalAuthority configuration key. + auth_config = AgentAuthConfiguration(REGIONALAUTHORITY="westeurope") + assert auth_config.AZURE_REGION == "westeurope" + + def test_azure_region_prefers_azure_region_over_legacy(self): + auth_config = AgentAuthConfiguration( + AZUREREGION="eastus", REGIONALAUTHORITY="westeurope" + ) + assert auth_config.AZURE_REGION == "eastus"