From 3d6ddb270de7ac4e3a39fa744725dce5ac11bdcb Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 19 Oct 2026 11:25:31 -0700 Subject: [PATCH 1/6] Replacing AuthHandler with AuthHandlerSettings --- .../microsoft_agents/hosting/core/__init__.py | 2 + .../hosting/core/app/__init__.py | 2 + .../hosting/core/app/app_options.py | 4 +- .../hosting/core/app/oauth/__init__.py | 3 +- .../oauth/_handlers/_authorization_handler.py | 31 ++++--- .../_handlers/agentic_user_authorization.py | 36 ++++---- .../_handlers/connector_user_authorization.py | 39 +++++---- .../hosting/core/app/oauth/auth_handler.py | 44 +++++++--- .../hosting/core/app/oauth/authorization.py | 18 ++-- test_samples/bizchat-sso/src/agent.py | 84 +++++++++++++++++++ test_samples/bizchat-sso/src/main.py | 12 +++ test_samples/bizchat-sso/src/start_server.py | 35 ++++++++ tests/_common/data/auth_test_data.py | 16 ++-- .../testing_objects/testing_authorization.py | 10 ++- .../app/_oauth/test_auth_handler.py | 8 +- .../app/_oauth/test_authorization.py | 4 +- 16 files changed, 251 insertions(+), 97 deletions(-) create mode 100644 test_samples/bizchat-sso/src/agent.py create mode 100644 test_samples/bizchat-sso/src/main.py create mode 100644 test_samples/bizchat-sso/src/start_server.py diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py index e1338972e..ef717587c 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py @@ -44,6 +44,7 @@ Authorization, AuthHandler, AgenticUserAuthorization, + AuthHandlerSettings, ) # App State @@ -151,6 +152,7 @@ "TempState", "Authorization", "AuthHandler", + "AuthHandlerSettings", "AccessTokenProviderBase", "AuthenticationConstants", "AnonymousTokenProvider", diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/__init__.py index 07ca96394..30635a97d 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/__init__.py @@ -18,6 +18,7 @@ from .oauth import ( Authorization, AuthHandler, + AuthHandlerSettings, AgenticUserAuthorization, ) @@ -59,6 +60,7 @@ "TempState", "Authorization", "AuthHandler", + "AuthHandlerSettings", "AgenticUserAuthorization", # Proactive "Conversation", diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/app_options.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/app_options.py index 283168f26..99527d972 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/app_options.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/app_options.py @@ -8,7 +8,7 @@ from dataclasses import dataclass, field from typing import Callable, Optional -from microsoft_agents.hosting.core.app.oauth import AuthHandler +from microsoft_agents.hosting.core.app.oauth import AuthHandlerSettings from microsoft_agents.hosting.core.authorization import Connections from microsoft_agents.hosting.core.storage import Storage @@ -81,7 +81,7 @@ class ApplicationOptions: If not provided, the default `TurnState` will be used. """ - authorization_handlers: Optional[dict[str, AuthHandler]] = None + authorization_handlers: Optional[dict[str, AuthHandlerSettings]] = None """ Optional. Authorization handler for OAuth flows. If not provided, no OAuth flows will be supported. diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/__init__.py index 79929ccfb..1e95dcf9a 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/__init__.py @@ -3,7 +3,7 @@ from .authorization import Authorization -from .auth_handler import AuthHandler +from .auth_handler import AuthHandler, AuthHandlerSettings from ._sign_in_state import _SignInState from ._sign_in_response import _SignInResponse from ._handlers import ( @@ -16,6 +16,7 @@ __all__ = [ "Authorization", "AuthHandler", + "AuthHandlerSettings", "_AuthorizationHandler", "_SignInState", "_SignInResponse", diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py index 99b9760ad..f40085778 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py @@ -3,16 +3,15 @@ Licensed under the MIT License. """ -from abc import ABC -from typing import Optional import logging +from abc import ABC from microsoft_agents.activity import TokenResponse from ....turn_context import TurnContext from ....storage import Storage from ....authorization import Connections -from ..auth_handler import AuthHandler +from ..auth_handler import AuthHandlerSettings from .._sign_in_response import _SignInResponse logger = logging.getLogger(__name__) @@ -23,16 +22,16 @@ class _AuthorizationHandler(ABC): _storage: Storage _connection_manager: Connections - _handler: AuthHandler + _handler: AuthHandlerSettings def __init__( self, storage: Storage, connection_manager: Connections, - auth_handler: Optional[AuthHandler] = None, + auth_handler: AuthHandlerSettings | None = None, *, - auth_handler_id: Optional[str] = None, - auth_handler_settings: Optional[dict] = None, + auth_handler_id: str | None = None, + auth_handler_settings: dict | None = None, **kwargs, ) -> None: """ @@ -43,7 +42,7 @@ def __init__( :param connection_manager: The connection manager for OAuth providers. :type connection_manager: Connections :param auth_handlers: Configuration for OAuth providers. - :type auth_handlers: dict[str, AuthHandler], Optional + :type auth_handlers: dict[str, AuthHandler] | None :raises ValueError: When storage is None or no auth handlers provided. """ if not storage: @@ -63,7 +62,7 @@ def __init__( raise ValueError( "auth_handler_settings must be provided if auth_handler is None." ) - self._handler = AuthHandler._from_settings(auth_handler_settings) + self._handler = AuthHandlerSettings._from_settings(auth_handler_settings) self._id = auth_handler_id or self._handler.name if not self._id: @@ -72,14 +71,14 @@ def __init__( ) async def _sign_in( - self, context: TurnContext, scopes: Optional[list[str]] = None + self, context: TurnContext, scopes: list[str] | None = None ) -> _SignInResponse: """Initiate or continue the sign-in process for the user with the given auth handler. :param context: The turn context for the current turn of conversation. :type context: TurnContext :param scopes: Optional list of scopes to request during sign-in. If None, default scopes will be used. - :type scopes: Optional[list[str]], Optional + :type scopes: list[str] | None :return: A SignInResponse indicating the result of the sign-in attempt. :rtype: SignInResponse """ @@ -88,17 +87,17 @@ async def _sign_in( async def get_refreshed_token( self, context: TurnContext, - exchange_connection: Optional[str] = None, - exchange_scopes: Optional[list[str]] = None, + exchange_connection: str | None = None, + exchange_scopes: list[str] | None = None, ) -> TokenResponse: """Attempts to get a refreshed token for the user with the given scopes :param context: The turn context for the current turn of conversation. :type context: TurnContext :param exchange_connection: Optional name of the connection to use for token exchange. If None, default connection will be used. - :type exchange_connection: Optional[str], Optional + :type exchange_connection: str | None :param exchange_scopes: Optional list of scopes to request during token exchange. If None, default scopes will be used. - :type exchange_scopes: Optional[list[str]], Optional + :type exchange_scopes: list[str] | None """ raise NotImplementedError() @@ -108,6 +107,6 @@ async def _sign_out(self, context: TurnContext) -> None: :param context: The turn context for the current turn of conversation. :type context: TurnContext :param auth_handler_id: The ID of the auth handler to sign out from. If None, sign out from all handlers. - :type auth_handler_id: Optional[str] + :type auth_handler_id: str | None """ raise NotImplementedError() 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 5b1dafd05..3667dd6f5 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 @@ -5,8 +5,6 @@ import logging -from typing import Optional - from microsoft_agents.activity import TokenResponse from ....turn_context import TurnContext @@ -15,7 +13,7 @@ from ._authorization_handler import _AuthorizationHandler from ....storage import Storage from ....authorization import Connections -from ..auth_handler import AuthHandler +from ..auth_handler import AuthHandlerSettings from ..telemetry import spans logger = logging.getLogger(__name__) @@ -28,10 +26,10 @@ def __init__( self, storage: Storage, connection_manager: Connections, - auth_handler: Optional[AuthHandler] = None, + auth_handler: AuthHandlerSettings | None = None, *, - auth_handler_id: Optional[str] = None, - auth_handler_settings: Optional[dict] = None, + auth_handler_id: str | None = None, + auth_handler_settings: dict | None = None, **kwargs, ) -> None: """ @@ -42,7 +40,7 @@ def __init__( :param connection_manager: The connection manager for OAuth providers. :type connection_manager: Connections :param auth_handlers: Configuration for OAuth providers. - :type auth_handlers: dict[str, AuthHandler], Optional + :type auth_handlers: dict[str, AuthHandler] | None :raises ValueError: When storage is None or no auth handlers provided. """ super().__init__( @@ -53,9 +51,7 @@ def __init__( auth_handler_settings=auth_handler_settings, **kwargs, ) - self._alt_blueprint_name = ( - auth_handler._alt_blueprint_name if auth_handler else None - ) + self._alt_blueprint_name = self._handler._alt_blueprint_name async def get_agentic_instance_token(self, context: TurnContext) -> TokenResponse: """Gets the agentic instance token for the current agent instance. @@ -63,7 +59,7 @@ async def get_agentic_instance_token(self, context: TurnContext) -> TokenRespons :param context: The context object for the current turn. :type context: TurnContext :return: The agentic instance token, or None if not an agentic request. - :rtype: Optional[str] + :rtype: TokenResponse """ if not context.activity.is_agentic_request(): @@ -103,7 +99,7 @@ async def get_agentic_user_token( :param scopes: The scopes to request for the token. :type scopes: list[str] :return: The agentic user token, or None if not an agentic request or no user. - :rtype: Optional[str] + :rtype: TokenResponse """ logger.info("Retrieving agentic user token for scopes: %s", scopes) @@ -163,8 +159,8 @@ async def get_agentic_user_token( async def _sign_in( self, context: TurnContext, - exchange_connection: Optional[str] = None, - exchange_scopes: Optional[list[str]] = None, + exchange_connection: str | None = None, + exchange_scopes: list[str] | None = None, ) -> _SignInResponse: """Retrieves the agentic user token if available. @@ -173,7 +169,7 @@ async def _sign_in( :param connection_name: The name of the connection to use for sign-in. :type connection_name: str :param scopes: The scopes to request for the token. - :type scopes: Optional[list[str]] + :type scopes: list[str] | None :return: A _SignInResponse containing the token response and flow state tag. :rtype: _SignInResponse """ @@ -189,17 +185,17 @@ async def _sign_in( async def get_refreshed_token( self, context: TurnContext, - exchange_connection: Optional[str] = None, - exchange_scopes: Optional[list[str]] = None, + exchange_connection: str | None = None, + exchange_scopes: list[str] | None = None, ) -> TokenResponse: """Attempts to get a refreshed token for the user with the given scopes :param context: The turn context for the current turn of conversation. :type context: TurnContext :param exchange_connection: Optional name of the connection to use for token exchange. If None, default connection will be used. - :type exchange_connection: Optional[str], Optional + :type exchange_connection: str | None :param exchange_scopes: Optional list of scopes to request during token exchange. If None, default scopes will be used. - :type exchange_scopes: Optional[list[str]], Optional + :type exchange_scopes: list[str] | None """ with spans.AgenticToken( auth_handler_id=self._id, @@ -211,6 +207,6 @@ async def get_refreshed_token( return await self.get_agentic_user_token(context, exchange_scopes) async def sign_out( - self, context: TurnContext, auth_handler_id: Optional[str] = None + self, context: TurnContext, auth_handler_id: str | None = None ) -> None: """Nothing to do for agentic sign out.""" diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py index 0e57433a0..93f613fdc 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py @@ -6,7 +6,6 @@ import logging import jwt from datetime import datetime, timezone -from typing import Optional from microsoft_agents.activity import TokenResponse from microsoft_agents.hosting.core.errors import ErrorResources @@ -15,7 +14,7 @@ from ....turn_context import TurnContext from ....storage import Storage from ....authorization import Connections -from ..auth_handler import AuthHandler +from ..auth_handler import AuthHandlerSettings from ._authorization_handler import _AuthorizationHandler from .._sign_in_response import _SignInResponse @@ -32,10 +31,10 @@ def __init__( self, storage: Storage, connection_manager: Connections, - auth_handler: Optional[AuthHandler] = None, + auth_handler: AuthHandlerSettings | None = None, *, - auth_handler_id: Optional[str] = None, - auth_handler_settings: Optional[dict] = None, + auth_handler_id: str | None = None, + auth_handler_settings: dict | None = None, **kwargs, ) -> None: """ @@ -46,11 +45,11 @@ def __init__( :param connection_manager: The connection manager for OAuth providers. :type connection_manager: Connections :param auth_handler: Configuration for OAuth provider. - :type auth_handler: AuthHandler, Optional + :type auth_handler: AuthHandlerSettings | None :param auth_handler_id: Optional ID of the auth handler. - :type auth_handler_id: str, Optional + :type auth_handler_id: str | None :param auth_handler_settings: Optional settings dict for the auth handler. - :type auth_handler_settings: dict, Optional + :type auth_handler_settings: dict | None """ super().__init__( storage, @@ -64,8 +63,8 @@ def __init__( async def _sign_in( self, context: TurnContext, - exchange_connection: Optional[str] = None, - exchange_scopes: Optional[list[str]] = None, + exchange_connection: str | None = None, + exchange_scopes: list[str] | None = None, ) -> _SignInResponse: """ For connector requests, there is no separate sign-in flow. @@ -74,9 +73,9 @@ async def _sign_in( :param context: The turn context for the current turn of conversation. :type context: TurnContext :param exchange_connection: Optional connection name for token exchange. - :type exchange_connection: Optional[str] + :type exchange_connection: str | None :param exchange_scopes: Optional list of scopes (unused for connector auth). - :type exchange_scopes: Optional[list[str]] + :type exchange_scopes: list[str] | None :return: A SignInResponse with the extracted token. :rtype: _SignInResponse """ @@ -89,8 +88,8 @@ async def _sign_in( async def get_refreshed_token( self, context: TurnContext, - exchange_connection: Optional[str] = None, - exchange_scopes: Optional[list[str]] = None, + exchange_connection: str | None = None, + exchange_scopes: list[str] | None = None, ) -> TokenResponse: """ Gets the connector user token and optionally exchanges it via OBO. @@ -98,9 +97,9 @@ async def get_refreshed_token( :param context: The turn context for the current turn of conversation. :type context: TurnContext :param exchange_connection: Optional name of the connection to use for token exchange. - :type exchange_connection: Optional[str], Optional + :type exchange_connection: str | None :param exchange_scopes: Optional list of scopes to request during token exchange. - :type exchange_scopes: Optional[list[str]], Optional + :type exchange_scopes: list[str] | None :return: The token response, potentially after OBO exchange. :rtype: TokenResponse """ @@ -146,8 +145,8 @@ async def _handle_obo( self, context: TurnContext, input_token_response: TokenResponse, - exchange_connection: Optional[str] = None, - exchange_scopes: Optional[list[str]] = None, + exchange_connection: str | None = None, + exchange_scopes: list[str] | None = None, ) -> TokenResponse: """ Exchanges a token for another token with different scopes via OBO flow. @@ -157,9 +156,9 @@ async def _handle_obo( :param input_token_response: The input token to exchange. :type input_token_response: TokenResponse :param exchange_connection: Optional connection name for exchange. - :type exchange_connection: Optional[str] + :type exchange_connection: str | None :param exchange_scopes: Optional scopes for the exchanged token. - :type exchange_scopes: Optional[list[str]] + :type exchange_scopes: list[str] | None :return: The token response after exchange, or the original if exchange not configured. :rtype: TokenResponse """ diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py index 4d11b2128..28f831594 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py @@ -4,14 +4,14 @@ """ import logging -from typing import Optional +import warnings logger = logging.getLogger(__name__) # name due to compat. # see AuthorizationHandler for a class that does work. -class AuthHandler: +class AuthHandlerSettings: """ Interface defining an authorization handler for OAuth flows. """ @@ -32,11 +32,11 @@ def __init__( abs_oauth_connection_name: str = "", obo_connection_name: str = "", auth_type: str = "", - scopes: Optional[list[str]] = None, + scopes: list[str] | None = None, **kwargs, ): """ - Initializes a new instance of AuthHandler. + Initializes a new instance of AuthHandlerSettings. :param name: The name of the handler. This is how it is accessed programatically in this library. @@ -67,8 +67,13 @@ def __init__( if scopes: self.scopes = list(scopes) else: - self.scopes = AuthHandler._format_scopes(kwargs.get("SCOPES", "")) + self.scopes = AuthHandlerSettings._format_scopes(kwargs.get("SCOPES", "")) self._alt_blueprint_name = kwargs.get("ALT_BLUEPRINT_NAME", None) + self._kwargs = kwargs + + @property + def kwargs(self) -> dict: + return dict(self._kwargs) @staticmethod def _format_scopes(scopes: str) -> list[str]: @@ -78,22 +83,37 @@ def _format_scopes(scopes: str) -> list[str]: @staticmethod def _from_settings(settings: dict): """ - Creates an AuthHandler instance from a settings dictionary. + Creates an AuthHandlerSettings instance from a settings dictionary. - :param settings: The settings dictionary containing configuration for the AuthHandler. + :param settings: The settings dictionary containing configuration for the AuthHandlerSettings. :type settings: dict - :return: An instance of AuthHandler configured with the provided settings. - :rtype: AuthHandler + :return: An instance of AuthHandlerSettings configured with the provided settings. + :rtype: AuthHandlerSettings """ if not settings: - raise ValueError("Settings dictionary is required to create AuthHandler") + raise ValueError( + "Settings dictionary is required to create AuthHandlerSettings" + ) - return AuthHandler( + return AuthHandlerSettings( name=settings.get("NAME", ""), title=settings.get("TITLE", ""), text=settings.get("TEXT", ""), abs_oauth_connection_name=settings.get("AZUREBOTOAUTHCONNECTIONNAME", ""), obo_connection_name=settings.get("OBOCONNECTIONNAME", ""), auth_type=settings.get("TYPE", ""), - scopes=AuthHandler._format_scopes(settings.get("SCOPES", "")), + scopes=AuthHandlerSettings._format_scopes(settings.get("SCOPES", "")), + ) + + +class AuthHandler(AuthHandlerSettings): + """Deprecated. Use AuthHandlerSettings instead.""" + + def __init__(self, *args, **kwargs): + """Initialize the deprecated AuthHandler class.""" + warnings.warn( + "AuthHandler is deprecated. Please use AuthHandlerSettings instead.", + DeprecationWarning, + stacklevel=2, ) + super().__init__(*args, **kwargs) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index 7d0300025..9de356c1c 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -27,7 +27,7 @@ from ...authorization import Connections from ..._oauth import _FlowStateTag from ..state import TurnState -from .auth_handler import AuthHandler +from .auth_handler import AuthHandlerSettings from ._sign_in_state import _SignInState from ._sign_in_response import _SignInResponse from ._handlers import ( @@ -66,7 +66,7 @@ def __init__( self, storage: Storage, connection_manager: Connections, - auth_handlers: dict[str, AuthHandler] | None = None, + auth_handlers: dict[str, AuthHandlerSettings] | None = None, auto_sign_in: bool = False, use_cache: bool = False, **kwargs, @@ -82,7 +82,7 @@ def __init__( :param connection_manager: The connection manager for OAuth providers. :type connection_manager: :class:`microsoft_agents.hosting.core.authorization.Connections` :param auth_handlers: Configuration for OAuth providers. - :type auth_handlers: dict[str, :class:`microsoft_agents.hosting.core.app.oauth.auth_handler.AuthHandler`], Optional + :type auth_handlers: dict[str, :class:`microsoft_agents.hosting.core.app.oauth.auth_handler.AuthHandlerSettings`], Optional :raises ValueError: When storage is None or no auth handlers provided. """ if not storage: @@ -107,7 +107,7 @@ def __init__( # get from config handlers_config: dict[str, dict] = auth_configuration.get("HANDLERS") or {} auth_handlers = { - handler_name: AuthHandler( + handler_name: AuthHandlerSettings( name=handler_name, auth_type=config.get("TYPE", ""), **config.get("SETTINGS", {}), @@ -132,7 +132,7 @@ def _init_handlers(self) -> None: it initializes an instance of each variant that is referenced. :param auth_handlers: A dictionary of auth handler configurations. - :type auth_handlers: dict[str, :class:`microsoft_agents.hosting.core.app.oauth.auth_handler.AuthHandler`] + :type auth_handlers: dict[str, :class:`microsoft_agents.hosting.core.app.oauth.auth_handler.AuthHandlerSettings`] """ for name, auth_handler in self._handler_settings.items(): auth_type = auth_handler.auth_type @@ -429,13 +429,13 @@ async def exchange_token( :param context: The context object for the current turn. :type context: :class:`microsoft_agents.hosting.core.turn_context.TurnContext` :param scopes: The scopes to request during the token exchange or refresh. Defaults - to the list given in the AuthHandler configuration if None. + to the list given in the AuthHandlerSettings configuration if None. :type scopes: list[str] | None :param auth_handler_id: The ID of the auth handler to exchange or refresh the token for. If None, the default handler will be used. :type auth_handler_id: str | None :param exchange_connection: The name of the connection to use for token exchange. If None, - the connection defined in the AuthHandler configuration will be used. + the connection defined in the AuthHandlerSettings configuration will be used. :type exchange_connection: str | None :return: The token response from the OAuth provider. :rtype: :class:`microsoft_agents.activity.TokenResponse` @@ -483,13 +483,13 @@ def exchange_token_as_token_credential( :param context: The context object for the current turn. :type context: :class:`microsoft_agents.hosting.core.turn_context.TurnContext` :param scopes: The scopes to request during the token exchange or refresh. Defaults - to the list given in the AuthHandler configuration if None. + to the list given in the AuthHandlerSettings configuration if None. :type scopes: list[str] | None :param auth_handler_id: The ID of the auth handler to exchange or refresh the token for. If None, the default handler will be used. :type auth_handler_id: str | None :param exchange_connection: The name of the connection to use for token exchange. If None, - the connection defined in the AuthHandler configuration will be used. + the connection defined in the AuthHandlerSettings configuration will be used. :type exchange_connection: str | None :return: An instance of `AsyncTokenCredential`. :rtype: :class:`azure.core.credentials_async.AsyncTokenCredential` diff --git a/test_samples/bizchat-sso/src/agent.py b/test_samples/bizchat-sso/src/agent.py new file mode 100644 index 000000000..8f19b1ec6 --- /dev/null +++ b/test_samples/bizchat-sso/src/agent.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging +from os import environ +from dotenv import load_dotenv +from microsoft_agents.activity import load_configuration_from_env +from microsoft_agents.authentication.msal import ( + MsalConnectionManager, +) +from microsoft_agents.hosting.core import ( + AgentApplication, + TurnState, + TurnContext, + MemoryStorage, + AuthHandlerSettings, +) +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core.app.oauth.authorization import Authorization + +logging.basicConfig(level=logging.INFO) +load_dotenv(".env") + +agents_sdk_config = load_configuration_from_env(environ) + +STORAGE = MemoryStorage() +CONNECTION_MANAGER = MsalConnectionManager(**agents_sdk_config) +ADAPTER = CloudAdapter(connection_manager=CONNECTION_MANAGER) + +AUTHORIZATION = Authorization( + STORAGE, + CONNECTION_MANAGER, + auth_handlers={ + "SSO": AuthHandlerSettings( + name="SSO", + title="Sign in to the Agent", + text="Please sign in to continue", + abs_oauth_connection_name="sso", + ) + }, + **agents_sdk_config, +) + +AGENT_APP = AgentApplication[TurnState]( + storage=STORAGE, authorization=AUTHORIZATION, **agents_sdk_config +) + + +async def _help(context: TurnContext, _state: TurnState): + await context.send_activity( + "Welcome to the Echo Agent sample 🚀. " + "Type /help for help or send a message to see the echo feature in action." + ) + + +AGENT_APP.conversation_update("membersAdded")(_help) + +AGENT_APP.message("/help")(_help) + + +@AGENT_APP.message("/auth", auth_handlers=["SSO"]) +async def auth_message(context: TurnContext, _): + await context.send_activity("You are authenticated! 🎉") + + +@AGENT_APP.message("/logout") +async def logout(context: TurnContext, state: TurnState) -> None: + await AGENT_APP.auth.sign_out(context, "SSO") + await context.send_activity("You have been logged out.") + + +@AGENT_APP.activity("message") +async def on_message(context: TurnContext, _): + await context.send_activity(f"you said: {context.activity.text}") + + +if __name__ == "__main__": + try: + start_server( + agent_application=AGENT_APP, + auth_configuration=CONNECTION_MANAGER.get_default_connection_configuration(), + ) + except Exception as error: + raise error diff --git a/test_samples/bizchat-sso/src/main.py b/test_samples/bizchat-sso/src/main.py new file mode 100644 index 000000000..fea459807 --- /dev/null +++ b/test_samples/bizchat-sso/src/main.py @@ -0,0 +1,12 @@ +from agent import AGENT_APP, CONNECTION_MANAGER, ADAPTER +from start_server import start_server + +if __name__ == "__main__": + try: + start_server( + agent_application=AGENT_APP, + adapter=ADAPTER, + auth_configuration=CONNECTION_MANAGER.get_default_connection_configuration(), + ) + except Exception as error: + raise error diff --git a/test_samples/bizchat-sso/src/start_server.py b/test_samples/bizchat-sso/src/start_server.py new file mode 100644 index 000000000..6235fe8ba --- /dev/null +++ b/test_samples/bizchat-sso/src/start_server.py @@ -0,0 +1,35 @@ +from os import environ +from microsoft_agents.hosting.core import AgentApplication, AgentAuthConfiguration +from microsoft_agents.hosting.aiohttp import ( + start_agent_process, + jwt_authorization_middleware, + CloudAdapter, +) +from aiohttp.web import Request, Response, Application, run_app + + +def start_server( + agent_application: AgentApplication, + adapter: CloudAdapter, + auth_configuration: AgentAuthConfiguration +): + async def entry_point(req: Request) -> Response: + agent: AgentApplication = req.app["agent_app"] + adapter: CloudAdapter = req.app["adapter"] + return await start_agent_process( + req, + agent, + adapter, + ) + + APP = Application(middlewares=[jwt_authorization_middleware]) + APP.router.add_post("/api/messages", entry_point) + APP.router.add_get("/api/messages", lambda _: Response(status=200)) + APP["agent_configuration"] = auth_configuration + APP["agent_app"] = agent_application + APP["adapter"] = adapter + + try: + run_app(APP, host="localhost", port=environ.get("PORT", 3978)) + except Exception as error: + raise error diff --git a/tests/_common/data/auth_test_data.py b/tests/_common/data/auth_test_data.py index 698cc1f5a..1f61be260 100644 --- a/tests/_common/data/auth_test_data.py +++ b/tests/_common/data/auth_test_data.py @@ -1,13 +1,13 @@ -from microsoft_agents.hosting.core import AuthHandler +from microsoft_agents.hosting.core import AuthHandlerSettings def create_test_auth_handler( name: str, obo: bool = False, title: str = None, text: str = None -) -> AuthHandler: +) -> AuthHandlerSettings: """ - Creates a test AuthHandler instance with standardized connection names. + Creates a test AuthHandlerSettings instance with standardized connection names. - This helper function simplifies the creation of AuthHandler objects for testing + This helper function simplifies the creation of AuthHandlerSettings objects for testing by automatically generating connection names based on the provided name and optionally including On-Behalf-Of (OBO) connection configuration. @@ -18,9 +18,9 @@ def create_test_auth_handler( text: Optional descriptive text for the auth handler Returns: - AuthHandler: Configured auth handler instance with test-friendly connection names + AuthHandlerSettings: Configured auth handler instance with test-friendly connection names """ - return AuthHandler( + return AuthHandlerSettings( name, abs_oauth_connection_name=f"{name}-abs-connection", obo_connection_name=f"{name}-obo-connection" if obo else None, @@ -32,9 +32,9 @@ def create_test_auth_handler( class AUTH_TEST_DATA: def __init__(self): - self.auth_handler: AuthHandler = create_test_auth_handler("graph") + self.auth_handler: AuthHandlerSettings = create_test_auth_handler("graph") - self.auth_handlers: dict[str, AuthHandler] = { + self.auth_handlers: dict[str, AuthHandlerSettings] = { "graph": create_test_auth_handler("graph"), "github": create_test_auth_handler("github", obo=True), "slack": create_test_auth_handler("slack", obo=True), diff --git a/tests/_common/testing_objects/testing_authorization.py b/tests/_common/testing_objects/testing_authorization.py index 6e962aa63..d9fcb6da9 100644 --- a/tests/_common/testing_objects/testing_authorization.py +++ b/tests/_common/testing_objects/testing_authorization.py @@ -2,7 +2,11 @@ from microsoft_agents.activity import TokenResponse -from microsoft_agents.hosting.core import Authorization, AuthHandler, MemoryStorage +from microsoft_agents.hosting.core import ( + Authorization, + AuthHandlerSettings, + MemoryStorage, +) from .testing_connection_manager import TestingConnectionManager @@ -22,7 +26,7 @@ class TestingAuthorization(Authorization): def __init__( self, mocker, - auth_handlers: dict[str, AuthHandler], + auth_handlers: dict[str, AuthHandlerSettings], token: Union[str, None] = "default", flow_started=False, sign_in_failed=False, @@ -35,7 +39,7 @@ def __init__( mock OAuth flows. Args: - auth_handlers: Dictionary mapping handler names to AuthHandler instances + auth_handlers: Dictionary mapping handler names to AuthHandlerSettings instances token: Token value to use in mock responses. "default" uses auto-generated tokens, None simulates no token available, or provide custom jwt token string flow_started: Simulate OAuth flows that have already started diff --git a/tests/hosting_core/app/_oauth/test_auth_handler.py b/tests/hosting_core/app/_oauth/test_auth_handler.py index b362891e4..5b0e8bb5f 100644 --- a/tests/hosting_core/app/_oauth/test_auth_handler.py +++ b/tests/hosting_core/app/_oauth/test_auth_handler.py @@ -1,6 +1,6 @@ import pytest -from microsoft_agents.hosting.core import AuthHandler +from microsoft_agents.hosting.core import AuthHandlerSettings from tests._common.data import ( DEFAULT_TEST_VALUES, @@ -13,7 +13,7 @@ AGENTIC_ENV_DICT = AGENTIC_TEST_ENV_DICT() -class TestAuthHandler: +class TestAuthHandlerSettings: @pytest.fixture def auth_setting(self): return ENV_DICT["AGENTAPPLICATION"]["USERAUTHORIZATION"]["HANDLERS"][ @@ -27,7 +27,7 @@ def agentic_auth_setting(self): ]["SETTINGS"] def test_init(self, auth_setting): - auth_handler = AuthHandler(DEFAULTS.auth_handler_id, **auth_setting) + auth_handler = AuthHandlerSettings(DEFAULTS.auth_handler_id, **auth_setting) assert auth_handler.name == DEFAULTS.auth_handler_id assert auth_handler.title == DEFAULTS.auth_handler_title assert auth_handler.text == DEFAULTS.auth_handler_text @@ -37,7 +37,7 @@ def test_init(self, auth_setting): ) def test_init_agentic(self, agentic_auth_setting): - auth_handler = AuthHandler( + auth_handler = AuthHandlerSettings( DEFAULTS.agentic_auth_handler_id, **agentic_auth_setting ) assert auth_handler.name == DEFAULTS.agentic_auth_handler_id diff --git a/tests/hosting_core/app/_oauth/test_authorization.py b/tests/hosting_core/app/_oauth/test_authorization.py index 50f603daa..a1f0a16b3 100644 --- a/tests/hosting_core/app/_oauth/test_authorization.py +++ b/tests/hosting_core/app/_oauth/test_authorization.py @@ -22,7 +22,7 @@ from microsoft_agents.hosting.core._oauth import _FlowStateTag from microsoft_agents.hosting.core import ( - AuthHandler, + AuthHandlerSettings, MemoryStorage, ) @@ -179,7 +179,7 @@ def test_resolve_handler(self, connection_manager, storage, auth_handler_id): handler_config = AGENTIC_ENV_DICT["AGENTAPPLICATION"]["USERAUTHORIZATION"][ "HANDLERS" ][auth_handler_id] - auth._resolve_handler(auth_handler_id) == AuthHandler( + auth._resolve_handler(auth_handler_id) == AuthHandlerSettings( auth_handler_id, **handler_config ) From b813d7142128ed15aca58b562bc30f9023b691df Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 19 Oct 2026 11:34:21 -0700 Subject: [PATCH 2/6] Replacing AuthHandler with AuthHandlerSettings in test file --- tests/_common/data/default_test_values.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/_common/data/default_test_values.py b/tests/_common/data/default_test_values.py index 98701ef3c..cbf5c2ca8 100644 --- a/tests/_common/data/default_test_values.py +++ b/tests/_common/data/default_test_values.py @@ -1,5 +1,5 @@ from microsoft_agents.hosting.core import ( - AuthHandler, + AuthHandlerSettings, ) @@ -40,4 +40,4 @@ def __init__(self): self.missing_abs_oauth_connection_name = "missing_connection_name" - self.auth_handlers = [AuthHandler()] + self.auth_handlers = [AuthHandlerSettings()] From e67b9a0cbcb5df6ec43ddeff846c8a28290b90af Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 19 Oct 2026 13:32:49 -0700 Subject: [PATCH 3/6] Fixing sample and updating changelog --- changelog.md | 9 + .../oauth/_handlers/_authorization_handler.py | 38 +++- .../_handlers/agentic_user_authorization.py | 16 +- .../_handlers/connector_user_authorization.py | 12 +- .../hosting/core/app/oauth/auth_handler.py | 45 +++-- .../hosting/core/app/oauth/authorization.py | 6 +- test_samples/bizchat-sso/src/agent.py | 4 + .../test_agentic_user_authorization.py | 30 ++-- .../test_connector_user_authorization.py | 8 +- .../_handlers/test_user_authorization.py | 6 +- .../app/_oauth/test_auth_handler.py | 163 +++++++++++++++++- .../app/_oauth/test_authorization.py | 43 ++++- 12 files changed, 319 insertions(+), 61 deletions(-) diff --git a/changelog.md b/changelog.md index 2fed6ddbf..ad2977ee2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,12 @@ +# Microsoft 365 Agents SDK for Python - Release Notes v1.10.0 (Unreleased) + +**Previous Version:** 1.9.0 (Released 2026-10-19) + +- **Auth Handler Configuration**: Added `AuthHandlerSettings` and a unified `settings` parameter accepting a settings object or dictionary across authorization handlers (#633). +- **Deprecation**: Deprecated `AuthHandler` in favor of `AuthHandlerSettings`, and handler constructor parameters `auth_handler` and `auth_handler_settings` in favor of `settings`, while retaining backward compatibility (#633). + +--- + # Microsoft 365 Agents SDK for Python - Release Notes v1.9.0 **Release Date:** 2026-10-19 diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py index f40085778..21f02a169 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py @@ -4,6 +4,8 @@ """ import logging +import warnings + from abc import ABC from microsoft_agents.activity import TokenResponse @@ -28,9 +30,10 @@ def __init__( self, storage: Storage, connection_manager: Connections, - auth_handler: AuthHandlerSettings | None = None, + settings: dict | AuthHandlerSettings | None = None, *, auth_handler_id: str | None = None, + auth_handler: AuthHandlerSettings | None = None, auth_handler_settings: dict | None = None, **kwargs, ) -> None: @@ -41,27 +44,48 @@ def __init__( :type storage: Storage :param connection_manager: The connection manager for OAuth providers. :type connection_manager: Connections - :param auth_handlers: Configuration for OAuth providers. - :type auth_handlers: dict[str, AuthHandler] | None + :param settings: Optional settings for the auth handler. + :type settings: dict | AuthHandlerSettings | None + :param auth_handler: (Deprecated) Configuration for OAuth provider. + :type auth_handler: AuthHandlerSettings | None + :param auth_handler_id: Optional ID of the auth handler. + :type auth_handler_id: str | None + :param auth_handler_settings: (Deprecated) Optional settings dict for the auth handler. + :type auth_handler_settings: dict | None :raises ValueError: When storage is None or no auth handlers provided. """ if not storage: raise ValueError("Storage is required for Authorization") - if not auth_handler and not auth_handler_settings: + if not settings and not auth_handler and not auth_handler_settings: raise ValueError( - "At least one of auth_handler or auth_handler_settings is required." + "At least one of settings or auth_handler or auth_handler_settings is required." + ) + if auth_handler or auth_handler_settings: + warnings.warn( + "auth_handler and auth_handler_settings are deprecated. Please use settings instead.", + DeprecationWarning, + stacklevel=2, ) self._storage = storage self._connection_manager = connection_manager - if auth_handler: - self._handler = auth_handler + if settings: + if isinstance(settings, dict): + settings = {**settings, **kwargs} + self._handler = AuthHandlerSettings(**settings) + else: + settings = {**settings._to_dict(), **kwargs} + self._handler = AuthHandlerSettings(**settings) + elif auth_handler: + settings = {**auth_handler._to_dict(), **kwargs} + self._handler = AuthHandlerSettings(**settings) else: if not auth_handler_settings: raise ValueError( "auth_handler_settings must be provided if auth_handler is None." ) + auth_handler_settings = {**auth_handler_settings, **kwargs} self._handler = AuthHandlerSettings._from_settings(auth_handler_settings) self._id = auth_handler_id or self._handler.name 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 3667dd6f5..f8e39d072 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 @@ -26,9 +26,10 @@ def __init__( self, storage: Storage, connection_manager: Connections, - auth_handler: AuthHandlerSettings | None = None, + settings: dict | AuthHandlerSettings | None = None, *, auth_handler_id: str | None = None, + auth_handler: AuthHandlerSettings | None = None, auth_handler_settings: dict | None = None, **kwargs, ) -> None: @@ -39,14 +40,21 @@ def __init__( :type storage: Storage :param connection_manager: The connection manager for OAuth providers. :type connection_manager: Connections - :param auth_handlers: Configuration for OAuth providers. - :type auth_handlers: dict[str, AuthHandler] | None + :param settings: Optional settings for the auth handler. + :type settings: dict | AuthHandlerSettings | None + :param auth_handler: (Deprecated) Configuration for OAuth provider. + :type auth_handler: AuthHandlerSettings | None + :param auth_handler_id: Optional ID of the auth handler. + :type auth_handler_id: str | None + :param auth_handler_settings: (Deprecated) Optional settings dict for the auth handler. + :type auth_handler_settings: dict | None :raises ValueError: When storage is None or no auth handlers provided. """ super().__init__( storage, connection_manager, - auth_handler, + settings, + auth_handler=auth_handler, auth_handler_id=auth_handler_id, auth_handler_settings=auth_handler_settings, **kwargs, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py index 93f613fdc..1682c51f1 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py @@ -31,9 +31,10 @@ def __init__( self, storage: Storage, connection_manager: Connections, - auth_handler: AuthHandlerSettings | None = None, + settings: dict | AuthHandlerSettings | None = None, *, auth_handler_id: str | None = None, + auth_handler: AuthHandlerSettings | None = None, auth_handler_settings: dict | None = None, **kwargs, ) -> None: @@ -44,17 +45,20 @@ def __init__( :type storage: Storage :param connection_manager: The connection manager for OAuth providers. :type connection_manager: Connections - :param auth_handler: Configuration for OAuth provider. + :param settings: Optional settings for the auth handler. + :type settings: dict | AuthHandlerSettings | None + :param auth_handler: (Deprecated) Configuration for OAuth provider. :type auth_handler: AuthHandlerSettings | None :param auth_handler_id: Optional ID of the auth handler. :type auth_handler_id: str | None - :param auth_handler_settings: Optional settings dict for the auth handler. + :param auth_handler_settings: (Deprecated) Optional settings dict for the auth handler. :type auth_handler_settings: dict | None """ super().__init__( storage, connection_manager, - auth_handler, + settings, + auth_handler=auth_handler, auth_handler_id=auth_handler_id, auth_handler_settings=auth_handler_settings, **kwargs, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py index 28f831594..f85052859 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py @@ -53,22 +53,23 @@ def __init__( to accept a class that implements AuthorizationVariant. :type auth_type: str """ - self.name = name or kwargs.get("NAME", "") - self.title = title or kwargs.get("TITLE", "") - self.text = text or kwargs.get("TEXT", "") + kwargs = {k.lower(): v for k, v in kwargs.items()} + self.name = name or kwargs.get("name", "") + self.title = title or kwargs.get("title", "") + self.text = text or kwargs.get("text", "") self.abs_oauth_connection_name = abs_oauth_connection_name or kwargs.get( - "AZUREBOTOAUTHCONNECTIONNAME", "" + "azurebotoauthconnectionname", "" ) self.obo_connection_name = obo_connection_name or kwargs.get( - "OBOCONNECTIONNAME", "" + "oboconnectionname", "" ) - self.auth_type = auth_type or kwargs.get("TYPE", "UserAuthorization") + self.auth_type = auth_type or kwargs.get("type", "UserAuthorization") self.auth_type = self.auth_type.lower() if scopes: self.scopes = list(scopes) else: - self.scopes = AuthHandlerSettings._format_scopes(kwargs.get("SCOPES", "")) - self._alt_blueprint_name = kwargs.get("ALT_BLUEPRINT_NAME", None) + self.scopes = AuthHandlerSettings._format_scopes(kwargs.get("scopes", "")) + self._alt_blueprint_name = kwargs.get("alt_blueprint_name", None) self._kwargs = kwargs @property @@ -94,16 +95,26 @@ def _from_settings(settings: dict): raise ValueError( "Settings dictionary is required to create AuthHandlerSettings" ) + return AuthHandlerSettings(**settings) - return AuthHandlerSettings( - name=settings.get("NAME", ""), - title=settings.get("TITLE", ""), - text=settings.get("TEXT", ""), - abs_oauth_connection_name=settings.get("AZUREBOTOAUTHCONNECTIONNAME", ""), - obo_connection_name=settings.get("OBOCONNECTIONNAME", ""), - auth_type=settings.get("TYPE", ""), - scopes=AuthHandlerSettings._format_scopes(settings.get("SCOPES", "")), - ) + def _to_dict(self) -> dict: + """ + Converts the AuthHandlerSettings instance to a dictionary. + + :return: A dictionary representation of the AuthHandlerSettings instance. + :rtype: dict + """ + base = { + "name": self.name, + "title": self.title, + "text": self.text, + "abs_oauth_connection_name": self.abs_oauth_connection_name, + "obo_connection_name": self.obo_connection_name, + "type": self.auth_type, + "scopes": self.scopes, + "alt_blueprint_name": self._alt_blueprint_name, + } + return {**self._kwargs, **base} class AuthHandler(AuthHandlerSettings): diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index 9de356c1c..0fc5c6244 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -134,15 +134,15 @@ def _init_handlers(self) -> None: :param auth_handlers: A dictionary of auth handler configurations. :type auth_handlers: dict[str, :class:`microsoft_agents.hosting.core.app.oauth.auth_handler.AuthHandlerSettings`] """ - for name, auth_handler in self._handler_settings.items(): - auth_type = auth_handler.auth_type + for name, settings in self._handler_settings.items(): + auth_type = settings.auth_type if auth_type not in AUTHORIZATION_TYPE_MAP: raise ValueError(f"Auth type {auth_type} not recognized.") self._handlers[name] = AUTHORIZATION_TYPE_MAP[auth_type]( storage=self._storage, connection_manager=self._connection_manager, - auth_handler=auth_handler, + settings=settings, ) @property diff --git a/test_samples/bizchat-sso/src/agent.py b/test_samples/bizchat-sso/src/agent.py index 8f19b1ec6..7417575ad 100644 --- a/test_samples/bizchat-sso/src/agent.py +++ b/test_samples/bizchat-sso/src/agent.py @@ -17,6 +17,9 @@ ) from microsoft_agents.hosting.aiohttp import CloudAdapter from microsoft_agents.hosting.core.app.oauth.authorization import Authorization +from microsoft_agents.hosting.testing.pytest_plugin import adapter + +from start_server import start_server logging.basicConfig(level=logging.INFO) load_dotenv(".env") @@ -78,6 +81,7 @@ async def on_message(context: TurnContext, _): try: start_server( agent_application=AGENT_APP, + adapter=ADAPTER, auth_configuration=CONNECTION_MANAGER.get_default_connection_configuration(), ) except Exception as error: 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 dec38b526..5c61cb4a6 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 @@ -45,17 +45,17 @@ def connection_manager(self, mocker): return MsalConnectionManager(**AGENTIC_ENV_DICT) @pytest.fixture - def auth_handler_settings(self): + def settings(self): return AGENTIC_ENV_DICT["AGENTAPPLICATION"]["USERAUTHORIZATION"]["HANDLERS"][ DEFAULTS.agentic_auth_handler_id ]["SETTINGS"] @pytest.fixture - def agentic_auth(self, storage, connection_manager, auth_handler_settings): + def agentic_auth(self, storage, connection_manager, settings): return AgenticUserAuthorization( storage, connection_manager, - auth_handler_settings=auth_handler_settings, + settings=settings, auth_handler_id=DEFAULTS.agentic_auth_handler_id, ) @@ -136,7 +136,7 @@ async def test_get_agentic_user_token_agentic_no_user_id( @pytest.mark.asyncio async def test_get_agentic_instance_token_is_agentic( - self, mocker, agentic_role, agentic_auth, auth_handler_settings + self, mocker, agentic_role, agentic_auth, settings ): mock_provider = self.mock_provider(mocker, instance_token=DEFAULTS.token) connection_manager = mocker.Mock(spec=MsalConnectionManager) @@ -145,7 +145,7 @@ async def test_get_agentic_instance_token_is_agentic( agentic_auth = AgenticUserAuthorization( MemoryStorage(), connection_manager, - auth_handler_settings=auth_handler_settings, + settings=settings, auth_handler_id=DEFAULTS.agentic_auth_handler_id, ) @@ -167,7 +167,7 @@ async def test_get_agentic_instance_token_is_agentic( @pytest.mark.asyncio async def test_get_agentic_user_token_is_agentic( - self, mocker, agentic_role, agentic_auth, auth_handler_settings + self, mocker, agentic_role, agentic_auth, settings ): mock_provider = self.mock_provider(mocker, user_token=DEFAULTS.token) @@ -177,7 +177,7 @@ async def test_get_agentic_user_token_is_agentic( agentic_auth = AgenticUserAuthorization( MemoryStorage(), connection_manager, - auth_handler_settings=auth_handler_settings, + settings=settings, auth_handler_id=DEFAULTS.agentic_auth_handler_id, ) @@ -212,7 +212,7 @@ async def test_sign_in_success( scopes_list, agentic_role, expected_scopes_list, - auth_handler_settings, + settings, ): mock_provider = self.mock_provider(mocker, user_token="my_token") @@ -222,7 +222,7 @@ async def test_sign_in_success( agentic_auth = AgenticUserAuthorization( MemoryStorage(), connection_manager, - auth_handler_settings=auth_handler_settings, + settings=settings, auth_handler_id=DEFAULTS.agentic_auth_handler_id, ) activity = Activity( @@ -260,7 +260,7 @@ async def test_sign_in_failure( scopes_list, agentic_role, expected_scopes_list, - auth_handler_settings, + settings, ): mock_provider = self.mock_provider(mocker, user_token=None) @@ -270,7 +270,7 @@ async def test_sign_in_failure( agentic_auth = AgenticUserAuthorization( MemoryStorage(), connection_manager, - auth_handler_settings=auth_handler_settings, + settings=settings, auth_handler_id=DEFAULTS.agentic_auth_handler_id, ) activity = Activity( @@ -308,7 +308,7 @@ async def test_get_refreshed_token_success( scopes_list, agentic_role, expected_scopes_list, - auth_handler_settings, + settings, ): mock_provider = self.mock_provider(mocker, user_token="my_token") @@ -318,7 +318,7 @@ async def test_get_refreshed_token_success( agentic_auth = AgenticUserAuthorization( MemoryStorage(), connection_manager, - auth_handler_settings=auth_handler_settings, + settings=settings, auth_handler_id=DEFAULTS.agentic_auth_handler_id, ) activity = Activity( @@ -357,7 +357,7 @@ async def test_get_refreshed_token_failure( scopes_list, agentic_role, expected_scopes_list, - auth_handler_settings, + settings, ): mock_provider = self.mock_provider(mocker, user_token=None) @@ -367,7 +367,7 @@ async def test_get_refreshed_token_failure( agentic_auth = AgenticUserAuthorization( MemoryStorage(), connection_manager, - auth_handler_settings=auth_handler_settings, + settings=settings, auth_handler_id=DEFAULTS.agentic_auth_handler_id, ) activity = Activity( 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 f66f46bb8..9564d0c9f 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 @@ -97,19 +97,17 @@ def connection_manager(self): return MsalConnectionManager(**AGENTIC_ENV_DICT) @pytest.fixture - def auth_handler_settings(self): + def settings(self): return AGENTIC_ENV_DICT["AGENTAPPLICATION"]["USERAUTHORIZATION"]["HANDLERS"][ DEFAULTS.auth_handler_id ]["SETTINGS"] @pytest.fixture - def connector_authorization( - self, connection_manager, storage, auth_handler_settings - ): + def connector_authorization(self, connection_manager, storage, settings): return ConnectorUserAuthorization( storage, connection_manager, - auth_handler_settings=auth_handler_settings, + settings=settings, auth_handler_id=DEFAULTS.auth_handler_id, ) 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 a7e805623..373d42a1a 100644 --- a/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py +++ b/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py @@ -114,17 +114,17 @@ def auth_handlers(self): return AUTH_TEST_DATA().auth_handlers @pytest.fixture - def auth_handler_settings(self): + def settings(self): return AGENTIC_ENV_DICT["AGENTAPPLICATION"]["USERAUTHORIZATION"]["HANDLERS"][ DEFAULTS.auth_handler_id ]["SETTINGS"] @pytest.fixture - def user_authorization(self, connection_manager, storage, auth_handler_settings): + def user_authorization(self, connection_manager, storage, settings): return MyUserAuthorization( storage, connection_manager, - auth_handler_settings=auth_handler_settings, + settings=settings, auth_handler_id=DEFAULTS.auth_handler_id, ) diff --git a/tests/hosting_core/app/_oauth/test_auth_handler.py b/tests/hosting_core/app/_oauth/test_auth_handler.py index 5b0e8bb5f..e3e63156e 100644 --- a/tests/hosting_core/app/_oauth/test_auth_handler.py +++ b/tests/hosting_core/app/_oauth/test_auth_handler.py @@ -1,6 +1,15 @@ import pytest -from microsoft_agents.hosting.core import AuthHandlerSettings +from microsoft_agents.hosting.core import ( + AuthHandler, + AuthHandlerSettings, + MemoryStorage, +) +from microsoft_agents.hosting.core.app.oauth import ( + AgenticUserAuthorization, + ConnectorUserAuthorization, + _UserAuthorization, +) from tests._common.data import ( DEFAULT_TEST_VALUES, @@ -49,3 +58,155 @@ def test_init_agentic(self, agentic_auth_setting): auth_handler.abs_oauth_connection_name == DEFAULTS.agentic_abs_oauth_connection_name ) + + @pytest.mark.parametrize("scopes", [[], ["User.Read"], ["User.Read", "Mail.Read"]]) + def test_settings_round_trip(self, scopes): + settings = AuthHandlerSettings( + name="SSO", + title="Chosen title", + TITLE="Fallback title", + scopes=scopes, + SCOPES=" ".join(scopes), + ALT_BLUEPRINT_NAME="blueprint", + CUSTOM_SETTING="custom", + ) + + copied = AuthHandlerSettings(**settings._to_dict()) + + assert copied.name == "SSO" + assert copied.title == "Chosen title" + assert copied.scopes == scopes + assert copied.scopes is not settings.scopes + assert copied._alt_blueprint_name == "blueprint" + assert copied.kwargs["custom_setting"] == "custom" + + def test_legacy_auth_handler(self, auth_setting): + with pytest.warns(DeprecationWarning, match="AuthHandler is deprecated"): + legacy = AuthHandler(DEFAULTS.auth_handler_id, **auth_setting) + + settings = AuthHandlerSettings(DEFAULTS.auth_handler_id, **auth_setting) + assert isinstance(legacy, AuthHandlerSettings) + assert legacy._to_dict() == settings._to_dict() + + +class TestAuthorizationHandlerSettings: + @pytest.fixture( + params=[ + _UserAuthorization, + AgenticUserAuthorization, + ConnectorUserAuthorization, + ] + ) + def handler_type(self, request): + return request.param + + @pytest.fixture + def configuration(self): + return { + "NAME": "SSO", + "TITLE": "Sign in", + "TEXT": "Please sign in", + "AZUREBOTOAUTHCONNECTIONNAME": "sso", + "OBOCONNECTIONNAME": "obo", + "TYPE": "AgenticUserAuthorization", + "SCOPES": "User.Read Mail.Read", + "ALT_BLUEPRINT_NAME": "blueprint", + "CUSTOM_SETTING": "custom", + } + + @pytest.fixture + def settings(self, configuration): + return AuthHandlerSettings(**configuration) + + @pytest.mark.parametrize("settings_kind", ["dictionary", "object"]) + def test_init_with_settings( + self, mocker, handler_type, configuration, settings, settings_kind + ): + storage = MemoryStorage() + connections = mocker.Mock() + source = configuration if settings_kind == "dictionary" else settings + + handler = handler_type(storage, connections, settings=source) + + assert handler._storage is storage + assert handler._connection_manager is connections + assert handler._id == "SSO" + assert handler._handler._to_dict() == settings._to_dict() + assert handler._handler.scopes == ["User.Read", "Mail.Read"] + if isinstance(handler, AgenticUserAuthorization): + assert handler._alt_blueprint_name == "blueprint" + + @pytest.mark.parametrize("settings_kind", ["dictionary", "object"]) + def test_settings_overrides( + self, mocker, handler_type, configuration, settings, settings_kind + ): + source = configuration if settings_kind == "dictionary" else settings + + handler = handler_type( + MemoryStorage(), + mocker.Mock(), + settings=source, + title="New title", + scopes=["Files.Read"], + obo_connection_name="new-obo", + alt_blueprint_name="new-blueprint", + ) + + assert handler._handler.title == "New title" + assert handler._handler.scopes == ["Files.Read"] + assert handler._handler.obo_connection_name == "new-obo" + assert handler._handler._alt_blueprint_name == "new-blueprint" + assert settings.title == "Sign in" + assert settings.scopes == ["User.Read", "Mail.Read"] + assert configuration["TITLE"] == "Sign in" + assert configuration["SCOPES"] == "User.Read Mail.Read" + + @pytest.mark.parametrize("argument", ["auth_handler", "auth_handler_settings"]) + def test_legacy_keyword_arguments( + self, mocker, handler_type, configuration, settings, argument + ): + source = settings if argument == "auth_handler" else configuration + + with pytest.warns(DeprecationWarning, match="Please use settings instead"): + handler = handler_type(MemoryStorage(), mocker.Mock(), **{argument: source}) + + assert handler._id == "SSO" + assert handler._handler._to_dict() == settings._to_dict() + assert handler._handler.scopes == ["User.Read", "Mail.Read"] + + @pytest.mark.parametrize("argument", ["positional", "auth_handler"]) + def test_legacy_auth_handler_instance( + self, mocker, handler_type, configuration, argument + ): + with pytest.warns(DeprecationWarning, match="AuthHandler is deprecated"): + legacy = AuthHandler(**configuration) + + if argument == "positional": + handler = handler_type(MemoryStorage(), mocker.Mock(), legacy) + else: + with pytest.warns(DeprecationWarning, match="Please use settings instead"): + handler = handler_type( + MemoryStorage(), mocker.Mock(), auth_handler=legacy + ) + + assert handler._id == "SSO" + assert handler._handler._to_dict() == legacy._to_dict() + assert handler._handler.scopes == ["User.Read", "Mail.Read"] + + def test_legacy_dictionary_with_explicit_id( + self, mocker, handler_type, configuration + ): + configuration.pop("NAME") + + with pytest.warns(DeprecationWarning, match="Please use settings instead"): + handler = handler_type( + MemoryStorage(), + mocker.Mock(), + auth_handler_settings=configuration, + auth_handler_id="legacy-id", + ) + + assert handler._id == "legacy-id" + assert handler._handler.abs_oauth_connection_name == "sso" + assert handler._handler.obo_connection_name == "obo" + assert handler._handler.scopes == ["User.Read", "Mail.Read"] diff --git a/tests/hosting_core/app/_oauth/test_authorization.py b/tests/hosting_core/app/_oauth/test_authorization.py index a1f0a16b3..711725236 100644 --- a/tests/hosting_core/app/_oauth/test_authorization.py +++ b/tests/hosting_core/app/_oauth/test_authorization.py @@ -17,11 +17,13 @@ _UserAuthorization, Authorization, AgenticUserAuthorization, + ConnectorUserAuthorization, ) from microsoft_agents.hosting.core._oauth import _FlowStateTag from microsoft_agents.hosting.core import ( + AuthHandler, AuthHandlerSettings, MemoryStorage, ) @@ -151,6 +153,37 @@ def auth_handler_id(self, request): class TestAuthorizationSetup(TestEnv): + @pytest.mark.parametrize( + "auth_type, handler_type", + [ + ("UserAuthorization", _UserAuthorization), + ("AgenticUserAuthorization", AgenticUserAuthorization), + ("ConnectorUserAuthorization", ConnectorUserAuthorization), + ], + ) + def test_legacy_auth_handler_configuration( + self, connection_manager, storage, auth_type, handler_type + ): + with pytest.warns(DeprecationWarning, match="AuthHandler is deprecated"): + settings = AuthHandler( + name="legacy", + auth_type=auth_type, + abs_oauth_connection_name="sso", + obo_connection_name="obo", + SCOPES="User.Read Mail.Read", + ) + + auth = Authorization( + storage, connection_manager, auth_handlers={"legacy": settings} + ) + handler = auth._resolve_handler("legacy") + + assert isinstance(handler, handler_type) + assert handler._id == "legacy" + assert handler._handler.abs_oauth_connection_name == "sso" + assert handler._handler.obo_connection_name == "obo" + assert handler._handler.scopes == ["User.Read", "Mail.Read"] + def test_init_user_auth(self, connection_manager, storage, env_dict): auth = Authorization(storage, connection_manager, **env_dict) assert auth._resolve_handler(DEFAULTS.auth_handler_id) is not None @@ -179,9 +212,15 @@ def test_resolve_handler(self, connection_manager, storage, auth_handler_id): handler_config = AGENTIC_ENV_DICT["AGENTAPPLICATION"]["USERAUTHORIZATION"][ "HANDLERS" ][auth_handler_id] - auth._resolve_handler(auth_handler_id) == AuthHandlerSettings( - auth_handler_id, **handler_config + settings = AuthHandlerSettings( + name=auth_handler_id, + auth_type=handler_config.get("TYPE", ""), + **handler_config["SETTINGS"], ) + handler = auth._resolve_handler(auth_handler_id) + assert handler._id == auth_handler_id + assert handler._handler._to_dict() == settings._to_dict() + assert handler._handler.scopes == settings.scopes def test_connection_manager_property(self, connection_manager, storage): auth = Authorization(storage, connection_manager, **ENV_DICT) From ad9e4908f85bd2d7ee66e7f035c07e49f550a5ec Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 19 Oct 2026 13:34:49 -0700 Subject: [PATCH 4/6] Removing files --- test_samples/bizchat-sso/src/agent.py | 88 -------------------- test_samples/bizchat-sso/src/main.py | 12 --- test_samples/bizchat-sso/src/start_server.py | 35 -------- 3 files changed, 135 deletions(-) delete mode 100644 test_samples/bizchat-sso/src/agent.py delete mode 100644 test_samples/bizchat-sso/src/main.py delete mode 100644 test_samples/bizchat-sso/src/start_server.py diff --git a/test_samples/bizchat-sso/src/agent.py b/test_samples/bizchat-sso/src/agent.py deleted file mode 100644 index 7417575ad..000000000 --- a/test_samples/bizchat-sso/src/agent.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging -from os import environ -from dotenv import load_dotenv -from microsoft_agents.activity import load_configuration_from_env -from microsoft_agents.authentication.msal import ( - MsalConnectionManager, -) -from microsoft_agents.hosting.core import ( - AgentApplication, - TurnState, - TurnContext, - MemoryStorage, - AuthHandlerSettings, -) -from microsoft_agents.hosting.aiohttp import CloudAdapter -from microsoft_agents.hosting.core.app.oauth.authorization import Authorization -from microsoft_agents.hosting.testing.pytest_plugin import adapter - -from start_server import start_server - -logging.basicConfig(level=logging.INFO) -load_dotenv(".env") - -agents_sdk_config = load_configuration_from_env(environ) - -STORAGE = MemoryStorage() -CONNECTION_MANAGER = MsalConnectionManager(**agents_sdk_config) -ADAPTER = CloudAdapter(connection_manager=CONNECTION_MANAGER) - -AUTHORIZATION = Authorization( - STORAGE, - CONNECTION_MANAGER, - auth_handlers={ - "SSO": AuthHandlerSettings( - name="SSO", - title="Sign in to the Agent", - text="Please sign in to continue", - abs_oauth_connection_name="sso", - ) - }, - **agents_sdk_config, -) - -AGENT_APP = AgentApplication[TurnState]( - storage=STORAGE, authorization=AUTHORIZATION, **agents_sdk_config -) - - -async def _help(context: TurnContext, _state: TurnState): - await context.send_activity( - "Welcome to the Echo Agent sample 🚀. " - "Type /help for help or send a message to see the echo feature in action." - ) - - -AGENT_APP.conversation_update("membersAdded")(_help) - -AGENT_APP.message("/help")(_help) - - -@AGENT_APP.message("/auth", auth_handlers=["SSO"]) -async def auth_message(context: TurnContext, _): - await context.send_activity("You are authenticated! 🎉") - - -@AGENT_APP.message("/logout") -async def logout(context: TurnContext, state: TurnState) -> None: - await AGENT_APP.auth.sign_out(context, "SSO") - await context.send_activity("You have been logged out.") - - -@AGENT_APP.activity("message") -async def on_message(context: TurnContext, _): - await context.send_activity(f"you said: {context.activity.text}") - - -if __name__ == "__main__": - try: - start_server( - agent_application=AGENT_APP, - adapter=ADAPTER, - auth_configuration=CONNECTION_MANAGER.get_default_connection_configuration(), - ) - except Exception as error: - raise error diff --git a/test_samples/bizchat-sso/src/main.py b/test_samples/bizchat-sso/src/main.py deleted file mode 100644 index fea459807..000000000 --- a/test_samples/bizchat-sso/src/main.py +++ /dev/null @@ -1,12 +0,0 @@ -from agent import AGENT_APP, CONNECTION_MANAGER, ADAPTER -from start_server import start_server - -if __name__ == "__main__": - try: - start_server( - agent_application=AGENT_APP, - adapter=ADAPTER, - auth_configuration=CONNECTION_MANAGER.get_default_connection_configuration(), - ) - except Exception as error: - raise error diff --git a/test_samples/bizchat-sso/src/start_server.py b/test_samples/bizchat-sso/src/start_server.py deleted file mode 100644 index 6235fe8ba..000000000 --- a/test_samples/bizchat-sso/src/start_server.py +++ /dev/null @@ -1,35 +0,0 @@ -from os import environ -from microsoft_agents.hosting.core import AgentApplication, AgentAuthConfiguration -from microsoft_agents.hosting.aiohttp import ( - start_agent_process, - jwt_authorization_middleware, - CloudAdapter, -) -from aiohttp.web import Request, Response, Application, run_app - - -def start_server( - agent_application: AgentApplication, - adapter: CloudAdapter, - auth_configuration: AgentAuthConfiguration -): - async def entry_point(req: Request) -> Response: - agent: AgentApplication = req.app["agent_app"] - adapter: CloudAdapter = req.app["adapter"] - return await start_agent_process( - req, - agent, - adapter, - ) - - APP = Application(middlewares=[jwt_authorization_middleware]) - APP.router.add_post("/api/messages", entry_point) - APP.router.add_get("/api/messages", lambda _: Response(status=200)) - APP["agent_configuration"] = auth_configuration - APP["agent_app"] = agent_application - APP["adapter"] = adapter - - try: - run_app(APP, host="localhost", port=environ.get("PORT", 3978)) - except Exception as error: - raise error From cf462500e2522fcbd66762fa671fb13b9b07d1a7 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 19 Oct 2026 16:21:59 -0700 Subject: [PATCH 5/6] Minor simplifications --- .../oauth/_handlers/_authorization_handler.py | 18 ++- .../hosting/core/app/oauth/auth_handler.py | 129 ++++++++++++---- .../hosting/core/app/oauth/authorization.py | 2 +- .../app/_oauth/test_auth_handler.py | 146 +++++++++++++++++- .../app/_oauth/test_authorization.py | 2 +- 5 files changed, 252 insertions(+), 45 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py index 21f02a169..bfd7c8568 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py @@ -72,21 +72,23 @@ def __init__( if settings: if isinstance(settings, dict): - settings = {**settings, **kwargs} - self._handler = AuthHandlerSettings(**settings) + self._handler = AuthHandlerSettings._from_settings(settings, **kwargs) else: - settings = {**settings._to_dict(), **kwargs} - self._handler = AuthHandlerSettings(**settings) + self._handler = AuthHandlerSettings._from_settings( + settings._to_dict(), **kwargs + ) elif auth_handler: - settings = {**auth_handler._to_dict(), **kwargs} - self._handler = AuthHandlerSettings(**settings) + self._handler = AuthHandlerSettings._from_settings( + auth_handler._to_dict(), **kwargs + ) else: if not auth_handler_settings: raise ValueError( "auth_handler_settings must be provided if auth_handler is None." ) - auth_handler_settings = {**auth_handler_settings, **kwargs} - self._handler = AuthHandlerSettings._from_settings(auth_handler_settings) + self._handler = AuthHandlerSettings._from_settings( + auth_handler_settings, **kwargs + ) self._id = auth_handler_id or self._handler.name if not self._id: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py index f85052859..412631c81 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py @@ -8,6 +8,17 @@ logger = logging.getLogger(__name__) +_BASE_KEYS = [ + "name", + "title", + "text", + "azurebotoauthconnectionname", + "oboconnectionname", + "type", + "scopes", + "alt_blueprint_name", +] + # name due to compat. # see AuthorizationHandler for a class that does work. @@ -26,12 +37,12 @@ class AuthHandlerSettings: def __init__( self, - name: str = "", - title: str = "", - text: str = "", - abs_oauth_connection_name: str = "", - obo_connection_name: str = "", - auth_type: str = "", + name: str | None = None, + title: str | None = None, + text: str | None = None, + abs_oauth_connection_name: str | None = None, + obo_connection_name: str | None = None, + auth_type: str | None = None, scopes: list[str] | None = None, **kwargs, ): @@ -53,36 +64,91 @@ def __init__( to accept a class that implements AuthorizationVariant. :type auth_type: str """ - kwargs = {k.lower(): v for k, v in kwargs.items()} - self.name = name or kwargs.get("name", "") - self.title = title or kwargs.get("title", "") - self.text = text or kwargs.get("text", "") - self.abs_oauth_connection_name = abs_oauth_connection_name or kwargs.get( - "azurebotoauthconnectionname", "" - ) - self.obo_connection_name = obo_connection_name or kwargs.get( - "oboconnectionname", "" + + base, extras = AuthHandlerSettings._resolve( + { + "name": name, + "title": title, + "text": text, + "azurebotoauthconnectionname": abs_oauth_connection_name, + "oboconnectionname": obo_connection_name, + "type": auth_type, + "scopes": scopes, + "alt_blueprint_name": kwargs.get("alt_blueprint_name", None), + }, + kwargs, ) - self.auth_type = auth_type or kwargs.get("type", "UserAuthorization") - self.auth_type = self.auth_type.lower() - if scopes: - self.scopes = list(scopes) - else: - self.scopes = AuthHandlerSettings._format_scopes(kwargs.get("scopes", "")) - self._alt_blueprint_name = kwargs.get("alt_blueprint_name", None) - self._kwargs = kwargs + + self.name = base.get("name", "") + self.title = base.get("title", "") + self.text = base.get("text", "") + self.abs_oauth_connection_name = base.get("azurebotoauthconnectionname", "") + self.obo_connection_name = base.get("oboconnectionname", "") + self.auth_type = base.get("type", "UserAuthorization").lower() + self.scopes = AuthHandlerSettings._format_scopes(base.get("scopes", "")) + self._alt_blueprint_name = base.get("alt_blueprint_name", None) + self._kwargs = extras + + @staticmethod + def _resolve(settings: dict, other: dict) -> tuple[dict, dict]: + + settings = {k.lower(): v for k, v in settings.items()} + other = {k.lower(): v for k, v in other.items()} + + if "abs_oauth_connection_name" in settings: + settings["azurebotoauthconnectionname"] = settings.pop( + "abs_oauth_connection_name" + ) + if "obo_connection_name" in settings: + settings["oboconnectionname"] = settings.pop("obo_connection_name") + if "auth_type" in settings: + settings["type"] = settings.pop("auth_type") + + if "abs_oauth_connection_name" in other: + other["azurebotoauthconnectionname"] = other.pop( + "abs_oauth_connection_name" + ) + if "obo_connection_name" in other: + other["oboconnectionname"] = other.pop("obo_connection_name") + if "auth_type" in other: + other["type"] = other.pop("auth_type") + + base = { + "name": settings.get("name") or other.get("name", ""), + "title": settings.get("title") or other.get("title", ""), + "text": settings.get("text") or other.get("text", ""), + "azurebotoauthconnectionname": settings.get("azurebotoauthconnectionname") + or other.get("azurebotoauthconnectionname", ""), + "oboconnectionname": settings.get("oboconnectionname") + or other.get("oboconnectionname", ""), + "type": settings.get("type") or other.get("type", "UserAuthorization"), + "scopes": settings.get("scopes") or other.get("scopes", ""), + "alt_blueprint_name": settings.get("alt_blueprint_name") + or other.get("alt_blueprint_name", None), + } + base_keys = base.keys() + + extras_base = {k: v for k, v in settings.items() if k not in base_keys} + extras_other = {k: v for k, v in other.items() if k not in base_keys} + extras = {**extras_other, **extras_base} + return base, extras @property def kwargs(self) -> dict: return dict(self._kwargs) @staticmethod - def _format_scopes(scopes: str) -> list[str]: - lst = scopes.strip().split(" ") - return [s for s in lst if s] + def _format_scopes(scopes: str | list[str]) -> list[str]: + if isinstance(scopes, str): + lst = scopes.strip().split(" ") + return [s for s in lst if s] + elif isinstance(scopes, list): + return list(scopes) + else: + return [] @staticmethod - def _from_settings(settings: dict): + def _from_settings(settings: dict, **kwargs): """ Creates an AuthHandlerSettings instance from a settings dictionary. @@ -95,7 +161,10 @@ def _from_settings(settings: dict): raise ValueError( "Settings dictionary is required to create AuthHandlerSettings" ) - return AuthHandlerSettings(**settings) + + base, extras = AuthHandlerSettings._resolve(settings, kwargs) + + return AuthHandlerSettings(**base, **extras) def _to_dict(self) -> dict: """ @@ -108,8 +177,8 @@ def _to_dict(self) -> dict: "name": self.name, "title": self.title, "text": self.text, - "abs_oauth_connection_name": self.abs_oauth_connection_name, - "obo_connection_name": self.obo_connection_name, + "azurebotoauthconnectionname": self.abs_oauth_connection_name, + "oboconnectionname": self.obo_connection_name, "type": self.auth_type, "scopes": self.scopes, "alt_blueprint_name": self._alt_blueprint_name, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index 0fc5c6244..6a4b30244 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -109,7 +109,7 @@ def __init__( auth_handlers = { handler_name: AuthHandlerSettings( name=handler_name, - auth_type=config.get("TYPE", ""), + auth_type=config.get("TYPE", None), **config.get("SETTINGS", {}), ) for handler_name, config in handlers_config.items() diff --git a/tests/hosting_core/app/_oauth/test_auth_handler.py b/tests/hosting_core/app/_oauth/test_auth_handler.py index e3e63156e..50bca3784 100644 --- a/tests/hosting_core/app/_oauth/test_auth_handler.py +++ b/tests/hosting_core/app/_oauth/test_auth_handler.py @@ -1,3 +1,5 @@ +from contextlib import nullcontext + import pytest from microsoft_agents.hosting.core import ( @@ -23,6 +25,27 @@ class TestAuthHandlerSettings: + def test_default_auth_type(self): + assert AuthHandlerSettings().auth_type == "userauthorization" + + def test_configuration_aliases(self): + settings = AuthHandlerSettings( + NAME="SSO", + TYPE="AgenticUserAuthorization", + AZUREBOTOAUTHCONNECTIONNAME="sso", + OBOCONNECTIONNAME="obo", + SCOPES="User.Read Mail.Read", + ) + + assert settings.name == "SSO" + assert settings.auth_type == "agenticuserauthorization" + assert settings.abs_oauth_connection_name == "sso" + assert settings.obo_connection_name == "obo" + assert settings.scopes == ["User.Read", "Mail.Read"] + + copied = AuthHandlerSettings(**settings._to_dict()) + assert copied._to_dict() == settings._to_dict() + @pytest.fixture def auth_setting(self): return ENV_DICT["AGENTAPPLICATION"]["USERAUTHORIZATION"]["HANDLERS"][ @@ -137,7 +160,7 @@ def test_init_with_settings( assert handler._alt_blueprint_name == "blueprint" @pytest.mark.parametrize("settings_kind", ["dictionary", "object"]) - def test_settings_overrides( + def test_settings_take_precedence( self, mocker, handler_type, configuration, settings, settings_kind ): source = configuration if settings_kind == "dictionary" else settings @@ -147,20 +170,133 @@ def test_settings_overrides( mocker.Mock(), settings=source, title="New title", + text="New text", scopes=["Files.Read"], + abs_oauth_connection_name="new-sso", obo_connection_name="new-obo", + auth_type="ConnectorUserAuthorization", alt_blueprint_name="new-blueprint", + CUSTOM_SETTING="other", ) - assert handler._handler.title == "New title" - assert handler._handler.scopes == ["Files.Read"] - assert handler._handler.obo_connection_name == "new-obo" - assert handler._handler._alt_blueprint_name == "new-blueprint" + assert handler._handler.title == "Sign in" + assert handler._handler.text == "Please sign in" + assert handler._handler.scopes == ["User.Read", "Mail.Read"] + assert handler._handler.abs_oauth_connection_name == "sso" + assert handler._handler.obo_connection_name == "obo" + assert handler._handler.auth_type == "agenticuserauthorization" + assert handler._handler._alt_blueprint_name == "blueprint" + assert handler._handler.kwargs["custom_setting"] == "custom" assert settings.title == "Sign in" assert settings.scopes == ["User.Read", "Mail.Read"] assert configuration["TITLE"] == "Sign in" assert configuration["SCOPES"] == "User.Read Mail.Read" + @pytest.mark.parametrize( + "source_kind", + ["dictionary", "object", "deprecated_dictionary", "deprecated_object"], + ) + def test_empty_arguments_preserve_settings( + self, mocker, handler_type, configuration, settings, source_kind + ): + source = configuration if "dictionary" in source_kind else settings + argument = "settings" + if source_kind == "deprecated_dictionary": + argument = "auth_handler_settings" + elif source_kind == "deprecated_object": + argument = "auth_handler" + + warning = ( + pytest.warns(DeprecationWarning, match="Please use settings instead") + if source_kind.startswith("deprecated") + else nullcontext() + ) + with warning: + handler = handler_type( + MemoryStorage(), + mocker.Mock(), + **{argument: source}, + title="", + text="", + abs_oauth_connection_name="", + obo_connection_name="", + scopes=[], + ) + + assert handler._handler.title == "Sign in" + assert handler._handler.text == "Please sign in" + assert handler._handler.abs_oauth_connection_name == "sso" + assert handler._handler.obo_connection_name == "obo" + assert handler._handler.scopes == ["User.Read", "Mail.Read"] + assert settings.obo_connection_name == "obo" + assert settings.scopes == ["User.Read", "Mail.Read"] + assert configuration["OBOCONNECTIONNAME"] == "obo" + assert configuration["SCOPES"] == "User.Read Mail.Read" + + @pytest.mark.parametrize("settings_kind", ["dictionary", "object"]) + @pytest.mark.parametrize("empty_value", [None, ""]) + def test_other_arguments_fill_empty_settings( + self, mocker, handler_type, configuration, settings_kind, empty_value + ): + for key in [ + "TITLE", + "TEXT", + "AZUREBOTOAUTHCONNECTIONNAME", + "OBOCONNECTIONNAME", + "ALT_BLUEPRINT_NAME", + ]: + configuration[key] = empty_value + configuration["SCOPES"] = [] if empty_value == "" else None + source = ( + configuration + if settings_kind == "dictionary" + else AuthHandlerSettings(**configuration) + ) + + handler = handler_type( + MemoryStorage(), + mocker.Mock(), + settings=source, + title="Fallback title", + text="Fallback text", + abs_oauth_connection_name="fallback-sso", + obo_connection_name="fallback-obo", + scopes=["Files.Read"], + alt_blueprint_name="fallback-blueprint", + OTHER_SETTING="other", + ) + + assert handler._handler.title == "Fallback title" + assert handler._handler.text == "Fallback text" + assert handler._handler.abs_oauth_connection_name == "fallback-sso" + assert handler._handler.obo_connection_name == "fallback-obo" + assert handler._handler.scopes == ["Files.Read"] + assert handler._handler._alt_blueprint_name == "fallback-blueprint" + assert handler._handler.kwargs["custom_setting"] == "custom" + assert handler._handler.kwargs["other_setting"] == "other" + + @pytest.mark.parametrize("argument", ["settings", "auth_handler"]) + def test_cleared_settings_survive_reconstruction( + self, mocker, handler_type, settings, argument + ): + settings.abs_oauth_connection_name = "" + settings.obo_connection_name = "" + settings.scopes = [] + + warning = ( + pytest.warns(DeprecationWarning, match="Please use settings instead") + if argument == "auth_handler" + else nullcontext() + ) + with warning: + handler = handler_type( + MemoryStorage(), mocker.Mock(), **{argument: settings} + ) + + assert handler._handler.abs_oauth_connection_name == "" + assert handler._handler.obo_connection_name == "" + assert handler._handler.scopes == [] + @pytest.mark.parametrize("argument", ["auth_handler", "auth_handler_settings"]) def test_legacy_keyword_arguments( self, mocker, handler_type, configuration, settings, argument diff --git a/tests/hosting_core/app/_oauth/test_authorization.py b/tests/hosting_core/app/_oauth/test_authorization.py index 711725236..74b773153 100644 --- a/tests/hosting_core/app/_oauth/test_authorization.py +++ b/tests/hosting_core/app/_oauth/test_authorization.py @@ -214,7 +214,7 @@ def test_resolve_handler(self, connection_manager, storage, auth_handler_id): ][auth_handler_id] settings = AuthHandlerSettings( name=auth_handler_id, - auth_type=handler_config.get("TYPE", ""), + auth_type=handler_config.get("TYPE"), **handler_config["SETTINGS"], ) handler = auth._resolve_handler(auth_handler_id) From d8aa60a1e3d64641e35231787781b0d3ad06dfff Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 19 Oct 2026 16:22:32 -0700 Subject: [PATCH 6/6] Another commit --- .../oauth/_handlers/_authorization_handler.py | 18 +- .../hosting/core/app/oauth/auth_handler.py | 105 ++------- .../app/_oauth/test_auth_handler.py | 214 ++++++++++-------- 3 files changed, 152 insertions(+), 185 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py index bfd7c8568..702a43b58 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py @@ -72,23 +72,17 @@ def __init__( if settings: if isinstance(settings, dict): - self._handler = AuthHandlerSettings._from_settings(settings, **kwargs) + self._handler = AuthHandlerSettings(**settings) else: - self._handler = AuthHandlerSettings._from_settings( - settings._to_dict(), **kwargs - ) - elif auth_handler: - self._handler = AuthHandlerSettings._from_settings( - auth_handler._to_dict(), **kwargs - ) - else: + self._handler = settings + elif auth_handler: # deprecated + self._handler = auth_handler + else: # deprecated if not auth_handler_settings: raise ValueError( "auth_handler_settings must be provided if auth_handler is None." ) - self._handler = AuthHandlerSettings._from_settings( - auth_handler_settings, **kwargs - ) + self._handler = AuthHandlerSettings(**auth_handler_settings) self._id = auth_handler_id or self._handler.name if not self._id: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py index 412631c81..bf75fd0b4 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py @@ -63,75 +63,31 @@ def __init__( :param auth_type: The authorization variant used. This is likely to change in the future to accept a class that implements AuthorizationVariant. :type auth_type: str + :param scopes: List of scopes to request during sign-in. + :type scopes: list[str] | None + :param kwargs: Additional keyword arguments for the OAuth handler. + _alt_blueprint_name: Alternative blueprint name for the OAuth handler. + :type kwargs: dict """ - base, extras = AuthHandlerSettings._resolve( - { - "name": name, - "title": title, - "text": text, - "azurebotoauthconnectionname": abs_oauth_connection_name, - "oboconnectionname": obo_connection_name, - "type": auth_type, - "scopes": scopes, - "alt_blueprint_name": kwargs.get("alt_blueprint_name", None), - }, - kwargs, - ) - - self.name = base.get("name", "") - self.title = base.get("title", "") - self.text = base.get("text", "") - self.abs_oauth_connection_name = base.get("azurebotoauthconnectionname", "") - self.obo_connection_name = base.get("oboconnectionname", "") - self.auth_type = base.get("type", "UserAuthorization").lower() - self.scopes = AuthHandlerSettings._format_scopes(base.get("scopes", "")) - self._alt_blueprint_name = base.get("alt_blueprint_name", None) - self._kwargs = extras - - @staticmethod - def _resolve(settings: dict, other: dict) -> tuple[dict, dict]: - - settings = {k.lower(): v for k, v in settings.items()} - other = {k.lower(): v for k, v in other.items()} - - if "abs_oauth_connection_name" in settings: - settings["azurebotoauthconnectionname"] = settings.pop( - "abs_oauth_connection_name" - ) - if "obo_connection_name" in settings: - settings["oboconnectionname"] = settings.pop("obo_connection_name") - if "auth_type" in settings: - settings["type"] = settings.pop("auth_type") - - if "abs_oauth_connection_name" in other: - other["azurebotoauthconnectionname"] = other.pop( - "abs_oauth_connection_name" - ) - if "obo_connection_name" in other: - other["oboconnectionname"] = other.pop("obo_connection_name") - if "auth_type" in other: - other["type"] = other.pop("auth_type") + kwargs = {k.lower(): v for k, v in kwargs.items()} - base = { - "name": settings.get("name") or other.get("name", ""), - "title": settings.get("title") or other.get("title", ""), - "text": settings.get("text") or other.get("text", ""), - "azurebotoauthconnectionname": settings.get("azurebotoauthconnectionname") - or other.get("azurebotoauthconnectionname", ""), - "oboconnectionname": settings.get("oboconnectionname") - or other.get("oboconnectionname", ""), - "type": settings.get("type") or other.get("type", "UserAuthorization"), - "scopes": settings.get("scopes") or other.get("scopes", ""), - "alt_blueprint_name": settings.get("alt_blueprint_name") - or other.get("alt_blueprint_name", None), - } - base_keys = base.keys() + self.name = name or kwargs.get("name", "") + self.title = title or kwargs.get("title", "") + self.text = text or kwargs.get("text", "") + self.abs_oauth_connection_name = abs_oauth_connection_name or kwargs.get( + "azurebotoauthconnectionname", "" + ) + self.obo_connection_name = obo_connection_name or kwargs.get( + "oboconnectionname", "" + ) + self.auth_type = (auth_type or kwargs.get("type", "UserAuthorization")).lower() + self.scopes = AuthHandlerSettings._format_scopes( + scopes or kwargs.get("scopes", "") + ) + self._alt_blueprint_name = kwargs.get("alt_blueprint_name", None) - extras_base = {k: v for k, v in settings.items() if k not in base_keys} - extras_other = {k: v for k, v in other.items() if k not in base_keys} - extras = {**extras_other, **extras_base} - return base, extras + self._kwargs = {k: v for k, v in kwargs.items() if k not in _BASE_KEYS} @property def kwargs(self) -> dict: @@ -147,25 +103,6 @@ def _format_scopes(scopes: str | list[str]) -> list[str]: else: return [] - @staticmethod - def _from_settings(settings: dict, **kwargs): - """ - Creates an AuthHandlerSettings instance from a settings dictionary. - - :param settings: The settings dictionary containing configuration for the AuthHandlerSettings. - :type settings: dict - :return: An instance of AuthHandlerSettings configured with the provided settings. - :rtype: AuthHandlerSettings - """ - if not settings: - raise ValueError( - "Settings dictionary is required to create AuthHandlerSettings" - ) - - base, extras = AuthHandlerSettings._resolve(settings, kwargs) - - return AuthHandlerSettings(**base, **extras) - def _to_dict(self) -> dict: """ Converts the AuthHandlerSettings instance to a dictionary. diff --git a/tests/hosting_core/app/_oauth/test_auth_handler.py b/tests/hosting_core/app/_oauth/test_auth_handler.py index 50bca3784..b6ead21b0 100644 --- a/tests/hosting_core/app/_oauth/test_auth_handler.py +++ b/tests/hosting_core/app/_oauth/test_auth_handler.py @@ -28,23 +28,85 @@ class TestAuthHandlerSettings: def test_default_auth_type(self): assert AuthHandlerSettings().auth_type == "userauthorization" - def test_configuration_aliases(self): + @pytest.mark.parametrize("key_style", ["upper", "lower", "snake"]) + def test_configuration_aliases(self, key_style): + configuration = { + "NAME": "SSO", + "TYPE": "AgenticUserAuthorization", + "AZUREBOTOAUTHCONNECTIONNAME": "sso", + "OBOCONNECTIONNAME": "obo", + "SCOPES": "User.Read Mail.Read", + } + if key_style != "upper": + configuration = {k.lower(): v for k, v in configuration.items()} + if key_style == "snake": + configuration["auth_type"] = configuration.pop("type") + configuration["abs_oauth_connection_name"] = configuration.pop( + "azurebotoauthconnectionname" + ) + configuration["obo_connection_name"] = configuration.pop( + "oboconnectionname" + ) + configuration["scopes"] = ["User.Read", "Mail.Read"] + settings = AuthHandlerSettings(**configuration) + + assert settings.name == "SSO" + assert settings.auth_type == "agenticuserauthorization" + assert settings.abs_oauth_connection_name == "sso" + assert settings.obo_connection_name == "obo" + assert settings.scopes == ["User.Read", "Mail.Read"] + + copied = AuthHandlerSettings(**settings._to_dict()) + assert copied._to_dict() == settings._to_dict() + + @pytest.mark.parametrize("scopes_key", ["scopes", "SCOPES"]) + def test_scopes_are_copied(self, scopes_key): + scopes = ["User.Read"] + settings = AuthHandlerSettings(**{scopes_key: scopes}) + + scopes.append("Mail.Read") + + assert settings.scopes == ["User.Read"] + + def test_kwargs_returns_a_copy(self): + settings = AuthHandlerSettings(CUSTOM_SETTING="custom") + extras = settings.kwargs + + extras["custom_setting"] = "changed" + extras["new_setting"] = "new" + + assert settings.kwargs == {"custom_setting": "custom"} + + @pytest.mark.parametrize("empty_value", [None, ""]) + def test_empty_arguments_use_configuration(self, empty_value): settings = AuthHandlerSettings( + name=empty_value, + title=empty_value, + text=empty_value, + abs_oauth_connection_name=empty_value, + obo_connection_name=empty_value, + auth_type=empty_value, + scopes=None if empty_value is None else [], NAME="SSO", - TYPE="AgenticUserAuthorization", + TITLE="Sign in", + TEXT="Please sign in", AZUREBOTOAUTHCONNECTIONNAME="sso", OBOCONNECTIONNAME="obo", + TYPE="AgenticUserAuthorization", SCOPES="User.Read Mail.Read", + ALT_BLUEPRINT_NAME="blueprint", + CUSTOM_SETTING="custom", ) assert settings.name == "SSO" - assert settings.auth_type == "agenticuserauthorization" + assert settings.title == "Sign in" + assert settings.text == "Please sign in" assert settings.abs_oauth_connection_name == "sso" assert settings.obo_connection_name == "obo" + assert settings.auth_type == "agenticuserauthorization" assert settings.scopes == ["User.Read", "Mail.Read"] - - copied = AuthHandlerSettings(**settings._to_dict()) - assert copied._to_dict() == settings._to_dict() + assert settings._alt_blueprint_name == "blueprint" + assert settings.kwargs == {"custom_setting": "custom"} @pytest.fixture def auth_setting(self): @@ -156,28 +218,44 @@ def test_init_with_settings( assert handler._id == "SSO" assert handler._handler._to_dict() == settings._to_dict() assert handler._handler.scopes == ["User.Read", "Mail.Read"] + if settings_kind == "object": + assert handler._handler is settings if isinstance(handler, AgenticUserAuthorization): assert handler._alt_blueprint_name == "blueprint" - @pytest.mark.parametrize("settings_kind", ["dictionary", "object"]) - def test_settings_take_precedence( + @pytest.mark.parametrize( + "settings_kind", + ["dictionary", "object", "deprecated_dictionary", "deprecated_object"], + ) + def test_handler_kwargs_do_not_modify_settings( self, mocker, handler_type, configuration, settings, settings_kind ): - source = configuration if settings_kind == "dictionary" else settings + source = configuration if "dictionary" in settings_kind else settings + argument = { + "deprecated_dictionary": "auth_handler_settings", + "deprecated_object": "auth_handler", + }.get(settings_kind, "settings") - handler = handler_type( - MemoryStorage(), - mocker.Mock(), - settings=source, - title="New title", - text="New text", - scopes=["Files.Read"], - abs_oauth_connection_name="new-sso", - obo_connection_name="new-obo", - auth_type="ConnectorUserAuthorization", - alt_blueprint_name="new-blueprint", - CUSTOM_SETTING="other", + warning = ( + pytest.warns(DeprecationWarning, match="Please use settings instead") + if settings_kind.startswith("deprecated") + else nullcontext() ) + with warning: + handler = handler_type( + MemoryStorage(), + mocker.Mock(), + **{argument: source}, + title="New title", + text="New text", + scopes=["Files.Read"], + abs_oauth_connection_name="new-sso", + obo_connection_name="new-obo", + auth_type="ConnectorUserAuthorization", + alt_blueprint_name="new-blueprint", + CUSTOM_SETTING="other", + OTHER_SETTING="ignored", + ) assert handler._handler.title == "Sign in" assert handler._handler.text == "Please sign in" @@ -187,28 +265,31 @@ def test_settings_take_precedence( assert handler._handler.auth_type == "agenticuserauthorization" assert handler._handler._alt_blueprint_name == "blueprint" assert handler._handler.kwargs["custom_setting"] == "custom" + assert "other_setting" not in handler._handler.kwargs assert settings.title == "Sign in" assert settings.scopes == ["User.Read", "Mail.Read"] assert configuration["TITLE"] == "Sign in" assert configuration["SCOPES"] == "User.Read Mail.Read" @pytest.mark.parametrize( - "source_kind", + "settings_kind", ["dictionary", "object", "deprecated_dictionary", "deprecated_object"], ) - def test_empty_arguments_preserve_settings( - self, mocker, handler_type, configuration, settings, source_kind - ): - source = configuration if "dictionary" in source_kind else settings - argument = "settings" - if source_kind == "deprecated_dictionary": - argument = "auth_handler_settings" - elif source_kind == "deprecated_object": - argument = "auth_handler" + def test_missing_optional_settings(self, mocker, handler_type, settings_kind): + configuration = {"NAME": "SSO"} + source = ( + configuration + if "dictionary" in settings_kind + else AuthHandlerSettings(**configuration) + ) + argument = { + "deprecated_dictionary": "auth_handler_settings", + "deprecated_object": "auth_handler", + }.get(settings_kind, "settings") warning = ( pytest.warns(DeprecationWarning, match="Please use settings instead") - if source_kind.startswith("deprecated") + if settings_kind.startswith("deprecated") else nullcontext() ) with warning: @@ -216,67 +297,20 @@ def test_empty_arguments_preserve_settings( MemoryStorage(), mocker.Mock(), **{argument: source}, - title="", - text="", - abs_oauth_connection_name="", - obo_connection_name="", - scopes=[], ) - assert handler._handler.title == "Sign in" - assert handler._handler.text == "Please sign in" - assert handler._handler.abs_oauth_connection_name == "sso" - assert handler._handler.obo_connection_name == "obo" - assert handler._handler.scopes == ["User.Read", "Mail.Read"] - assert settings.obo_connection_name == "obo" - assert settings.scopes == ["User.Read", "Mail.Read"] - assert configuration["OBOCONNECTIONNAME"] == "obo" - assert configuration["SCOPES"] == "User.Read Mail.Read" - - @pytest.mark.parametrize("settings_kind", ["dictionary", "object"]) - @pytest.mark.parametrize("empty_value", [None, ""]) - def test_other_arguments_fill_empty_settings( - self, mocker, handler_type, configuration, settings_kind, empty_value - ): - for key in [ - "TITLE", - "TEXT", - "AZUREBOTOAUTHCONNECTIONNAME", - "OBOCONNECTIONNAME", - "ALT_BLUEPRINT_NAME", - ]: - configuration[key] = empty_value - configuration["SCOPES"] = [] if empty_value == "" else None - source = ( - configuration - if settings_kind == "dictionary" - else AuthHandlerSettings(**configuration) - ) - - handler = handler_type( - MemoryStorage(), - mocker.Mock(), - settings=source, - title="Fallback title", - text="Fallback text", - abs_oauth_connection_name="fallback-sso", - obo_connection_name="fallback-obo", - scopes=["Files.Read"], - alt_blueprint_name="fallback-blueprint", - OTHER_SETTING="other", - ) - - assert handler._handler.title == "Fallback title" - assert handler._handler.text == "Fallback text" - assert handler._handler.abs_oauth_connection_name == "fallback-sso" - assert handler._handler.obo_connection_name == "fallback-obo" - assert handler._handler.scopes == ["Files.Read"] - assert handler._handler._alt_blueprint_name == "fallback-blueprint" - assert handler._handler.kwargs["custom_setting"] == "custom" - assert handler._handler.kwargs["other_setting"] == "other" + assert handler._id == "SSO" + assert handler._handler.title == "" + assert handler._handler.text == "" + assert handler._handler.abs_oauth_connection_name == "" + assert handler._handler.obo_connection_name == "" + assert handler._handler.auth_type == "userauthorization" + assert handler._handler.scopes == [] + assert handler._handler._alt_blueprint_name is None + assert handler._handler.kwargs == {} @pytest.mark.parametrize("argument", ["settings", "auth_handler"]) - def test_cleared_settings_survive_reconstruction( + def test_modified_settings_object_is_preserved( self, mocker, handler_type, settings, argument ): settings.abs_oauth_connection_name = "" @@ -293,6 +327,7 @@ def test_cleared_settings_survive_reconstruction( MemoryStorage(), mocker.Mock(), **{argument: settings} ) + assert handler._handler is settings assert handler._handler.abs_oauth_connection_name == "" assert handler._handler.obo_connection_name == "" assert handler._handler.scopes == [] @@ -326,6 +361,7 @@ def test_legacy_auth_handler_instance( ) assert handler._id == "SSO" + assert handler._handler is legacy assert handler._handler._to_dict() == legacy._to_dict() assert handler._handler.scopes == ["User.Read", "Mail.Read"]