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 e3400d86..1362249f 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 @@ -61,6 +61,15 @@ def __init__(self, msal_configuration: AgentAuthConfiguration): f"Initializing MsalAuth with configuration: {self._msal_configuration}" ) + @property + def configuration(self) -> AgentAuthConfiguration: + """ + The configuration for the access token provider. + + :return: The configuration as an AgentAuthConfiguration object. + """ + return self._msal_configuration + async def get_access_token( self, resource_url: str, scopes: list[str], force_refresh: bool = False ) -> str: 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 26c748a1..b3b845ce 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 @@ -1,11 +1,23 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Protocol, Optional -from abc import abstractmethod +from abc import ABC, abstractmethod +from .agent_auth_configuration import AgentAuthConfiguration + + +class AccessTokenProviderBase(ABC): + + @property + @abstractmethod + def configuration(self) -> AgentAuthConfiguration: + """ + The configuration for the access token provider. + + :return: The configuration as an AgentAuthConfiguration object. + """ + raise NotImplementedError() -class AccessTokenProviderBase(Protocol): @abstractmethod async def get_access_token( self, resource_url: str, scopes: list[str], force_refresh: bool = False @@ -18,7 +30,7 @@ async def get_access_token( :param force_refresh: True to force a refresh of the token; or false to get the token only if it is necessary. :return: The access token as a string. """ - pass + raise NotImplementedError() async def acquire_token_on_behalf_of( self, scopes: list[str], user_assertion: str @@ -34,7 +46,7 @@ async def acquire_token_on_behalf_of( async def get_agentic_application_token( self, tenant_id: str, agent_app_instance_id: str - ) -> Optional[str]: + ) -> str | None: raise NotImplementedError() async def get_agentic_instance_token( @@ -48,5 +60,5 @@ async def get_agentic_user_token( agent_app_instance_id: str, agentic_user_id: str, scopes: list[str], - ) -> Optional[str]: + ) -> str | None: 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 722b3945..c4256ab5 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 @@ -3,6 +3,7 @@ from typing import Optional +from .agent_auth_configuration import AgentAuthConfiguration from .access_token_provider_base import AccessTokenProviderBase @@ -12,6 +13,14 @@ class AnonymousTokenProvider(AccessTokenProviderBase): This is used when no authentication is required. """ + @property + def configuration(self) -> AgentAuthConfiguration: + """ + The configuration for the anonymous token provider. + Since this provider does not require any configuration, it returns None. + """ + return AgentAuthConfiguration() + async def get_access_token( self, resource_url: str, scopes: list[str], force_refresh: bool = False ) -> str: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connection_manager.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connection_manager.py index 3efb71fc..a4e5a258 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connection_manager.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connection_manager.py @@ -6,6 +6,11 @@ from collections.abc import Callable +from microsoft_agents.activity import ( + Activity, + RoleTypes +) + from .agent_auth_configuration import AgentAuthConfiguration from .access_token_provider_base import AccessTokenProviderBase from .claims_identity import ClaimsIdentity @@ -151,7 +156,7 @@ def _service_url_matches(pattern: str, service_url: str) -> bool: raise ValueError( f"Invalid SERVICEURL regex '{pattern}' in connections map: {exc}" ) from exc - + def get_token_provider( self, claims_identity: ClaimsIdentity, service_url: str ) -> AccessTokenProviderBase: @@ -189,6 +194,40 @@ def get_token_provider( raise ValueError( f"No connection found for audience '{aud}' and serviceUrl '{service_url}'." ) + + def get_token_provider_from_activity( + self, + claims_identity: ClaimsIdentity, + activity: Activity + ) -> AccessTokenProviderBase: + """ + Get the OAuth token provider for the agent from an activity. + + :param claims_identity: The claims identity of the bot. + :type claims_identity: :class:`microsoft_agents.hosting.core.ClaimsIdentity` + :param activity: The activity of the bot. + :type activity: dict + :return: The OAuth token provider for the agent. + :rtype: :class:`microsoft_agents.hosting.core.AccessTokenProviderBase` + :raises ValueError: If no connection is found for the given audience and service URL. + """ + connection: AccessTokenProviderBase | None = None + try: + connection = self.get_token_provider(claims_identity, activity.service_url) + finally: + if (connection is not None and ( + activity.recipient.role == RoleTypes.agentic_identity or + activity.recipient.role == RoleTypes.agentic_user + )): + if connection.configuration.ALT_BLUEPRINT_ID: + connection = self.get_connection(connection.configuration.ALT_BLUEPRINT_ID) + + if connection: + return connection + + raise RuntimeError( + "The connection returned by get_token_provider is not compatible with the activity's recipient role." + ) def get_default_connection_configuration(self) -> AgentAuthConfiguration: """ 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..6c8749c1 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 @@ -4,6 +4,8 @@ from abc import abstractmethod from typing import Protocol +from microsoft_agents.activity import Activity + from .agent_auth_configuration import AgentAuthConfiguration from .access_token_provider_base import AccessTokenProviderBase from .claims_identity import ClaimsIdentity @@ -15,6 +17,9 @@ class Connections(Protocol): def get_connection(self, connection_name: str) -> AccessTokenProviderBase: """ Get the OAuth connection for the agent. + + :param connection_name: The name of the connection. + :return: The OAuth connection for the agent. """ raise NotImplementedError() @@ -22,6 +27,8 @@ def get_connection(self, connection_name: str) -> AccessTokenProviderBase: def get_default_connection(self) -> AccessTokenProviderBase: """ Get the default OAuth connection for the agent. + + :return: The default OAuth connection for the agent. """ raise NotImplementedError() @@ -31,12 +38,31 @@ def get_token_provider( ) -> AccessTokenProviderBase: """ Get the OAuth token provider for the agent. + + :param claims_identity: The claims identity of the agent. + :param service_url: The service URL of the agent. + :return: The OAuth token provider for the agent. """ raise NotImplementedError() + + @abstractmethod + def get_token_provider_from_activity( + self, claims_identity: ClaimsIdentity, activity: Activity + ) -> AccessTokenProviderBase: + """ + Get the OAuth token provider for the agent from an activity. + + :param claims_identity: The claims identity of the agent. + :param activity: The activity from which to get the token provider. + """ + raise NotImplementedError( + ) @abstractmethod def get_default_connection_configuration(self) -> AgentAuthConfiguration: """ Get the default connection configuration for the agent. + + :return: The default connection configuration for the agent. """ raise NotImplementedError() 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 e1347b78..19640539 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 @@ -49,17 +49,9 @@ async def _get_agentic_token(self, context: TurnContext, service_url: str) -> st context.identity, service_url ) - # Provider-agnostic access to the connection's auth configuration. MSAL - # exposes it as ``_msal_configuration``; other providers (e.g. the Entra - # sidecar) expose it as ``configuration``. The only value needed here is - # the optional alternate-blueprint connection name. - configuration = getattr(connection, "_msal_configuration", None) - if configuration is None: - configuration = getattr(connection, "configuration", None) - - alt_blueprint_id = ( - getattr(configuration, "ALT_BLUEPRINT_ID", None) if configuration else None - ) + configuration = connection.configuration + alt_blueprint_id = configuration.ALT_BLUEPRINT_ID + if alt_blueprint_id: logger.debug( "Using alternative blueprint ID for agentic token retrieval: %s",