From 677cd69ae2462e30f38f3891efeb1ee6ce862440 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Wed, 2 Apr 2025 19:48:37 -0700 Subject: [PATCH 1/2] Auth flow and ENV pattern for all samples --- .../agents/builder/channel_service_adapter.py | 2 +- .../rest_channel_service_client_factory.py | 22 ++++++++++++++----- .../agents/authorization/__init__.py | 2 ++ .../authorization/anonymous_token_provider.py | 12 ++++++++++ .../authorization/jwt_token_validator.py | 3 +++ .../aiohttp/jwt_authorization_middleware.py | 6 ++--- test_samples/agent_to_agent/agent_1/app.py | 3 +++ test_samples/agent_to_agent/agent_1/config.py | 18 ++++++++------- test_samples/agent_to_agent/agent_2/app.py | 3 +++ test_samples/agent_to_agent/agent_2/config.py | 12 +++++----- test_samples/echo_agent/app.py | 3 +++ test_samples/echo_agent/config.py | 12 +++++----- 12 files changed, 70 insertions(+), 28 deletions(-) create mode 100644 libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/anonymous_token_provider.py diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/channel_service_adapter.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/channel_service_adapter.py index 975a40b5..18150b5a 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/channel_service_adapter.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/channel_service_adapter.py @@ -333,7 +333,7 @@ async def process_activity( use_anonymous_auth_callback = False if ( not claims_identity.is_authenticated - and activity.channel_id == Channels.emulator + and claims_identity.authentication_type == "Anonymous" ): use_anonymous_auth_callback = True diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py index d5c8497c..75acd58d 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py @@ -2,6 +2,7 @@ from microsoft.agents.authorization import ( AuthenticationConstants, + AnonymousTokenProvider, ClaimsIdentity, Connections, ) @@ -15,6 +16,8 @@ class RestChannelServiceClientFactory(ChannelServiceClientFactoryBase): + _ANONYMOUS_TOKEN_PROVIDER = AnonymousTokenProvider() + def __init__( self, configuration: Any, @@ -42,12 +45,14 @@ async def create_connector_client( raise TypeError( "RestChannelServiceClientFactory.create_connector_client: audience can't be None or Empty" ) + + token_provider = self._connections.get_token_provider( + claims_identity, service_url + ) if not use_anonymous else self._ANONYMOUS_TOKEN_PROVIDER return ConnectorClient( endpoint=service_url, - credential_token_provider=self._connections.get_token_provider( - claims_identity, service_url - ), + credential_token_provider=token_provider, credential_resource_url=audience, credential_scopes=scopes, ) @@ -55,10 +60,15 @@ async def create_connector_client( async def create_user_token_client( self, claims_identity: ClaimsIdentity, use_anonymous: bool = False ) -> UserTokenClient: - return UserTokenClient( - credential_token_provider=self._connections.get_token_provider( + token_provider = ( + self._connections.get_token_provider( claims_identity, self._token_service_endpoint - ), + ) + if not use_anonymous + else self._ANONYMOUS_TOKEN_PROVIDER + ) + return UserTokenClient( + credential_token_provider=token_provider, credential_resource_url=self._token_service_audience, credential_scopes=[f"{self._token_service_audience}/.default"], endpoint=self._token_service_endpoint, diff --git a/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/__init__.py b/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/__init__.py index 5f589df0..4a78f57b 100644 --- a/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/__init__.py +++ b/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/__init__.py @@ -1,5 +1,6 @@ from .access_token_provider_base import AccessTokenProviderBase from .authentication_constants import AuthenticationConstants +from .anonymous_token_provider import AnonymousTokenProvider from .connections import Connections from .agent_auth_configuration import AgentAuthConfiguration from .claims_identity import ClaimsIdentity @@ -8,6 +9,7 @@ __all__ = [ "AccessTokenProviderBase", "AuthenticationConstants", + "AnonymousTokenProvider", "Connections", "AgentAuthConfiguration", "ClaimsIdentity", diff --git a/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/anonymous_token_provider.py b/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/anonymous_token_provider.py new file mode 100644 index 00000000..4ad1b640 --- /dev/null +++ b/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/anonymous_token_provider.py @@ -0,0 +1,12 @@ +from .access_token_provider_base import AccessTokenProviderBase + +class AnonymousTokenProvider(AccessTokenProviderBase): + """ + A class that provides an anonymous token for authentication. + This is used when no authentication is required. + """ + + async def get_access_token( + self, resource_url: str, scopes: list[str], force_refresh: bool = False + ) -> str: + return "" \ No newline at end of file diff --git a/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/jwt_token_validator.py b/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/jwt_token_validator.py index 56249ad3..a1851e7c 100644 --- a/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/jwt_token_validator.py +++ b/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/jwt_token_validator.py @@ -24,6 +24,9 @@ def validate_token(self, token: str) -> ClaimsIdentity: # This probably should return a ClaimsIdentity return ClaimsIdentity(decoded_token, True) + + def get_anonymous_claims(self) -> ClaimsIdentity: + return ClaimsIdentity({}, False, authentication_type="Anonymous") def _get_public_key_or_secret(self, token: str) -> PyJWK: header = get_unverified_header(token) diff --git a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py index e463b03a..60027fb8 100644 --- a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py +++ b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py @@ -17,9 +17,9 @@ async def jwt_authorization_middleware(request: Request, handler): except ValueError as e: return json_response({"error": str(e)}, status=401) else: - if (not auth_config.CLIENT_ID) and (request.app["env"] == "DEV"): - # TODO: Define anonymous strategy - request["user"] = {"name": "anonymous"} + if (not auth_config.CLIENT_ID): + # TODO: Refine anonymous strategy + request["claims_identity"] = token_validator.get_anonymous_claims() else: return json_response( {"error": "Authorization header not found"}, status=401 diff --git a/test_samples/agent_to_agent/agent_1/app.py b/test_samples/agent_to_agent/agent_1/app.py index 06786aaa..b41e73d7 100644 --- a/test_samples/agent_to_agent/agent_1/app.py +++ b/test_samples/agent_to_agent/agent_1/app.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. from aiohttp.web import Application, Request, Response, run_app +from dotenv import load_dotenv from microsoft.agents.builder import RestChannelServiceClientFactory from microsoft.agents.hosting.aiohttp import ( @@ -25,6 +26,8 @@ from agent1 import Agent1 from config import DefaultConfig +load_dotenv() + AUTH_PROVIDER = MsalAuth(DefaultConfig()) diff --git a/test_samples/agent_to_agent/agent_1/config.py b/test_samples/agent_to_agent/agent_1/config.py index e4c8cc5c..9533c2dd 100644 --- a/test_samples/agent_to_agent/agent_1/config.py +++ b/test_samples/agent_to_agent/agent_1/config.py @@ -1,3 +1,4 @@ +from os import environ from microsoft.agents.authentication.msal import AuthTypes, MsalAuthConfiguration from microsoft.agents.client import ( ChannelHostConfiguration, @@ -9,12 +10,13 @@ class DefaultConfig(MsalAuthConfiguration, ChannelsConfiguration): """Agent Configuration""" - AUTH_TYPE = AuthTypes.client_secret - TENANT_ID = "" - CLIENT_ID = "" - CLIENT_SECRET = "" - PORT = 3978 - SCOPES = ["https://api.botframework.com/.default"] + def __init__(self) -> None: + self.AUTH_TYPE = AuthTypes.client_secret + self.TENANT_ID = "" or environ.get("TENANT_ID") + self.CLIENT_ID = "" or environ.get("CLIENT_ID") + self.CLIENT_SECRET = "" or environ.get("CLIENT_SECRET") + self.PORT = 3978 + self.SCOPES = ["https://api.botframework.com/.default"] # ChannelHost configuration @staticmethod @@ -23,7 +25,7 @@ def CHANNEL_HOST_CONFIGURATION(): CHANNELS=[ ChannelInfo( id="EchoAgent", - app_id="", # Target agent's app_id + app_id="" or environ.get("TARGET_APP_ID"), # Target agent's app_id resource_url="http://localhost:3999/api/messages", token_provider="ChannelConnection", channel_factory="HttpAgentClient", @@ -31,5 +33,5 @@ def CHANNEL_HOST_CONFIGURATION(): ) ], HOST_ENDPOINT="http://localhost:3978/api/botresponse/", - HOST_APP_ID="", # usually the same as CLIENT_ID + HOST_APP_ID="" or environ.get("CLIENT_ID"), # usually the same as CLIENT_ID ) diff --git a/test_samples/agent_to_agent/agent_2/app.py b/test_samples/agent_to_agent/agent_2/app.py index 60f8713e..3b4497b9 100644 --- a/test_samples/agent_to_agent/agent_2/app.py +++ b/test_samples/agent_to_agent/agent_2/app.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. from aiohttp.web import Application, Request, Response, run_app +from dotenv import load_dotenv from microsoft.agents.builder import RestChannelServiceClientFactory from microsoft.agents.hosting.aiohttp import CloudAdapter, jwt_authorization_middleware @@ -15,6 +16,8 @@ from agent2 import Agent2 from config import DefaultConfig +load_dotenv() + AUTH_PROVIDER = MsalAuth(DefaultConfig()) diff --git a/test_samples/agent_to_agent/agent_2/config.py b/test_samples/agent_to_agent/agent_2/config.py index eb64d0d1..63360859 100644 --- a/test_samples/agent_to_agent/agent_2/config.py +++ b/test_samples/agent_to_agent/agent_2/config.py @@ -1,11 +1,13 @@ +from os import environ from microsoft.agents.authentication.msal import AuthTypes, MsalAuthConfiguration class DefaultConfig(MsalAuthConfiguration): """Agent Configuration""" - AUTH_TYPE = AuthTypes.client_secret - TENANT_ID = "" - CLIENT_ID = "" - CLIENT_SECRET = "" - PORT = 3999 + def __init__(self) -> None: + self.AUTH_TYPE = AuthTypes.client_secret + self.TENANT_ID = "" or environ.get("TENANT_ID") + self.CLIENT_ID = "" or environ.get("CLIENT_ID") + self.CLIENT_SECRET = "" or environ.get("CLIENT_SECRET") + self.PORT = 3999 diff --git a/test_samples/echo_agent/app.py b/test_samples/echo_agent/app.py index d3122d79..99025d64 100644 --- a/test_samples/echo_agent/app.py +++ b/test_samples/echo_agent/app.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. from aiohttp.web import Application, Request, Response, run_app +from dotenv import load_dotenv from microsoft.agents.builder import RestChannelServiceClientFactory from microsoft.agents.hosting.aiohttp import CloudAdapter, jwt_authorization_middleware @@ -15,6 +16,8 @@ from echo_agent import EchoAgent from config import DefaultConfig +load_dotenv() + AUTH_PROVIDER = MsalAuth(DefaultConfig()) diff --git a/test_samples/echo_agent/config.py b/test_samples/echo_agent/config.py index 90074c76..cb76b758 100644 --- a/test_samples/echo_agent/config.py +++ b/test_samples/echo_agent/config.py @@ -1,11 +1,13 @@ +from os import environ from microsoft.agents.authentication.msal import AuthTypes, MsalAuthConfiguration class DefaultConfig(MsalAuthConfiguration): """Agent Configuration""" - AUTH_TYPE = AuthTypes.client_secret - TENANT_ID = "" - CLIENT_ID = "" - CLIENT_SECRET = "" - PORT = 3978 + def __init__(self) -> None: + self.AUTH_TYPE = AuthTypes.client_secret + self.TENANT_ID = "" or environ.get("TENANT_ID") + self.CLIENT_ID = "" or environ.get("CLIENT_ID") + self.CLIENT_SECRET = "" or environ.get("CLIENT_SECRET") + self.PORT = 3978 From f684035b5d30a793d5e227dab77d211c653f1a19 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Wed, 2 Apr 2025 19:51:14 -0700 Subject: [PATCH 2/2] Auth flow and ENV pattern for all samples - Formatting --- .../builder/rest_channel_service_client_factory.py | 10 ++++++---- .../agents/authorization/anonymous_token_provider.py | 3 ++- .../agents/authorization/jwt_token_validator.py | 2 +- .../hosting/aiohttp/jwt_authorization_middleware.py | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py index 75acd58d..c89d90cd 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py @@ -45,10 +45,12 @@ async def create_connector_client( raise TypeError( "RestChannelServiceClientFactory.create_connector_client: audience can't be None or Empty" ) - - token_provider = self._connections.get_token_provider( - claims_identity, service_url - ) if not use_anonymous else self._ANONYMOUS_TOKEN_PROVIDER + + token_provider = ( + self._connections.get_token_provider(claims_identity, service_url) + if not use_anonymous + else self._ANONYMOUS_TOKEN_PROVIDER + ) return ConnectorClient( endpoint=service_url, diff --git a/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/anonymous_token_provider.py b/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/anonymous_token_provider.py index 4ad1b640..318566a3 100644 --- a/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/anonymous_token_provider.py +++ b/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/anonymous_token_provider.py @@ -1,5 +1,6 @@ from .access_token_provider_base import AccessTokenProviderBase + class AnonymousTokenProvider(AccessTokenProviderBase): """ A class that provides an anonymous token for authentication. @@ -9,4 +10,4 @@ class AnonymousTokenProvider(AccessTokenProviderBase): async def get_access_token( self, resource_url: str, scopes: list[str], force_refresh: bool = False ) -> str: - return "" \ No newline at end of file + return "" diff --git a/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/jwt_token_validator.py b/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/jwt_token_validator.py index a1851e7c..837f9b15 100644 --- a/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/jwt_token_validator.py +++ b/libraries/Core/microsoft-agents-authorization/microsoft/agents/authorization/jwt_token_validator.py @@ -24,7 +24,7 @@ def validate_token(self, token: str) -> ClaimsIdentity: # This probably should return a ClaimsIdentity return ClaimsIdentity(decoded_token, True) - + def get_anonymous_claims(self) -> ClaimsIdentity: return ClaimsIdentity({}, False, authentication_type="Anonymous") diff --git a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py index 60027fb8..c6efced5 100644 --- a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py +++ b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py @@ -17,7 +17,7 @@ async def jwt_authorization_middleware(request: Request, handler): except ValueError as e: return json_response({"error": str(e)}, status=401) else: - if (not auth_config.CLIENT_ID): + if not auth_config.CLIENT_ID: # TODO: Refine anonymous strategy request["claims_identity"] = token_validator.get_anonymous_claims() else: