From e0b56968f16f45e12b110cddde5267222e44d4d4 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 2 Feb 2026 18:37:43 -0800 Subject: [PATCH 01/15] Robustness of msal client caching --- .../authentication/msal/msal_auth.py | 106 ++++++++++++------ 1 file changed, 74 insertions(+), 32 deletions(-) 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 72e118ed..41a96f9b 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 @@ -3,10 +3,11 @@ from __future__ import annotations +import re import asyncio import logging import jwt -from typing import Optional +from typing import Optional, cast from urllib.parse import urlparse, ParseResult as URI from msal import ( ConfidentialClientApplication, @@ -53,11 +54,19 @@ def __init__(self, msal_configuration: AgentAuthConfiguration): """ self._msal_configuration = msal_configuration - self._msal_auth_client = None + self._msal_auth_client_map: dict[str, ConfidentialClientApplication | ManagedIdentityClient] = {} logger.debug( f"Initializing MsalAuth with configuration: {self._msal_configuration}" ) + def _client_rep(self, tenant_id: str | None = None, instance_id: str | None = None) -> str: + return f"{tenant_id}-{instance_id}" + + def _client(self, tenant_id: str | None = None, instance_id: str | None = None) -> ConfidentialClientApplication | ManagedIdentityClient: + tenant_id = tenant_id or self._msal_configuration.TENANT_ID + rep = self._client_rep(tenant_id, instance_id) + return cast(ConfidentialClientApplication | ManagedIdentityClient, self._msal_auth_client_map.get(rep)) + async def get_access_token( self, resource_url: str, scopes: list[str], force_refresh: bool = False ) -> str: @@ -71,15 +80,16 @@ async def get_access_token( local_scopes = self._resolve_scopes_list(instance_uri, scopes) self._create_client_application() - if isinstance(self._msal_auth_client, ManagedIdentityClient): + msal_auth_client = self._client() + if isinstance(msal_auth_client, ManagedIdentityClient): logger.info("Acquiring token using Managed Identity Client.") auth_result_payload = await _async_acquire_token_for_client( - self._msal_auth_client, resource=resource_url + msal_auth_client, resource=resource_url ) - elif isinstance(self._msal_auth_client, ConfidentialClientApplication): + elif isinstance(msal_auth_client, ConfidentialClientApplication): logger.info("Acquiring token using Confidential Client Application.") auth_result_payload = await _async_acquire_token_for_client( - self._msal_auth_client, scopes=local_scopes + msal_auth_client, scopes=local_scopes ) else: auth_result_payload = None @@ -106,20 +116,21 @@ async def acquire_token_on_behalf_of( """ self._create_client_application() - if isinstance(self._msal_auth_client, ManagedIdentityClient): + msal_auth_client = self._client() + if isinstance(msal_auth_client, ManagedIdentityClient): logger.error( "Attempted on-behalf-of flow with Managed Identity authentication." ) raise NotImplementedError( str(authentication_errors.OnBehalfOfFlowNotSupportedManagedIdentity) ) - elif isinstance(self._msal_auth_client, ConfidentialClientApplication): + elif isinstance(msal_auth_client, ConfidentialClientApplication): # TODO: Handling token error / acquisition failed # MSAL in Python does not support async, so we use asyncio.to_thread to run it in # a separate thread and avoid blocking the event loop token = await asyncio.to_thread( - lambda: self._msal_auth_client.acquire_token_on_behalf_of( + lambda: msal_auth_client.acquire_token_on_behalf_of( scopes=scopes, user_assertion=user_assertion ) ) @@ -135,21 +146,47 @@ async def acquire_token_on_behalf_of( return token["access_token"] logger.error( - f"On-behalf-of flow is not supported with the current authentication type: {self._msal_auth_client.__class__.__name__}" + f"On-behalf-of flow is not supported with the current authentication type: {msal_auth_client.__class__.__name__}" ) raise NotImplementedError( authentication_errors.OnBehalfOfFlowNotSupportedAuthType.format( - self._msal_auth_client.__class__.__name__ + msal_auth_client.__class__.__name__ ) ) + + @staticmethod + def _resolve_authority(config: AgentAuthConfiguration, tenant_id: str | None = None) -> str: + if not tenant_id: + return config.AUTHORITY or f"https://login.microsoftonline.com/{config.TENANT_ID}" + + if config.AUTHORITY: + return re.sub(r"/common(?=/|$)", f"/{tenant_id}", config.AUTHORITY) + + return f"https://login.microsoftonline.com/{tenant_id}" + + @staticmethod + def _resolve_tenant_id(config: AgentAuthConfiguration, tenant_id: str | None = None) -> str | None: - def _create_client_application(self) -> None: + if not config.TENANT_ID: + return None + + if tenant_id and config.TENANT_ID.lower() == "common": + return tenant_id + + return config.TENANT_ID + + def _create_client_application(self, tenant_id: str | None = None, instance_id: str | None = None) -> None: - if self._msal_auth_client: + tenant_id = tenant_id or self._msal_configuration.TENANT_ID + + if self._client(tenant_id, instance_id): return + + msal_auth_client = None + client_rep = self._client_rep(tenant_id, instance_id) if self._msal_configuration.AUTH_TYPE == AuthTypes.user_managed_identity: - self._msal_auth_client = ManagedIdentityClient( + msal_auth_client = ManagedIdentityClient( UserAssignedManagedIdentity( client_id=self._msal_configuration.CLIENT_ID ), @@ -157,13 +194,18 @@ def _create_client_application(self) -> None: ) elif self._msal_configuration.AUTH_TYPE == AuthTypes.system_managed_identity: - self._msal_auth_client = ManagedIdentityClient( + msal_auth_client = ManagedIdentityClient( SystemAssignedManagedIdentity(), http_client=Session(), ) else: - authority_path = self._msal_configuration.TENANT_ID or "botframework.com" - authority = f"https://login.microsoftonline.com/{authority_path}" + if self._msal_configuration.AUTHORITY: + authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id) + else: + # TODO -> there is no withTenantId, and I'm guessing it's equivalent to this + # circle back and check + # maybe I'm overthinking entirely... + authority = f"https://login.microsoftonline.com/{MsalAuth._resolve_tenant_id(self._msal_configuration, tenant_id)}" if self._client_credential_cache: logger.info("Using cached client credentials for MSAL authentication.") @@ -199,12 +241,14 @@ def _create_client_application(self) -> None: raise NotImplementedError( str(authentication_errors.AuthenticationTypeNotSupported) ) - - self._msal_auth_client = ConfidentialClientApplication( + + msal_auth_client = ConfidentialClientApplication( client_id=self._msal_configuration.CLIENT_ID, authority=authority, client_credential=self._client_credential_cache, ) + + self._msal_auth_client_map[client_rep] = msal_auth_client @staticmethod def _uri_validator(url_str: str) -> tuple[bool, Optional[URI]]: @@ -254,11 +298,12 @@ async def get_agentic_application_token( ) self._create_client_application() - if isinstance(self._msal_auth_client, ConfidentialClientApplication): + msal_auth_client = self._client() + if isinstance(msal_auth_client, ConfidentialClientApplication): # https://github.dev/AzureAD/microsoft-authentication-library-for-dotnet auth_result_payload = await _async_acquire_token_for_client( - self._msal_auth_client, + msal_auth_client, ["api://AzureAdTokenExchange/.default"], data={"fmi_path": agent_app_instance_id}, ) @@ -268,8 +313,9 @@ async def get_agentic_application_token( return None + # UPDATE TEST TODO -> change to API async def get_agentic_instance_token( - self, agent_app_instance_id: str + self, tenant_id: str, agent_app_instance_id: str ) -> tuple[str, str]: """Gets the agentic instance token for the given agent application instance ID. @@ -302,10 +348,8 @@ async def get_agentic_instance_token( agent_app_instance_id ) ) - - authority = ( - f"https://login.microsoftonline.com/{self._msal_configuration.TENANT_ID}" - ) + + authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id) instance_app = ConfidentialClientApplication( client_id=agent_app_instance_id, @@ -353,7 +397,7 @@ async def get_agentic_instance_token( return agentic_instance_token["access_token"], agent_token_result async def get_agentic_user_token( - self, agent_app_instance_id: str, agentic_user_id: str, scopes: list[str] + self, tenant_id: str, agent_app_instance_id: str, agentic_user_id: str, scopes: list[str] ) -> Optional[str]: """Gets the agentic user token for the given agent application instance ID and agentic user Id and the scopes. @@ -377,7 +421,7 @@ async def get_agentic_user_token( agentic_user_id, ) instance_token, agent_token = await self.get_agentic_instance_token( - agent_app_instance_id + tenant_id, agent_app_instance_id ) if not instance_token or not agent_token: @@ -391,10 +435,8 @@ async def get_agentic_user_token( agent_app_instance_id, agentic_user_id ) ) - - authority = ( - f"https://login.microsoftonline.com/{self._msal_configuration.TENANT_ID}" - ) + + authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id) instance_app = ConfidentialClientApplication( client_id=agent_app_instance_id, From fba8a53ccf4389a782befbd8986cae73ded40bfc Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 2 Feb 2026 18:59:14 -0800 Subject: [PATCH 02/15] Adding missing tenant_id arg and resolving broken state --- .../microsoft_agents/activity/activity.py | 8 ++++++++ .../app/oauth/_handlers/agentic_user_authorization.py | 4 ++-- .../core/authorization/access_token_provider_base.py | 6 +++--- .../core/authorization/anonymous_token_provider.py | 4 ++-- .../hosting/core/rest_channel_service_client_factory.py | 3 ++- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 5ea03b26..42083dbe 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -785,3 +785,11 @@ def get_agentic_user(self) -> Optional[str]: if not self.is_agentic_request() or not self.recipient: return None return self.recipient.agentic_user_id + + def get_agentic_tenant_id(self) -> Optional[str]: + """Gets the agentic tenant ID from the context if it's an agentic request.""" + if self.recipient and self.recipient.tenant_id: + return self.recipient.tenant_id + if self.conversation and self.conversation.tenant_id: + return self.conversation.tenant_id + return None \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py index eaabdf8c..0c0e3d31 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py @@ -75,7 +75,7 @@ async def get_agentic_instance_token(self, context: TurnContext) -> TokenRespons agentic_instance_id = context.activity.get_agentic_instance_id() assert agentic_instance_id instance_token, _ = await connection.get_agentic_instance_token( - agentic_instance_id + context.activity.get_agentic_tenant_id(), agentic_instance_id ) return ( TokenResponse(token=instance_token) if instance_token else TokenResponse() @@ -131,7 +131,7 @@ async def get_agentic_user_token( ) token = await connection.get_agentic_user_token( - agentic_instance_id, agentic_user_id, scopes + context.activity.get_agentic_tenant_id(), agentic_instance_id, agentic_user_id, scopes ) return TokenResponse(token=token) if token else TokenResponse() diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py index 4d0a593d..59d3c282 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py @@ -33,16 +33,16 @@ async def acquire_token_on_behalf_of( raise NotImplementedError() async def get_agentic_application_token( - self, agent_app_instance_id: str + self, tenant_id: str, agent_app_instance_id: str ) -> Optional[str]: raise NotImplementedError() async def get_agentic_instance_token( - self, agent_app_instance_id: str + self, tenant_id: str, agent_app_instance_id: str ) -> tuple[str, str]: raise NotImplementedError() async def get_agentic_user_token( - self, agent_app_instance_id: str, agentic_user_id: str, scopes: list[str] + self, tenant_id: str, agent_app_instance_id: str, agentic_user_id: str, scopes: list[str] ) -> Optional[str]: raise NotImplementedError() diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py index d262c586..567a0356 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py @@ -28,11 +28,11 @@ async def get_agentic_application_token( return "" async def get_agentic_instance_token( - self, agent_app_instance_id: str + self, tenant_id: str, agent_app_instance_id: str ) -> tuple[str, str]: return "", "" async def get_agentic_user_token( - self, agent_app_instance_id: str, agentic_user_id: str, scopes: list[str] + self, tenant_id: str, agent_app_instance_id: str, agentic_user_id: str, scopes: list[str] ) -> Optional[str]: return "" diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py index e24bb690..51ce22a7 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py @@ -67,12 +67,13 @@ async def _get_agentic_token(self, context: TurnContext, service_url: str) -> st raise ValueError("Agent instance ID is required for agentic identity role") if context.activity.recipient.role == RoleTypes.agentic_identity: - token, _ = await connection.get_agentic_instance_token(agent_instance_id) + token, _ = await connection.get_agentic_instance_token(context.activity.get_agentic_tenant_id(), agent_instance_id) else: agentic_user = context.activity.get_agentic_user() if not agentic_user: raise ValueError("Agentic user is required for agentic user role") token = await connection.get_agentic_user_token( + context.activity.get_agentic_tenant_id(), agent_instance_id, agentic_user, [AuthenticationConstants.APX_PRODUCTION_SCOPE], From d13ca270fa1a7bce22be71c950d25a1537886264 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 2 Feb 2026 19:17:56 -0800 Subject: [PATCH 03/15] Fixed tests --- tests/_common/data/default_test_values.py | 1 + .../testing_objects/mocks/mock_msal_auth.py | 3 ++- .../test_agentic_user_authorization.py | 19 ++++++++++++------- ...est_rest_channel_service_client_factory.py | 3 ++- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/_common/data/default_test_values.py b/tests/_common/data/default_test_values.py index 0b80feeb..98701ef3 100644 --- a/tests/_common/data/default_test_values.py +++ b/tests/_common/data/default_test_values.py @@ -12,6 +12,7 @@ def __init__(self): self.bot_url = "https://botframework.com" self.ms_app_id = "__ms_app_id" self.service_url = "https://service.url/" + self.tenant_id = "test-tenant-id" # Auth Handler Settings self.abs_oauth_connection_name = "connection_name" diff --git a/tests/_common/testing_objects/mocks/mock_msal_auth.py b/tests/_common/testing_objects/mocks/mock_msal_auth.py index c9e9eb09..4ee24c39 100644 --- a/tests/_common/testing_objects/mocks/mock_msal_auth.py +++ b/tests/_common/testing_objects/mocks/mock_msal_auth.py @@ -26,7 +26,8 @@ def __init__( self.mock_client = mock_client def _create_client_application(self) -> None: - self._msal_auth_client = self.mock_client + rep = self._client_rep() + self._msal_auth_client_map[rep] = self.mock_client def agentic_mock_class_MsalAuth( diff --git a/tests/hosting_core/app/_oauth/_handlers/test_agentic_user_authorization.py b/tests/hosting_core/app/_oauth/_handlers/test_agentic_user_authorization.py index 579623b0..32c8b93d 100644 --- a/tests/hosting_core/app/_oauth/_handlers/test_agentic_user_authorization.py +++ b/tests/hosting_core/app/_oauth/_handlers/test_agentic_user_authorization.py @@ -27,7 +27,12 @@ class TestUtils: def setup_method(self, mocker): - self.TurnContext = create_testing_TurnContext_magic + + def my_func(context): + context.activity.recipient.tenant_id = DEFAULTS.tenant_id + return context + + self.TurnContext = lambda *args, **kwargs: my_func(create_testing_TurnContext_magic(*args, **kwargs)) @pytest.fixture def storage(self): @@ -155,7 +160,7 @@ async def test_get_agentic_instance_token_is_agentic( token = await agentic_auth.get_agentic_instance_token(context) assert token == TokenResponse(token=DEFAULTS.token) mock_provider.get_agentic_instance_token.assert_called_once_with( - DEFAULTS.agentic_instance_id + DEFAULTS.tenant_id, DEFAULTS.agentic_instance_id ) @pytest.mark.asyncio @@ -187,7 +192,7 @@ async def test_get_agentic_user_token_is_agentic( token = await agentic_auth.get_agentic_user_token(context, ["user.Read"]) assert token == TokenResponse(token=DEFAULTS.token) mock_provider.get_agentic_user_token.assert_called_once_with( - DEFAULTS.agentic_instance_id, "some_id", ["user.Read"] + DEFAULTS.tenant_id, DEFAULTS.agentic_instance_id, "some_id", ["user.Read"] ) @pytest.mark.asyncio @@ -232,7 +237,7 @@ async def test_sign_in_success( assert res.tag == _FlowStateTag.COMPLETE mock_provider.get_agentic_user_token.assert_called_once_with( - DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list + DEFAULTS.tenant_id, DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list ) @pytest.mark.asyncio @@ -277,7 +282,7 @@ async def test_sign_in_failure( assert res.tag == _FlowStateTag.FAILURE mock_provider.get_agentic_user_token.assert_called_once_with( - DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list + DEFAULTS.tenant_id, DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list ) @pytest.mark.asyncio @@ -323,7 +328,7 @@ async def test_get_refreshed_token_success( assert res == TokenResponse(token="my_token") mock_provider.get_agentic_user_token.assert_called_once_with( - DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list + DEFAULTS.tenant_id, DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list ) @pytest.mark.asyncio @@ -368,5 +373,5 @@ async def test_get_refreshed_token_failure( ) assert res == TokenResponse() mock_provider.get_agentic_user_token.assert_called_once_with( - DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list + DEFAULTS.tenant_id, DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list ) diff --git a/tests/hosting_core/test_rest_channel_service_client_factory.py b/tests/hosting_core/test_rest_channel_service_client_factory.py index aa2bd59f..80539ff3 100644 --- a/tests/hosting_core/test_rest_channel_service_client_factory.py +++ b/tests/hosting_core/test_rest_channel_service_client_factory.py @@ -327,7 +327,7 @@ async def test_create_connector_client_agentic_identity( ) assert token_provider.get_agentic_instance_token.call_count == 1 token_provider.get_agentic_instance_token.assert_called_once_with( - "agentic_app_id" + None, "agentic_app_id" ) TeamsConnectorClient.__new__.assert_called_once_with( TeamsConnectorClient, endpoint=DEFAULTS.service_url, token=DEFAULTS.token @@ -391,6 +391,7 @@ async def test_create_connector_client_agentic_user( ) assert token_provider.get_agentic_user_token.call_count == 1 token_provider.get_agentic_user_token.assert_called_once_with( + None, "agentic_app_id", "agentic_user_id", [AuthenticationConstants.APX_PRODUCTION_SCOPE], From f23eb6e24af7c502ac982b574b5959b9243582dc Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 2 Feb 2026 22:42:14 -0800 Subject: [PATCH 04/15] Bug fixing --- .../authentication/msal/msal_auth.py | 63 ++++++++--------- .../msal/msal_connection_manager.py | 41 +++++++++--- .../aiohttp/jwt_authorization_middleware.py | 7 ++ .../authorization/anonymous_token_provider.py | 2 +- .../hosting/core/authorization/connections.py | 6 ++ .../core/authorization/jwt_token_validator.py | 67 +++++++++++++++++++ .../testing_objects/mocks/mock_msal_auth.py | 5 +- 7 files changed, 147 insertions(+), 44 deletions(-) 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 41a96f9b..32668eb6 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 @@ -14,6 +14,7 @@ ManagedIdentityClient, UserAssignedManagedIdentity, SystemAssignedManagedIdentity, + TokenCache, ) from requests import Session from cryptography.x509 import load_pem_x509_certificate @@ -55,18 +56,11 @@ def __init__(self, msal_configuration: AgentAuthConfiguration): self._msal_configuration = msal_configuration self._msal_auth_client_map: dict[str, ConfidentialClientApplication | ManagedIdentityClient] = {} + self._token_cache = TokenCache() logger.debug( f"Initializing MsalAuth with configuration: {self._msal_configuration}" ) - def _client_rep(self, tenant_id: str | None = None, instance_id: str | None = None) -> str: - return f"{tenant_id}-{instance_id}" - - def _client(self, tenant_id: str | None = None, instance_id: str | None = None) -> ConfidentialClientApplication | ManagedIdentityClient: - tenant_id = tenant_id or self._msal_configuration.TENANT_ID - rep = self._client_rep(tenant_id, instance_id) - return cast(ConfidentialClientApplication | ManagedIdentityClient, self._msal_auth_client_map.get(rep)) - async def get_access_token( self, resource_url: str, scopes: list[str], force_refresh: bool = False ) -> str: @@ -76,11 +70,12 @@ async def get_access_token( valid_uri, instance_uri = self._uri_validator(resource_url) if not valid_uri: raise ValueError(str(authentication_errors.InvalidInstanceUrl)) - + + assert instance_uri is not None local_scopes = self._resolve_scopes_list(instance_uri, scopes) - self._create_client_application() + msal_auth_client = self._get_client() - msal_auth_client = self._client() + # msal_auth_client = self._client() if isinstance(msal_auth_client, ManagedIdentityClient): logger.info("Acquiring token using Managed Identity Client.") auth_result_payload = await _async_acquire_token_for_client( @@ -115,8 +110,8 @@ async def acquire_token_on_behalf_of( :return: The access token as a string. """ - self._create_client_application() - msal_auth_client = self._client() + msal_auth_client = self._get_client() + # msal_auth_client = self._client() if isinstance(msal_auth_client, ManagedIdentityClient): logger.error( "Attempted on-behalf-of flow with Managed Identity authentication." @@ -175,18 +170,12 @@ def _resolve_tenant_id(config: AgentAuthConfiguration, tenant_id: str | None = N return config.TENANT_ID - def _create_client_application(self, tenant_id: str | None = None, instance_id: str | None = None) -> None: - - tenant_id = tenant_id or self._msal_configuration.TENANT_ID + def _create_client_application(self, tenant_id: str | None = None) -> ConfidentialClientApplication | ManagedIdentityClient: - if self._client(tenant_id, instance_id): - return + tenant_id = MsalAuth._resolve_tenant_id(self._msal_configuration, tenant_id) - msal_auth_client = None - client_rep = self._client_rep(tenant_id, instance_id) - if self._msal_configuration.AUTH_TYPE == AuthTypes.user_managed_identity: - msal_auth_client = ManagedIdentityClient( + return ManagedIdentityClient( UserAssignedManagedIdentity( client_id=self._msal_configuration.CLIENT_ID ), @@ -194,7 +183,7 @@ def _create_client_application(self, tenant_id: str | None = None, instance_id: ) elif self._msal_configuration.AUTH_TYPE == AuthTypes.system_managed_identity: - msal_auth_client = ManagedIdentityClient( + return ManagedIdentityClient( SystemAssignedManagedIdentity(), http_client=Session(), ) @@ -242,13 +231,24 @@ def _create_client_application(self, tenant_id: str | None = None, instance_id: str(authentication_errors.AuthenticationTypeNotSupported) ) - msal_auth_client = ConfidentialClientApplication( + return ConfidentialClientApplication( client_id=self._msal_configuration.CLIENT_ID, authority=authority, client_credential=self._client_credential_cache, ) - - self._msal_auth_client_map[client_rep] = msal_auth_client + + def _client_rep(self, tenant_id: str | None = None) -> str: + tenant_id = tenant_id or self._msal_configuration.TENANT_ID + return f"tenant:{tenant_id}" # might add more later + + def _get_client(self, tenant_id: str | None = None) -> ConfidentialClientApplication | ManagedIdentityClient: + rep = self._client_rep(tenant_id) + if rep in self._msal_auth_client_map: + return self._msal_auth_client_map[rep] + else: + client = self._create_client_application(tenant_id) + self._msal_auth_client_map[rep] = client + return client @staticmethod def _uri_validator(url_str: str) -> tuple[bool, Optional[URI]]: @@ -264,7 +264,8 @@ def _resolve_scopes_list(self, instance_url: URI, scopes=None) -> list[str]: return scopes temp_list: list[str] = [] - for scope in self._msal_configuration.SCOPES: + lst = self._msal_configuration.SCOPES or [] + for scope in lst: scope_placeholder = scope if "{instance}" in scope_placeholder.lower(): scope_placeholder = scope_placeholder.replace( @@ -277,7 +278,7 @@ def _resolve_scopes_list(self, instance_url: URI, scopes=None) -> list[str]: # the call to MSAL is blocking, but in the future we want to create an asyncio task # to avoid this async def get_agentic_application_token( - self, agent_app_instance_id: str + self, tenant_id: str, agent_app_instance_id: str ) -> Optional[str]: """Gets the agentic application token for the given agent application instance ID. @@ -296,9 +297,8 @@ async def get_agentic_application_token( "Attempting to get agentic application token from agent_app_instance_id %s", agent_app_instance_id, ) - self._create_client_application() + msal_auth_client = self._get_client(tenant_id) - msal_auth_client = self._client() if isinstance(msal_auth_client, ConfidentialClientApplication): # https://github.dev/AzureAD/microsoft-authentication-library-for-dotnet @@ -335,6 +335,7 @@ async def get_agentic_instance_token( agent_app_instance_id, ) agent_token_result = await self.get_agentic_application_token( + tenant_id, agent_app_instance_id ) @@ -355,6 +356,7 @@ async def get_agentic_instance_token( client_id=agent_app_instance_id, authority=authority, client_credential={"client_assertion": agent_token_result}, + token_cache=self._token_cache, ) agentic_instance_token = await _async_acquire_token_for_client( @@ -442,6 +444,7 @@ async def get_agentic_user_token( client_id=agent_app_instance_id, authority=authority, client_credential={"client_assertion": agent_token}, + token_cache=self._token_cache, ) logger.info( diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py index 819b3d73..5176cad8 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py @@ -3,6 +3,7 @@ import re from typing import Dict, List, Optional +from collections.abc import Iterator from microsoft_agents.hosting.core import ( AgentAuthConfiguration, AccessTokenProviderBase, @@ -36,16 +37,17 @@ def __init__( self._connections: Dict[str, MsalAuth] = {} self._connections_map = connections_map or kwargs.get("CONNECTIONSMAP", {}) - self._service_connection_configuration: AgentAuthConfiguration = None + self._config_map: dict[str, AgentAuthConfiguration] = {} if connections_configurations: for ( connection_name, - connection_settings, + agent_auth_config, ) in connections_configurations.items(): self._connections[connection_name] = MsalAuth( - AgentAuthConfiguration(**connection_settings) + agent_auth_config ) + self._config_map[connection_name] = agent_auth_config else: raw_configurations: Dict[str, Dict] = kwargs.get("CONNECTIONS", {}) for connection_name, connection_settings in raw_configurations.items(): @@ -53,11 +55,13 @@ def __init__( **connection_settings.get("SETTINGS", {}) ) self._connections[connection_name] = MsalAuth(parsed_configuration) - if connection_name == "SERVICE_CONNECTION": - self._service_connection_configuration = parsed_configuration + self._config_map[connection_name] = parsed_configuration if not self._connections.get("SERVICE_CONNECTION", None): raise ValueError("No service connection configuration provided.") + + def __iter__(self) -> Iterator[AgentAuthConfiguration]: + return iter(self._config_map.values()) def get_connection(self, connection_name: Optional[str]) -> AccessTokenProviderBase: """ @@ -68,8 +72,11 @@ def get_connection(self, connection_name: Optional[str]) -> AccessTokenProviderB :return: The OAuth connection for the agent. :rtype: :class:`microsoft_agents.hosting.core.AccessTokenProviderBase` """ - # should never be None - return self._connections.get(connection_name, None) + connection_name = connection_name or "SERVICE_CONNECTION" + connection = self._connections.get(connection_name, None) + if not connection: + raise ValueError(f"No connection found for '{connection_name}'.") + return connection def get_default_connection(self) -> AccessTokenProviderBase: """ @@ -78,8 +85,10 @@ def get_default_connection(self) -> AccessTokenProviderBase: :return: The default OAuth connection for the agent. :rtype: :class:`microsoft_agents.hosting.core.AccessTokenProviderBase` """ - # should never be None - return self._connections.get("SERVICE_CONNECTION", None) + connection = self._connections.get("SERVICE_CONNECTION", None) + if not connection: + raise ValueError("No default service connection found. Expected 'SERVICE_CONNECTION'.") + return connection def get_token_provider( self, claims_identity: ClaimsIdentity, service_url: str @@ -137,4 +146,16 @@ def get_default_connection_configuration(self) -> AgentAuthConfiguration: :return: The default connection configuration for the agent. :rtype: :class:`microsoft_agents.hosting.core.AgentAuthConfiguration` """ - return self._service_connection_configuration + config = self._config_map.get("SERVICE_CONNECTION") + if not config: + raise ValueError("No default service connection configuration found. Expected 'SERVICE_CONNECTION'.") + return config + + def _get_all_configurations(self) -> List[AgentAuthConfiguration]: + """ + Get all connection configurations for the agent. + + :return: A list of all connection configurations for the agent. + :rtype: List[:class:`microsoft_agents.hosting.core.AgentAuthConfiguration`] + """ + return list(self._config_map.values()) \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/jwt_authorization_middleware.py b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/jwt_authorization_middleware.py index f09604ba..d32c7649 100644 --- a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/jwt_authorization_middleware.py +++ b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/jwt_authorization_middleware.py @@ -10,8 +10,15 @@ @middleware async def jwt_authorization_middleware(request: Request, handler): + # if "agent_configuration" in request.app: auth_config: AgentAuthConfiguration = request.app["agent_configuration"] token_validator = JwtTokenValidator(auth_config) + # elif "connections" in request.app: + # connections = request.app["connections"] + # service_connection_config = connections._get_default_connection_configuration() + # token_validator = JwtTokenValidator(service_connection_config) + + auth_header = request.headers.get("Authorization") if auth_header: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py index 567a0356..9bf6c61c 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py @@ -23,7 +23,7 @@ async def acquire_token_on_behalf_of( return "" async def get_agentic_application_token( - self, agent_app_instance_id: str + self, tenant_id: str, agent_app_instance_id: str ) -> Optional[str]: return "" diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py index e11103e2..1a853483 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py @@ -40,3 +40,9 @@ def get_default_connection_configuration(self) -> AgentAuthConfiguration: Get the default connection configuration for the agent. """ raise NotImplementedError() + + def _get_all_configurations(self) -> list[AgentAuthConfiguration]: + """ + Get all connection configurations for the agent. + """ + raise NotImplementedError() \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py index b1fa00de..1cf5281b 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py @@ -54,3 +54,70 @@ async def _get_public_key_or_secret(self, token: str) -> PyJWK: key = await asyncio.to_thread(jwks_client.get_signing_key, header["kid"]) return key + + +# # Copyright (c) Microsoft Corporation. All rights reserved. +# # Licensed under the MIT License. + +# import asyncio +# import logging +# import jwt + +# from jwt import PyJWKClient, PyJWK, decode, get_unverified_header + +# from .agent_auth_configuration import AgentAuthConfiguration +# from .claims_identity import ClaimsIdentity + +# logger = logging.getLogger(__name__) + + +# class JwtTokenValidator: + +# def __init__(self, configuration: AgentAuthConfiguration | list[AgentAuthConfiguration]): +# if isinstance(configuration, AgentAuthConfiguration): +# configuration = [configuration] +# self.configuration = configuration + +# def _validate_aud(self, aud: str) -> bool: +# for config in self.configuration: +# if aud == config.CLIENT_ID: +# return True +# return False + +# async def validate_token(self, token: str) -> ClaimsIdentity: + +# logger.debug("Validating JWT token.") +# key = await self._get_public_key_or_secret(token) +# decoded_token = jwt.decode( +# token, +# key=key, +# algorithms=["RS256"], +# leeway=300.0, +# options={"verify_aud": False}, +# ) +# if not self._validate_aud(decoded_token["aud"]): +# logger.error(f"Invalid audience: {decoded_token['aud']}", stack_info=True) +# raise ValueError("Invalid audience.") + +# # This probably should return a ClaimsIdentity +# logger.debug("JWT token validated successfully.") +# return ClaimsIdentity(decoded_token, True, security_token=token) + +# def get_anonymous_claims(self) -> ClaimsIdentity: +# logger.debug("Returning anonymous claims identity.") +# return ClaimsIdentity({}, False, authentication_type="Anonymous") + +# async def _get_public_key_or_secret(self, token: str) -> PyJWK: +# header = get_unverified_header(token) +# unverified_payload: dict = decode(token, options={"verify_signature": False}) + +# jwksUri = ( +# "https://login.botframework.com/v1/.well-known/keys" +# if unverified_payload.get("iss") == "https://api.botframework.com" +# else f"https://login.microsoftonline.com/{self.configuration.TENANT_ID}/discovery/v2.0/keys" +# ) +# jwks_client = PyJWKClient(jwksUri) + +# key = await asyncio.to_thread(jwks_client.get_signing_key, header["kid"]) + +# return key diff --git a/tests/_common/testing_objects/mocks/mock_msal_auth.py b/tests/_common/testing_objects/mocks/mock_msal_auth.py index 4ee24c39..53091fa9 100644 --- a/tests/_common/testing_objects/mocks/mock_msal_auth.py +++ b/tests/_common/testing_objects/mocks/mock_msal_auth.py @@ -25,9 +25,8 @@ def __init__( ) self.mock_client = mock_client - def _create_client_application(self) -> None: - rep = self._client_rep() - self._msal_auth_client_map[rep] = self.mock_client + def _get_client(self, tenant_id: str | None = None) -> None: + return self.mock_client def agentic_mock_class_MsalAuth( From cbe62242e84fef59f20bdbfc6a32793370a247cf Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 2 Feb 2026 22:53:43 -0800 Subject: [PATCH 05/15] Going back to cached clients --- .../authentication/msal/msal_auth.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) 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 32668eb6..af7b9577 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 @@ -191,9 +191,6 @@ def _create_client_application(self, tenant_id: str | None = None) -> Confidenti if self._msal_configuration.AUTHORITY: authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id) else: - # TODO -> there is no withTenantId, and I'm guessing it's equivalent to this - # circle back and check - # maybe I'm overthinking entirely... authority = f"https://login.microsoftonline.com/{MsalAuth._resolve_tenant_id(self._msal_configuration, tenant_id)}" if self._client_credential_cache: @@ -237,12 +234,12 @@ def _create_client_application(self, tenant_id: str | None = None) -> Confidenti client_credential=self._client_credential_cache, ) - def _client_rep(self, tenant_id: str | None = None) -> str: + def _client_rep(self, tenant_id: str | None = None, instance_id: str | None = None) -> str: tenant_id = tenant_id or self._msal_configuration.TENANT_ID - return f"tenant:{tenant_id}" # might add more later + return f"tenant:{tenant_id}.instance:{instance_id}" # might add more later - def _get_client(self, tenant_id: str | None = None) -> ConfidentialClientApplication | ManagedIdentityClient: - rep = self._client_rep(tenant_id) + def _get_client(self, tenant_id: str | None = None, instance_id: str | None = None) -> ConfidentialClientApplication | ManagedIdentityClient: + rep = self._client_rep(tenant_id, instance_id) if rep in self._msal_auth_client_map: return self._msal_auth_client_map[rep] else: @@ -297,7 +294,7 @@ async def get_agentic_application_token( "Attempting to get agentic application token from agent_app_instance_id %s", agent_app_instance_id, ) - msal_auth_client = self._get_client(tenant_id) + msal_auth_client = self._get_client(tenant_id, agent_app_instance_id) if isinstance(msal_auth_client, ConfidentialClientApplication): @@ -313,7 +310,6 @@ async def get_agentic_application_token( return None - # UPDATE TEST TODO -> change to API async def get_agentic_instance_token( self, tenant_id: str, agent_app_instance_id: str ) -> tuple[str, str]: From c0be886b4efcbc3348d3862bb5dc5e3e14398e3e Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 2 Feb 2026 23:02:04 -0800 Subject: [PATCH 06/15] Undid jwt changes --- .../msal/msal_connection_manager.py | 14 +--- .../aiohttp/jwt_authorization_middleware.py | 7 -- .../hosting/core/authorization/connections.py | 6 -- .../core/authorization/jwt_token_validator.py | 69 +------------------ 4 files changed, 2 insertions(+), 94 deletions(-) diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py index 5176cad8..6c11cde4 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py @@ -59,9 +59,6 @@ def __init__( if not self._connections.get("SERVICE_CONNECTION", None): raise ValueError("No service connection configuration provided.") - - def __iter__(self) -> Iterator[AgentAuthConfiguration]: - return iter(self._config_map.values()) def get_connection(self, connection_name: Optional[str]) -> AccessTokenProviderBase: """ @@ -149,13 +146,4 @@ def get_default_connection_configuration(self) -> AgentAuthConfiguration: config = self._config_map.get("SERVICE_CONNECTION") if not config: raise ValueError("No default service connection configuration found. Expected 'SERVICE_CONNECTION'.") - return config - - def _get_all_configurations(self) -> List[AgentAuthConfiguration]: - """ - Get all connection configurations for the agent. - - :return: A list of all connection configurations for the agent. - :rtype: List[:class:`microsoft_agents.hosting.core.AgentAuthConfiguration`] - """ - return list(self._config_map.values()) \ No newline at end of file + return config \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/jwt_authorization_middleware.py b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/jwt_authorization_middleware.py index d32c7649..f09604ba 100644 --- a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/jwt_authorization_middleware.py +++ b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/jwt_authorization_middleware.py @@ -10,15 +10,8 @@ @middleware async def jwt_authorization_middleware(request: Request, handler): - # if "agent_configuration" in request.app: auth_config: AgentAuthConfiguration = request.app["agent_configuration"] token_validator = JwtTokenValidator(auth_config) - # elif "connections" in request.app: - # connections = request.app["connections"] - # service_connection_config = connections._get_default_connection_configuration() - # token_validator = JwtTokenValidator(service_connection_config) - - auth_header = request.headers.get("Authorization") if auth_header: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py index 1a853483..9bcc5a71 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py @@ -39,10 +39,4 @@ def get_default_connection_configuration(self) -> AgentAuthConfiguration: """ Get the default connection configuration for the agent. """ - raise NotImplementedError() - - def _get_all_configurations(self) -> list[AgentAuthConfiguration]: - """ - Get all connection configurations for the agent. - """ raise NotImplementedError() \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py index 1cf5281b..a9b47a34 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py @@ -53,71 +53,4 @@ async def _get_public_key_or_secret(self, token: str) -> PyJWK: key = await asyncio.to_thread(jwks_client.get_signing_key, header["kid"]) - return key - - -# # Copyright (c) Microsoft Corporation. All rights reserved. -# # Licensed under the MIT License. - -# import asyncio -# import logging -# import jwt - -# from jwt import PyJWKClient, PyJWK, decode, get_unverified_header - -# from .agent_auth_configuration import AgentAuthConfiguration -# from .claims_identity import ClaimsIdentity - -# logger = logging.getLogger(__name__) - - -# class JwtTokenValidator: - -# def __init__(self, configuration: AgentAuthConfiguration | list[AgentAuthConfiguration]): -# if isinstance(configuration, AgentAuthConfiguration): -# configuration = [configuration] -# self.configuration = configuration - -# def _validate_aud(self, aud: str) -> bool: -# for config in self.configuration: -# if aud == config.CLIENT_ID: -# return True -# return False - -# async def validate_token(self, token: str) -> ClaimsIdentity: - -# logger.debug("Validating JWT token.") -# key = await self._get_public_key_or_secret(token) -# decoded_token = jwt.decode( -# token, -# key=key, -# algorithms=["RS256"], -# leeway=300.0, -# options={"verify_aud": False}, -# ) -# if not self._validate_aud(decoded_token["aud"]): -# logger.error(f"Invalid audience: {decoded_token['aud']}", stack_info=True) -# raise ValueError("Invalid audience.") - -# # This probably should return a ClaimsIdentity -# logger.debug("JWT token validated successfully.") -# return ClaimsIdentity(decoded_token, True, security_token=token) - -# def get_anonymous_claims(self) -> ClaimsIdentity: -# logger.debug("Returning anonymous claims identity.") -# return ClaimsIdentity({}, False, authentication_type="Anonymous") - -# async def _get_public_key_or_secret(self, token: str) -> PyJWK: -# header = get_unverified_header(token) -# unverified_payload: dict = decode(token, options={"verify_signature": False}) - -# jwksUri = ( -# "https://login.botframework.com/v1/.well-known/keys" -# if unverified_payload.get("iss") == "https://api.botframework.com" -# else f"https://login.microsoftonline.com/{self.configuration.TENANT_ID}/discovery/v2.0/keys" -# ) -# jwks_client = PyJWKClient(jwksUri) - -# key = await asyncio.to_thread(jwks_client.get_signing_key, header["kid"]) - -# return key + return key \ No newline at end of file From 56940d531db9f8f4d7a0ca62c15b1f1c694a415d Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 3 Feb 2026 08:36:30 -0800 Subject: [PATCH 07/15] Adding patch to AgentAuthConfig to support checking against several client IDs in JWTTokenValidator --- .../msal/msal_connection_manager.py | 4 ++++ .../authorization/agent_auth_configuration.py | 19 +++++++++++++++++++ .../core/authorization/jwt_token_validator.py | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py index 6c11cde4..62ec1b4c 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py @@ -57,6 +57,10 @@ def __init__( self._connections[connection_name] = MsalAuth(parsed_configuration) self._config_map[connection_name] = parsed_configuration + # JWT-patch + for connection_name, config in self._config_map.items(): + config._connections = self._config_map + if not self._connections.get("SERVICE_CONNECTION", None): raise ValueError("No service connection configuration provided.") 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 e10bf0ce..38d95dea 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 @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from __future__ import annotations from typing import Optional from microsoft_agents.hosting.core.authorization.auth_types import AuthTypes @@ -34,6 +35,10 @@ class AgentAuthConfiguration: ALT_BLUEPRINT_ID: Optional[str] ANONYMOUS_ALLOWED: bool = False + # JWT-patch: for internal use only + # patch needed to support multiple connections + _connections: dict[str, AgentAuthConfiguration] + def __init__( self, auth_type: AuthTypes = None, @@ -63,6 +68,9 @@ def __init__( "ANONYMOUS_ALLOWED", False ) + # JWT-patch: always at least include self for backward compat + self._connections = {str(self.CONNECTION_NAME): self} + @property def ISSUERS(self) -> list[str]: """ @@ -73,3 +81,14 @@ def ISSUERS(self) -> list[str]: f"https://sts.windows.net/{self.TENANT_ID}/", f"https://login.microsoftonline.com/{self.TENANT_ID}/v2.0", ] + + def _jwt_patch_is_valid_aud(self, aud: str) -> bool: + """ + JWT-patch: Checks if the given audience is valid for any of the connections. + """ + for conn in self._connections.values(): + if not conn.CLIENT_ID: + continue + if aud.lower() == conn.CLIENT_ID.lower(): + return True + return False \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py index a9b47a34..1a7ddd17 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py @@ -28,7 +28,7 @@ async def validate_token(self, token: str) -> ClaimsIdentity: leeway=300.0, options={"verify_aud": False}, ) - if decoded_token["aud"] != self.configuration.CLIENT_ID: + if not self.configuration._jwt_patch_is_valid_aud(decoded_token["aud"]): logger.error(f"Invalid audience: {decoded_token['aud']}", stack_info=True) raise ValueError("Invalid audience.") From 9543722363ca73298c97274c5a14e4070372881a Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 3 Feb 2026 08:37:32 -0800 Subject: [PATCH 08/15] Reformatting --- .../microsoft_agents/activity/activity.py | 2 +- .../authentication/msal/msal_auth.py | 70 +++++++++------ .../msal/msal_connection_manager.py | 14 +-- .../hosting/core/app/agent_application.py | 36 +++++--- .../_handlers/agentic_user_authorization.py | 5 +- .../access_token_provider_base.py | 6 +- .../authorization/agent_auth_configuration.py | 2 +- .../authorization/anonymous_token_provider.py | 6 +- .../hosting/core/authorization/connections.py | 2 +- .../core/authorization/jwt_token_validator.py | 2 +- .../rest_channel_service_client_factory.py | 4 +- .../testing_objects/mocks/mock_msal_auth.py | 2 +- .../test_agentic_user_authorization.py | 24 ++++-- .../test_connector_user_authorization.py | 86 ++++++++++--------- .../_handlers/test_user_authorization.py | 36 ++++++-- .../app/_oauth/test_authorization.py | 8 +- 16 files changed, 200 insertions(+), 105 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 42083dbe..38ca854c 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -792,4 +792,4 @@ def get_agentic_tenant_id(self) -> Optional[str]: return self.recipient.tenant_id if self.conversation and self.conversation.tenant_id: return self.conversation.tenant_id - return None \ No newline at end of file + return None 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 af7b9577..2902d020 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 @@ -55,7 +55,9 @@ def __init__(self, msal_configuration: AgentAuthConfiguration): """ self._msal_configuration = msal_configuration - self._msal_auth_client_map: dict[str, ConfidentialClientApplication | ManagedIdentityClient] = {} + self._msal_auth_client_map: dict[ + str, ConfidentialClientApplication | ManagedIdentityClient + ] = {} self._token_cache = TokenCache() logger.debug( f"Initializing MsalAuth with configuration: {self._msal_configuration}" @@ -70,7 +72,7 @@ async def get_access_token( valid_uri, instance_uri = self._uri_validator(resource_url) if not valid_uri: raise ValueError(str(authentication_errors.InvalidInstanceUrl)) - + assert instance_uri is not None local_scopes = self._resolve_scopes_list(instance_uri, scopes) msal_auth_client = self._get_client() @@ -148,32 +150,41 @@ async def acquire_token_on_behalf_of( msal_auth_client.__class__.__name__ ) ) - + @staticmethod - def _resolve_authority(config: AgentAuthConfiguration, tenant_id: str | None = None) -> str: + def _resolve_authority( + config: AgentAuthConfiguration, tenant_id: str | None = None + ) -> str: if not tenant_id: - return config.AUTHORITY or f"https://login.microsoftonline.com/{config.TENANT_ID}" - + return ( + config.AUTHORITY + or f"https://login.microsoftonline.com/{config.TENANT_ID}" + ) + if config.AUTHORITY: return re.sub(r"/common(?=/|$)", f"/{tenant_id}", config.AUTHORITY) - + return f"https://login.microsoftonline.com/{tenant_id}" @staticmethod - def _resolve_tenant_id(config: AgentAuthConfiguration, tenant_id: str | None = None) -> str | None: + def _resolve_tenant_id( + config: AgentAuthConfiguration, tenant_id: str | None = None + ) -> str | None: if not config.TENANT_ID: return None - + if tenant_id and config.TENANT_ID.lower() == "common": return tenant_id - + return config.TENANT_ID - def _create_client_application(self, tenant_id: str | None = None) -> ConfidentialClientApplication | ManagedIdentityClient: + def _create_client_application( + self, tenant_id: str | None = None + ) -> ConfidentialClientApplication | ManagedIdentityClient: tenant_id = MsalAuth._resolve_tenant_id(self._msal_configuration, tenant_id) - + if self._msal_configuration.AUTH_TYPE == AuthTypes.user_managed_identity: return ManagedIdentityClient( UserAssignedManagedIdentity( @@ -189,7 +200,9 @@ def _create_client_application(self, tenant_id: str | None = None) -> Confidenti ) else: if self._msal_configuration.AUTHORITY: - authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id) + authority = MsalAuth._resolve_authority( + self._msal_configuration, tenant_id + ) else: authority = f"https://login.microsoftonline.com/{MsalAuth._resolve_tenant_id(self._msal_configuration, tenant_id)}" @@ -227,18 +240,22 @@ def _create_client_application(self, tenant_id: str | None = None) -> Confidenti raise NotImplementedError( str(authentication_errors.AuthenticationTypeNotSupported) ) - + return ConfidentialClientApplication( client_id=self._msal_configuration.CLIENT_ID, authority=authority, client_credential=self._client_credential_cache, ) - - def _client_rep(self, tenant_id: str | None = None, instance_id: str | None = None) -> str: + + def _client_rep( + self, tenant_id: str | None = None, instance_id: str | None = None + ) -> str: tenant_id = tenant_id or self._msal_configuration.TENANT_ID - return f"tenant:{tenant_id}.instance:{instance_id}" # might add more later - - def _get_client(self, tenant_id: str | None = None, instance_id: str | None = None) -> ConfidentialClientApplication | ManagedIdentityClient: + return f"tenant:{tenant_id}.instance:{instance_id}" # might add more later + + def _get_client( + self, tenant_id: str | None = None, instance_id: str | None = None + ) -> ConfidentialClientApplication | ManagedIdentityClient: rep = self._client_rep(tenant_id, instance_id) if rep in self._msal_auth_client_map: return self._msal_auth_client_map[rep] @@ -331,8 +348,7 @@ async def get_agentic_instance_token( agent_app_instance_id, ) agent_token_result = await self.get_agentic_application_token( - tenant_id, - agent_app_instance_id + tenant_id, agent_app_instance_id ) if not agent_token_result: @@ -345,7 +361,7 @@ async def get_agentic_instance_token( agent_app_instance_id ) ) - + authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id) instance_app = ConfidentialClientApplication( @@ -395,7 +411,11 @@ async def get_agentic_instance_token( return agentic_instance_token["access_token"], agent_token_result async def get_agentic_user_token( - self, tenant_id: str, agent_app_instance_id: str, agentic_user_id: str, scopes: list[str] + self, + tenant_id: str, + agent_app_instance_id: str, + agentic_user_id: str, + scopes: list[str], ) -> Optional[str]: """Gets the agentic user token for the given agent application instance ID and agentic user Id and the scopes. @@ -419,7 +439,7 @@ async def get_agentic_user_token( agentic_user_id, ) instance_token, agent_token = await self.get_agentic_instance_token( - tenant_id, agent_app_instance_id + tenant_id, agent_app_instance_id ) if not instance_token or not agent_token: @@ -433,7 +453,7 @@ async def get_agentic_user_token( agent_app_instance_id, agentic_user_id ) ) - + authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id) instance_app = ConfidentialClientApplication( diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py index 62ec1b4c..fb520139 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py @@ -44,9 +44,7 @@ def __init__( connection_name, agent_auth_config, ) in connections_configurations.items(): - self._connections[connection_name] = MsalAuth( - agent_auth_config - ) + self._connections[connection_name] = MsalAuth(agent_auth_config) self._config_map[connection_name] = agent_auth_config else: raw_configurations: Dict[str, Dict] = kwargs.get("CONNECTIONS", {}) @@ -88,7 +86,9 @@ def get_default_connection(self) -> AccessTokenProviderBase: """ connection = self._connections.get("SERVICE_CONNECTION", None) if not connection: - raise ValueError("No default service connection found. Expected 'SERVICE_CONNECTION'.") + raise ValueError( + "No default service connection found. Expected 'SERVICE_CONNECTION'." + ) return connection def get_token_provider( @@ -149,5 +149,7 @@ def get_default_connection_configuration(self) -> AgentAuthConfiguration: """ config = self._config_map.get("SERVICE_CONNECTION") if not config: - raise ValueError("No default service connection configuration found. Expected 'SERVICE_CONNECTION'.") - return config \ No newline at end of file + raise ValueError( + "No default service connection configuration found. Expected 'SERVICE_CONNECTION'." + ) + return config diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index d0eb6c1e..60875a0e 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -120,9 +120,11 @@ def __init__( "ApplicationOptions.storage is required and was not configured.", stack_info=True, ) - raise ApplicationError(""" + raise ApplicationError( + """ The `ApplicationOptions.storage` property is required and was not configured. - """) + """ + ) if options.long_running_messages and ( not options.adapter or not options.bot_app_id @@ -131,10 +133,12 @@ def __init__( "ApplicationOptions.long_running_messages requires an adapter and bot_app_id.", stack_info=True, ) - raise ApplicationError(""" + raise ApplicationError( + """ The `ApplicationOptions.long_running_messages` property is unavailable because no adapter or `bot_app_id` was configured. - """) + """ + ) if options.adapter: self._adapter = options.adapter @@ -176,10 +180,12 @@ def adapter(self) -> ChannelServiceAdapter: "AgentApplication.adapter(): self._adapter is not configured.", stack_info=True, ) - raise ApplicationError(""" + raise ApplicationError( + """ The AgentApplication.adapter property is unavailable because it was not configured when creating the AgentApplication. - """) + """ + ) return self._adapter @@ -197,10 +203,12 @@ def auth(self) -> Authorization: "AgentApplication.auth(): self._auth is not configured.", stack_info=True, ) - raise ApplicationError(""" + raise ApplicationError( + """ The `AgentApplication.auth` property is unavailable because no Auth options were configured. - """) + """ + ) return self._auth @@ -584,10 +592,12 @@ async def sign_in_success(context: TurnContext, state: TurnState, connection_id: f"Failed to register sign-in success handler for route handler {func.__name__}", stack_info=True, ) - raise ApplicationError(""" + raise ApplicationError( + """ The `AgentApplication.on_sign_in_success` method is unavailable because no Auth options were configured. - """) + """ + ) return func def on_sign_in_failure( @@ -618,10 +628,12 @@ async def sign_in_failure(context: TurnContext, state: TurnState, connection_id: f"Failed to register sign-in failure handler for route handler {func.__name__}", stack_info=True, ) - raise ApplicationError(""" + raise ApplicationError( + """ The `AgentApplication.on_sign_in_failure` method is unavailable because no Auth options were configured. - """) + """ + ) return func def error( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py index 0c0e3d31..85fdc9a4 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py @@ -131,7 +131,10 @@ async def get_agentic_user_token( ) token = await connection.get_agentic_user_token( - context.activity.get_agentic_tenant_id(), agentic_instance_id, agentic_user_id, scopes + context.activity.get_agentic_tenant_id(), + agentic_instance_id, + agentic_user_id, + scopes, ) return TokenResponse(token=token) if token else TokenResponse() diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py index 59d3c282..26c748a1 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py @@ -43,6 +43,10 @@ async def get_agentic_instance_token( raise NotImplementedError() async def get_agentic_user_token( - self, tenant_id: str, agent_app_instance_id: str, agentic_user_id: str, scopes: list[str] + self, + tenant_id: str, + agent_app_instance_id: str, + agentic_user_id: str, + scopes: list[str], ) -> Optional[str]: raise NotImplementedError() 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 38d95dea..e24878ff 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 @@ -91,4 +91,4 @@ def _jwt_patch_is_valid_aud(self, aud: str) -> bool: continue if aud.lower() == conn.CLIENT_ID.lower(): return True - return False \ No newline at end of file + return False diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py index 9bf6c61c..722b3945 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py @@ -33,6 +33,10 @@ async def get_agentic_instance_token( return "", "" async def get_agentic_user_token( - self, tenant_id: str, agent_app_instance_id: str, agentic_user_id: str, scopes: list[str] + self, + tenant_id: str, + agent_app_instance_id: str, + agentic_user_id: str, + scopes: list[str], ) -> Optional[str]: return "" diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py index 9bcc5a71..e11103e2 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py @@ -39,4 +39,4 @@ def get_default_connection_configuration(self) -> AgentAuthConfiguration: """ Get the default connection configuration for the agent. """ - raise NotImplementedError() \ No newline at end of file + raise NotImplementedError() diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py index 1a7ddd17..e64e0987 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py @@ -53,4 +53,4 @@ async def _get_public_key_or_secret(self, token: str) -> PyJWK: key = await asyncio.to_thread(jwks_client.get_signing_key, header["kid"]) - return key \ No newline at end of file + return key diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py index 51ce22a7..4855b3ea 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py @@ -67,7 +67,9 @@ async def _get_agentic_token(self, context: TurnContext, service_url: str) -> st raise ValueError("Agent instance ID is required for agentic identity role") if context.activity.recipient.role == RoleTypes.agentic_identity: - token, _ = await connection.get_agentic_instance_token(context.activity.get_agentic_tenant_id(), agent_instance_id) + token, _ = await connection.get_agentic_instance_token( + context.activity.get_agentic_tenant_id(), agent_instance_id + ) else: agentic_user = context.activity.get_agentic_user() if not agentic_user: diff --git a/tests/_common/testing_objects/mocks/mock_msal_auth.py b/tests/_common/testing_objects/mocks/mock_msal_auth.py index 53091fa9..d8bf363e 100644 --- a/tests/_common/testing_objects/mocks/mock_msal_auth.py +++ b/tests/_common/testing_objects/mocks/mock_msal_auth.py @@ -25,7 +25,7 @@ def __init__( ) self.mock_client = mock_client - def _get_client(self, tenant_id: str | None = None) -> None: + def _get_client(self, tenant_id: str | None = None) -> None: return self.mock_client diff --git a/tests/hosting_core/app/_oauth/_handlers/test_agentic_user_authorization.py b/tests/hosting_core/app/_oauth/_handlers/test_agentic_user_authorization.py index 32c8b93d..dec38b52 100644 --- a/tests/hosting_core/app/_oauth/_handlers/test_agentic_user_authorization.py +++ b/tests/hosting_core/app/_oauth/_handlers/test_agentic_user_authorization.py @@ -32,7 +32,9 @@ def my_func(context): context.activity.recipient.tenant_id = DEFAULTS.tenant_id return context - self.TurnContext = lambda *args, **kwargs: my_func(create_testing_TurnContext_magic(*args, **kwargs)) + self.TurnContext = lambda *args, **kwargs: my_func( + create_testing_TurnContext_magic(*args, **kwargs) + ) @pytest.fixture def storage(self): @@ -237,7 +239,10 @@ async def test_sign_in_success( assert res.tag == _FlowStateTag.COMPLETE mock_provider.get_agentic_user_token.assert_called_once_with( - DEFAULTS.tenant_id, DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list + DEFAULTS.tenant_id, + DEFAULTS.agentic_instance_id, + "some_id", + expected_scopes_list, ) @pytest.mark.asyncio @@ -282,7 +287,10 @@ async def test_sign_in_failure( assert res.tag == _FlowStateTag.FAILURE mock_provider.get_agentic_user_token.assert_called_once_with( - DEFAULTS.tenant_id, DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list + DEFAULTS.tenant_id, + DEFAULTS.agentic_instance_id, + "some_id", + expected_scopes_list, ) @pytest.mark.asyncio @@ -328,7 +336,10 @@ async def test_get_refreshed_token_success( assert res == TokenResponse(token="my_token") mock_provider.get_agentic_user_token.assert_called_once_with( - DEFAULTS.tenant_id, DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list + DEFAULTS.tenant_id, + DEFAULTS.agentic_instance_id, + "some_id", + expected_scopes_list, ) @pytest.mark.asyncio @@ -373,5 +384,8 @@ async def test_get_refreshed_token_failure( ) assert res == TokenResponse() mock_provider.get_agentic_user_token.assert_called_once_with( - DEFAULTS.tenant_id, DEFAULTS.agentic_instance_id, "some_id", expected_scopes_list + DEFAULTS.tenant_id, + DEFAULTS.agentic_instance_id, + "some_id", + expected_scopes_list, ) diff --git a/tests/hosting_core/app/_oauth/_handlers/test_connector_user_authorization.py b/tests/hosting_core/app/_oauth/_handlers/test_connector_user_authorization.py index 05b79497..48c75371 100644 --- a/tests/hosting_core/app/_oauth/_handlers/test_connector_user_authorization.py +++ b/tests/hosting_core/app/_oauth/_handlers/test_connector_user_authorization.py @@ -37,8 +37,10 @@ def make_jwt( if app_id: payload["appid"] = app_id if include_exp: - payload["exp"] = (datetime.now(timezone.utc) + timedelta(seconds=exp_delta_seconds)).timestamp() - + payload["exp"] = ( + datetime.now(timezone.utc) + timedelta(seconds=exp_delta_seconds) + ).timestamp() + return jwt.encode(payload, token, algorithm="HS256") @@ -59,13 +61,13 @@ def create_testing_TurnContext( turn_context.activity.from_property.id = user_id turn_context.activity.type = ActivityTypes.message turn_context.adapter.AGENT_IDENTITY_KEY = "__agent_identity_key" - + # Create identity with security token identity = mocker.Mock() if security_token is not None: identity.security_token = security_token turn_context.identity = identity - + agent_identity = mocker.Mock() agent_identity.claims = {"aud": DEFAULTS.ms_app_id} turn_context.turn_state = { @@ -106,7 +108,9 @@ def auth_handler_settings(self): ]["SETTINGS"] @pytest.fixture - def connector_authorization(self, connection_manager, storage, auth_handler_settings): + def connector_authorization( + self, connection_manager, storage, auth_handler_settings + ): return ConnectorUserAuthorization( storage, connection_manager, @@ -137,7 +141,6 @@ def connection_set(self, request): class TestConnectorUserAuthorization(TestEnv): - @pytest.mark.asyncio async def test_sign_in_without_obo_when_token_not_exchangeable( self, @@ -148,7 +151,7 @@ async def test_sign_in_without_obo_when_token_not_exchangeable( """Test that _sign_in raises error when token is not exchangeable but OBO is configured.""" security_token = make_jwt(aud=None) # Non-exchangeable token context = self.TurnContext(mocker, security_token=security_token) - + # When token is not exchangeable but OBO is configured (connection and scopes are set), # it should raise ValueError with pytest.raises(ValueError, match="not exchangeable"): @@ -165,7 +168,7 @@ async def test_sign_in_raises_when_no_identity( """Test that _sign_in raises ValueError when context has no identity.""" context = self.TurnContext(mocker) context.identity = None - + with pytest.raises(ValueError, match="no security token found"): await connector_authorization._sign_in(context) @@ -178,8 +181,8 @@ async def test_sign_in_raises_when_no_security_token( """Test that _sign_in raises ValueError when identity has no security_token attribute.""" context = self.TurnContext(mocker) # Remove the security_token attribute entirely - delattr(context.identity, 'security_token') - + delattr(context.identity, "security_token") + with pytest.raises(ValueError, match="no security token found"): await connector_authorization._sign_in(context) @@ -192,7 +195,7 @@ async def test_sign_in_raises_when_security_token_is_none( """Test that _sign_in raises ValueError when security_token is None.""" context = self.TurnContext(mocker) context.identity.security_token = None - + with pytest.raises(ValueError, match="security token is None"): await connector_authorization._sign_in(context) @@ -205,7 +208,7 @@ async def test_get_refreshed_token_raises_on_expired_token( """Test that get_refreshed_token raises ValueError for expired token.""" expired_token = make_expired_jwt() context = self.TurnContext(mocker, security_token=expired_token) - + with pytest.raises(ValueError, match="Unexpected connector token expiration"): await connector_authorization.get_refreshed_token(context) @@ -219,15 +222,14 @@ async def test_get_refreshed_token_handles_obo_failure( security_token = make_jwt() context = self.TurnContext(mocker, security_token=security_token) mock_provider(mocker, exchange_token=None) # OBO returns None - + token_response = await connector_authorization.get_refreshed_token( context, "some_connection", ["scope1"] ) - + # Should return None when OBO fails assert token_response is None - @pytest.mark.asyncio async def test_sign_out_is_noop( self, @@ -236,10 +238,10 @@ async def test_sign_out_is_noop( ): """Test that _sign_out is a no-op for connector authorization.""" context = self.TurnContext(mocker, security_token=make_jwt()) - + # Should not raise any exceptions await connector_authorization._sign_out(context) - + # Verify it completes successfully (no assertions needed, just verify no exception) @pytest.mark.asyncio @@ -252,14 +254,12 @@ async def test_handle_obo_without_configuration( security_token = make_jwt() context = self.TurnContext(mocker, security_token=security_token) token_response = TokenResponse(token=security_token) - + # Don't mock provider since we shouldn't reach it - + with pytest.raises(ValueError, match="Unable to get authority configuration"): # Call with no connection or scopes (OBO not configured) - await connector_authorization._handle_obo( - context, token_response, None, [] - ) + await connector_authorization._handle_obo(context, token_response, None, []) @pytest.mark.asyncio async def test_handle_obo_with_non_exchangeable_token( @@ -271,7 +271,7 @@ async def test_handle_obo_with_non_exchangeable_token( security_token = make_jwt(aud=None) context = self.TurnContext(mocker, security_token=security_token) token_response = TokenResponse(token=security_token) - + with pytest.raises(ValueError, match="not exchangeable"): await connector_authorization._handle_obo( context, token_response, "some_connection", ["scope1"] @@ -287,10 +287,10 @@ async def test_handle_obo_with_missing_connection( security_token = make_jwt() context = self.TurnContext(mocker, security_token=security_token) token_response = TokenResponse(token=security_token) - + # Mock get_connection to return None mocker.patch.object(MsalConnectionManager, "get_connection", return_value=None) - + with pytest.raises(ValueError, match="not found"): await connector_authorization._handle_obo( context, token_response, "nonexistent_connection", ["scope1"] @@ -305,14 +305,16 @@ async def test_create_token_response_extracts_expiration( """Test that _create_token_response extracts expiration from JWT.""" security_token = make_jwt(exp_delta_seconds=7200) context = self.TurnContext(mocker, security_token=security_token) - + token_response = connector_authorization._create_token_response(context) - + assert token_response.token == security_token assert token_response.expiration is not None - + # Verify expiration is in the future - expiration = datetime.fromisoformat(token_response.expiration.replace("Z", "+00:00")) + expiration = datetime.fromisoformat( + token_response.expiration.replace("Z", "+00:00") + ) assert expiration > datetime.now(timezone.utc) @pytest.mark.asyncio @@ -324,7 +326,7 @@ async def test_create_token_response_handles_jwt_without_exp( """Test that _create_token_response handles JWT without expiration claim.""" security_token = make_jwt(include_exp=False) context = self.TurnContext(mocker, security_token=security_token) - + # Should succeed but not have expiration token_response = connector_authorization._create_token_response(context) assert token_response.token == security_token @@ -340,22 +342,24 @@ async def test_get_refreshed_token_signs_out_on_obo_error( """Test that get_refreshed_token signs out when OBO exchange fails with exception.""" security_token = make_jwt() context = self.TurnContext(mocker, security_token=security_token) - + # Mock provider to raise an exception provider = mock_instance( mocker, MsalAuth, {"acquire_token_on_behalf_of": Exception("OBO failed")} ) - mocker.patch.object(MsalConnectionManager, "get_connection", return_value=provider) + mocker.patch.object( + MsalConnectionManager, "get_connection", return_value=provider + ) provider.acquire_token_on_behalf_of.side_effect = Exception("OBO failed") - + # Mock _sign_out to verify it's called sign_out_mock = mocker.patch.object(connector_authorization, "_sign_out") - + with pytest.raises(Exception, match="OBO failed"): await connector_authorization.get_refreshed_token( context, "some_connection", ["scope1"] ) - + # Verify sign_out was called sign_out_mock.assert_called_once_with(context) @@ -368,21 +372,23 @@ async def test_sign_in_signs_out_on_obo_error( """Test that _sign_in signs out when OBO exchange fails with exception.""" security_token = make_jwt() context = self.TurnContext(mocker, security_token=security_token) - + # Mock provider to raise an exception provider = mock_instance( mocker, MsalAuth, {"acquire_token_on_behalf_of": Exception("OBO failed")} ) - mocker.patch.object(MsalConnectionManager, "get_connection", return_value=provider) + mocker.patch.object( + MsalConnectionManager, "get_connection", return_value=provider + ) provider.acquire_token_on_behalf_of.side_effect = Exception("OBO failed") - + # Mock _sign_out to verify it's called sign_out_mock = mocker.patch.object(connector_authorization, "_sign_out") - + with pytest.raises(Exception, match="OBO failed"): await connector_authorization._sign_in( context, "some_connection", ["scope1"] ) - + # Verify sign_out was called sign_out_mock.assert_called_once_with(context) diff --git a/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py b/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py index 26ff35f4..e11c612b 100644 --- a/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py +++ b/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py @@ -194,7 +194,10 @@ class TestUserAuthorization(TestEnv): [ _FlowResponse( token_response=TokenResponse( - token=make_jwt(token="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=", aud="other") + token=make_jwt( + token="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=", + aud="other", + ) ), flow_state=_FlowState( tag=_FlowStateTag.COMPLETE, @@ -205,14 +208,21 @@ class TestUserAuthorization(TestEnv): DEFAULTS.token, _SignInResponse( token_response=TokenResponse( - token=make_jwt("some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=", aud="other") + token=make_jwt( + "some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=", + aud="other", + ) ), tag=_FlowStateTag.COMPLETE, ), ], [ _FlowResponse( - token_response=TokenResponse(token=make_jwt(token="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=")), + token_response=TokenResponse( + token=make_jwt( + token="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=" + ) + ), flow_state=_FlowState( tag=_FlowStateTag.COMPLETE, auth_handler_id=DEFAULTS.auth_handler_id, @@ -315,13 +325,27 @@ async def test_sign_out_individual( TokenResponse(token=make_jwt(aud=None)), ], [ - TokenResponse(token=make_jwt(token="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=", aud="other")), + TokenResponse( + token=make_jwt( + token="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=", + aud="other", + ) + ), False, DEFAULTS.token, - TokenResponse(token=make_jwt("some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=", aud="other")), + TokenResponse( + token=make_jwt( + "some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=", + aud="other", + ) + ), ], [ - TokenResponse(token=make_jwt(token="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=")), + TokenResponse( + token=make_jwt( + token="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=" + ) + ), True, None, TokenResponse(), diff --git a/tests/hosting_core/app/_oauth/test_authorization.py b/tests/hosting_core/app/_oauth/test_authorization.py index dd7cced3..c60ad4b3 100644 --- a/tests/hosting_core/app/_oauth/test_authorization.py +++ b/tests/hosting_core/app/_oauth/test_authorization.py @@ -202,7 +202,9 @@ class TestAuthorizationUsage(TestEnv): [ {DEFAULTS.auth_handler_id: DEFAULTS.token}, {}, - _SignInState(active_handler_id="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E="), + _SignInState( + active_handler_id="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=" + ), DEFAULTS.auth_handler_id, ], [ @@ -217,7 +219,9 @@ class TestAuthorizationUsage(TestEnv): DEFAULTS.auth_handler_id: "value", }, {DEFAULTS.auth_handler_id: "value"}, - _SignInState(active_handler_id="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E="), + _SignInState( + active_handler_id="some_value_bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJV0E=" + ), DEFAULTS.agentic_auth_handler_id, ], ], From b87cf7ad37f5337ceb00e37e356748550d6f4de3 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 3 Feb 2026 09:01:35 -0800 Subject: [PATCH 09/15] Addressing PR comments --- .../microsoft_agents/authentication/msal/msal_auth.py | 8 +++----- .../authentication/msal/msal_connection_manager.py | 1 - .../core/authorization/agent_auth_configuration.py | 3 +++ tests/_common/testing_objects/mocks/mock_msal_auth.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) 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 2902d020..fea640fc 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 @@ -7,7 +7,7 @@ import asyncio import logging import jwt -from typing import Optional, cast +from typing import Optional from urllib.parse import urlparse, ParseResult as URI from msal import ( ConfidentialClientApplication, @@ -77,7 +77,6 @@ async def get_access_token( local_scopes = self._resolve_scopes_list(instance_uri, scopes) msal_auth_client = self._get_client() - # msal_auth_client = self._client() if isinstance(msal_auth_client, ManagedIdentityClient): logger.info("Acquiring token using Managed Identity Client.") auth_result_payload = await _async_acquire_token_for_client( @@ -113,7 +112,6 @@ async def acquire_token_on_behalf_of( """ msal_auth_client = self._get_client() - # msal_auth_client = self._client() if isinstance(msal_auth_client, ManagedIdentityClient): logger.error( "Attempted on-behalf-of flow with Managed Identity authentication." @@ -169,10 +167,10 @@ def _resolve_authority( @staticmethod def _resolve_tenant_id( config: AgentAuthConfiguration, tenant_id: str | None = None - ) -> str | None: + ) -> str: if not config.TENANT_ID: - return None + raise ValueError("TENANT_ID is not set in the configuration.") if tenant_id and config.TENANT_ID.lower() == "common": return tenant_id diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py index fb520139..63b07c15 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py @@ -3,7 +3,6 @@ import re from typing import Dict, List, Optional -from collections.abc import Iterator from microsoft_agents.hosting.core import ( AgentAuthConfiguration, AccessTokenProviderBase, 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 e24878ff..795f8403 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 @@ -37,6 +37,9 @@ class AgentAuthConfiguration: # JWT-patch: for internal use only # patch needed to support multiple connections + # + # existing flow was to only pass in one AgentAuthConfiguration to JWTTokenValidator, + # this addition avoids breaking changes _connections: dict[str, AgentAuthConfiguration] def __init__( diff --git a/tests/_common/testing_objects/mocks/mock_msal_auth.py b/tests/_common/testing_objects/mocks/mock_msal_auth.py index d8bf363e..f2988f1d 100644 --- a/tests/_common/testing_objects/mocks/mock_msal_auth.py +++ b/tests/_common/testing_objects/mocks/mock_msal_auth.py @@ -25,7 +25,7 @@ def __init__( ) self.mock_client = mock_client - def _get_client(self, tenant_id: str | None = None) -> None: + def _get_client(self, tenant_id: str | None = None, *args): return self.mock_client From 39d248563b427734fe129fc87a4e773ba7518b9f Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 3 Feb 2026 09:13:14 -0800 Subject: [PATCH 10/15] Forcing reformat of agent_application --- .../microsoft_agents/hosting/core/app/agent_application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index 60875a0e..f977f1f7 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -4,11 +4,11 @@ """ from __future__ import annotations +import re import logging from copy import copy from functools import partial -import re from typing import ( Any, Awaitable, From 6c6b347e146bbd3689f0e8afabbe4f0510e6142a Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 3 Feb 2026 09:17:12 -0800 Subject: [PATCH 11/15] Pasting source from repo --- .../hosting/core/app/agent_application.py | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index f977f1f7..bbc48ba4 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -4,11 +4,11 @@ """ from __future__ import annotations -import re import logging from copy import copy from functools import partial +import re from typing import ( Any, Awaitable, @@ -120,11 +120,9 @@ def __init__( "ApplicationOptions.storage is required and was not configured.", stack_info=True, ) - raise ApplicationError( - """ + raise ApplicationError(""" The `ApplicationOptions.storage` property is required and was not configured. - """ - ) + """) if options.long_running_messages and ( not options.adapter or not options.bot_app_id @@ -133,12 +131,10 @@ def __init__( "ApplicationOptions.long_running_messages requires an adapter and bot_app_id.", stack_info=True, ) - raise ApplicationError( - """ + raise ApplicationError(""" The `ApplicationOptions.long_running_messages` property is unavailable because no adapter or `bot_app_id` was configured. - """ - ) + """) if options.adapter: self._adapter = options.adapter @@ -180,12 +176,10 @@ def adapter(self) -> ChannelServiceAdapter: "AgentApplication.adapter(): self._adapter is not configured.", stack_info=True, ) - raise ApplicationError( - """ + raise ApplicationError(""" The AgentApplication.adapter property is unavailable because it was not configured when creating the AgentApplication. - """ - ) + """) return self._adapter @@ -203,12 +197,10 @@ def auth(self) -> Authorization: "AgentApplication.auth(): self._auth is not configured.", stack_info=True, ) - raise ApplicationError( - """ + raise ApplicationError(""" The `AgentApplication.auth` property is unavailable because no Auth options were configured. - """ - ) + """) return self._auth @@ -592,12 +584,10 @@ async def sign_in_success(context: TurnContext, state: TurnState, connection_id: f"Failed to register sign-in success handler for route handler {func.__name__}", stack_info=True, ) - raise ApplicationError( - """ + raise ApplicationError(""" The `AgentApplication.on_sign_in_success` method is unavailable because no Auth options were configured. - """ - ) + """) return func def on_sign_in_failure( @@ -628,12 +618,10 @@ async def sign_in_failure(context: TurnContext, state: TurnState, connection_id: f"Failed to register sign-in failure handler for route handler {func.__name__}", stack_info=True, ) - raise ApplicationError( - """ + raise ApplicationError(""" The `AgentApplication.on_sign_in_failure` method is unavailable because no Auth options were configured. - """ - ) + """) return func def error( @@ -876,4 +864,4 @@ async def _on_error(self, context: TurnContext, err: ApplicationError) -> None: exc_info=True, ) logger.error(err) - raise err + raise err \ No newline at end of file From 874f51be1d37475e7e35c24ca7b43e72046c0f78 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 3 Feb 2026 09:20:51 -0800 Subject: [PATCH 12/15] Attempting another reformat --- .../microsoft_agents/hosting/core/app/agent_application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index bbc48ba4..d0eb6c1e 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -864,4 +864,4 @@ async def _on_error(self, context: TurnContext, err: ApplicationError) -> None: exc_info=True, ) logger.error(err) - raise err \ No newline at end of file + raise err From aed9f13e185b0f6488c2f0260332d27b0c1fac44 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 3 Feb 2026 10:02:54 -0800 Subject: [PATCH 13/15] Addressing PR review --- .../microsoft_agents/activity/activity.py | 9 +++++---- .../authentication/msal/msal_auth.py | 13 +++---------- .../authentication/msal/msal_connection_manager.py | 8 +++++++- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 38ca854c..80a24363 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -788,8 +788,9 @@ def get_agentic_user(self) -> Optional[str]: def get_agentic_tenant_id(self) -> Optional[str]: """Gets the agentic tenant ID from the context if it's an agentic request.""" - if self.recipient and self.recipient.tenant_id: - return self.recipient.tenant_id - if self.conversation and self.conversation.tenant_id: - return self.conversation.tenant_id + if self.is_agentic_request(): + if self.recipient and self.recipient.tenant_id: + return self.recipient.tenant_id + if self.conversation and self.conversation.tenant_id: + return self.conversation.tenant_id return None 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 fea640fc..abf596fa 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 @@ -70,10 +70,9 @@ async def get_access_token( f"Requesting access token for resource: {resource_url}, scopes: {scopes}" ) valid_uri, instance_uri = self._uri_validator(resource_url) - if not valid_uri: + if not valid_uri or instance_uri is None: raise ValueError(str(authentication_errors.InvalidInstanceUrl)) - assert instance_uri is not None local_scopes = self._resolve_scopes_list(instance_uri, scopes) msal_auth_client = self._get_client() @@ -153,6 +152,7 @@ async def acquire_token_on_behalf_of( def _resolve_authority( config: AgentAuthConfiguration, tenant_id: str | None = None ) -> str: + tenant_id = MsalAuth._resolve_tenant_id(config, tenant_id) if not tenant_id: return ( config.AUTHORITY @@ -181,8 +181,6 @@ def _create_client_application( self, tenant_id: str | None = None ) -> ConfidentialClientApplication | ManagedIdentityClient: - tenant_id = MsalAuth._resolve_tenant_id(self._msal_configuration, tenant_id) - if self._msal_configuration.AUTH_TYPE == AuthTypes.user_managed_identity: return ManagedIdentityClient( UserAssignedManagedIdentity( @@ -197,12 +195,7 @@ def _create_client_application( http_client=Session(), ) else: - if self._msal_configuration.AUTHORITY: - authority = MsalAuth._resolve_authority( - self._msal_configuration, tenant_id - ) - else: - authority = f"https://login.microsoftonline.com/{MsalAuth._resolve_tenant_id(self._msal_configuration, tenant_id)}" + authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id) if self._client_credential_cache: logger.info("Using cached client credentials for MSAL authentication.") diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py index 63b07c15..a1424014 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py @@ -70,10 +70,16 @@ def get_connection(self, connection_name: Optional[str]) -> AccessTokenProviderB :return: The OAuth connection for the agent. :rtype: :class:`microsoft_agents.hosting.core.AccessTokenProviderBase` """ + original_name = connection_name connection_name = connection_name or "SERVICE_CONNECTION" connection = self._connections.get(connection_name, None) if not connection: - raise ValueError(f"No connection found for '{connection_name}'.") + if original_name: + raise ValueError(f"No connection found for '{original_name}'.") + else: + raise ValueError( + "No default service connection found. Expected 'SERVICE_CONNECTION'." + ) return connection def get_default_connection(self) -> AccessTokenProviderBase: From 67577a45f98e7f4833252c36171f2038f206ef3c Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 3 Feb 2026 11:17:34 -0800 Subject: [PATCH 14/15] addressing PR comments --- .../microsoft_agents/authentication/msal/msal_auth.py | 11 +++++++++-- .../core/authorization/agent_auth_configuration.py | 10 ++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) 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 abf596fa..0e1f7d52 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 @@ -58,6 +58,8 @@ def __init__(self, msal_configuration: AgentAuthConfiguration): self._msal_auth_client_map: dict[ str, ConfidentialClientApplication | ManagedIdentityClient ] = {} + + # TokenCache is thread-safe and async-safe per MSAL documentation self._token_cache = TokenCache() logger.debug( f"Initializing MsalAuth with configuration: {self._msal_configuration}" @@ -70,8 +72,9 @@ async def get_access_token( f"Requesting access token for resource: {resource_url}, scopes: {scopes}" ) valid_uri, instance_uri = self._uri_validator(resource_url) - if not valid_uri or instance_uri is None: + if not valid_uri: raise ValueError(str(authentication_errors.InvalidInstanceUrl)) + assert instance_uri is not None # for mypy local_scopes = self._resolve_scopes_list(instance_uri, scopes) msal_auth_client = self._get_client() @@ -170,6 +173,8 @@ def _resolve_tenant_id( ) -> str: if not config.TENANT_ID: + if tenant_id: + return tenant_id raise ValueError("TENANT_ID is not set in the configuration.") if tenant_id and config.TENANT_ID.lower() == "common": @@ -241,8 +246,10 @@ def _create_client_application( def _client_rep( self, tenant_id: str | None = None, instance_id: str | None = None ) -> str: + # Create a unique representation for the client based on tenant_id and instance_id + # instance_id None is for when no agentic instance is associated with the request. tenant_id = tenant_id or self._msal_configuration.TENANT_ID - return f"tenant:{tenant_id}.instance:{instance_id}" # might add more later + return f"tenant:{tenant_id}.instance:{instance_id}" def _get_client( self, tenant_id: str | None = None, instance_id: str | None = None 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 795f8403..dfccfde0 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 @@ -35,11 +35,13 @@ class AgentAuthConfiguration: ALT_BLUEPRINT_ID: Optional[str] ANONYMOUS_ALLOWED: bool = False - # JWT-patch: for internal use only - # patch needed to support multiple connections + # Multi-connection support: Maintains a map of all configured connections + # to enable JWT validation across connections. This allows tokens issued + # for any configured connection to be validated, supporting multi-tenant + # scenarios where connections share a security boundary. # - # existing flow was to only pass in one AgentAuthConfiguration to JWTTokenValidator, - # this addition avoids breaking changes + # Note: This is an internal implementation detail. External code should + # not directly access _connections. _connections: dict[str, AgentAuthConfiguration] def __init__( From cd5a36e9b15d16d72810dd76cc834ec0399ea72f Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 3 Feb 2026 11:28:00 -0800 Subject: [PATCH 15/15] Reformatting --- .../microsoft_agents/authentication/msal/msal_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0e1f7d52..6eb77be3 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 @@ -74,7 +74,7 @@ async def get_access_token( valid_uri, instance_uri = self._uri_validator(resource_url) if not valid_uri: raise ValueError(str(authentication_errors.InvalidInstanceUrl)) - assert instance_uri is not None # for mypy + assert instance_uri is not None # for mypy local_scopes = self._resolve_scopes_list(instance_uri, scopes) msal_auth_client = self._get_client()