From bee806a13b83c45af7a7d222c9fd095f32fc8aad Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 4 Nov 2026 15:13:53 -0800 Subject: [PATCH 1/5] First commit --- .../authentication/msal/msal_auth.py | 1 - .../microsoft_agents/hosting/core/__init__.py | 4 +- .../hosting/core/app/__init__.py | 4 +- .../hosting/core/app/app_options.py | 5 +- .../oauth/_handlers/_authorization_handler.py | 52 ++++--- .../oauth/_handlers/_user_authorization.py | 2 +- .../_handlers/agentic_user_authorization.py | 36 ++--- .../_handlers/connector_user_authorization.py | 36 ++--- .../hosting/core/app/oauth/auth_handler.py | 135 ++++++++---------- .../hosting/core/app/oauth/authorization.py | 16 +-- 10 files changed, 151 insertions(+), 140 deletions(-) diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py index 1fdb2afce..2307c91e9 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py @@ -10,7 +10,6 @@ import jwt import warnings -from typing import Any from urllib.parse import urlparse, ParseResult as URI from msal import ( ConfidentialClientApplication, 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 310487196..b8017aa06 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 @@ -42,7 +42,7 @@ # App Auth from .app.oauth import ( Authorization, - AuthHandler, + AuthHandlerSettings, AgenticUserAuthorization, ) @@ -155,7 +155,7 @@ "TurnState", "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..a8736ed7c 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 @@ -17,7 +17,7 @@ # Auth from .oauth import ( Authorization, - AuthHandler, + AuthHandlerSettings, AgenticUserAuthorization, ) @@ -58,7 +58,7 @@ "TurnState", "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..96eff2416 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,8 +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.authorization import Connections +from microsoft_agents.hosting.core.app.oauth import AuthHandlerSettings from microsoft_agents.hosting.core.storage import Storage # from .auth import AuthOptions @@ -81,7 +80,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/_handlers/_authorization_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py index 99b9760ad..9ab180070 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,18 @@ Licensed under the MIT License. """ -from abc import ABC -from typing import Optional import logging +import warnings + +from abc import ABC +from typing import Mapping 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 +25,17 @@ 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, + settings: Mapping | AuthHandlerSettings | None = None, *, - auth_handler_id: Optional[str] = None, - auth_handler_settings: Optional[dict] = None, + auth_handler_id: str | None = None, + auth_handler: AuthHandlerSettings | None = None, + auth_handler_settings: dict | None = None, **kwargs, ) -> None: """ @@ -42,12 +45,27 @@ 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], Optional - :raises ValueError: When storage is None or no auth handlers provided. + :param settings: Optional settings for the auth handler. + :type settings: Mapping | AuthHandlerSettings | None + :param auth_handler_id: Optional ID of the auth handler to use. + :type auth_handler_id: str | None + :param auth_handler: Optional auth handler settings. + :type auth_handler: AuthHandlerSettings | None + :param auth_handler_settings: Optional dictionary of auth handler settings. + :type auth_handler_settings: dict | None """ if not storage: raise ValueError("Storage is required for Authorization") + if not settings and not auth_handler and not auth_handler_settings: + raise ValueError( + "At least one of settings, 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, + ) if not auth_handler and not auth_handler_settings: raise ValueError( "At least one of auth_handler or auth_handler_settings is required." @@ -56,14 +74,16 @@ def __init__( self._storage = storage self._connection_manager = connection_manager - if auth_handler: + if settings: + self._handler = AuthHandlerSettings.from_mapping(settings) + elif auth_handler: # deprecated self._handler = auth_handler - else: + else: # deprecated if not auth_handler_settings: 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_mapping(auth_handler_settings) self._id = auth_handler_id or self._handler.name if not self._id: @@ -72,7 +92,7 @@ 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. @@ -88,8 +108,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: """Attempts to get a refreshed token for the user with the given scopes diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py index 50fcaae1c..5fbd7b408 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py @@ -4,8 +4,8 @@ """ from __future__ import annotations + import logging -from typing import cast from microsoft_agents.activity import ( Activity, 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..3bb69edbd 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,7 +5,7 @@ import logging -from typing import Optional +from typing import Mapping from microsoft_agents.activity import TokenResponse @@ -28,10 +28,11 @@ def __init__( self, storage: Storage, connection_manager: Connections, - auth_handler: Optional[AuthHandler] = None, + settings: Mapping | AuthHandlerSettings | None = None, *, - auth_handler_id: Optional[str] = None, - auth_handler_settings: Optional[dict] = None, + auth_handler_id: str | None = None, + auth_handler: AuthHandler | None = None, + auth_handler_settings: dict | None = None, **kwargs, ) -> None: """ @@ -41,21 +42,20 @@ 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], Optional + :param auth_handler: Configuration for OAuth provider. + :type auth_handler: AuthHandler | 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, ) - self._alt_blueprint_name = ( - auth_handler._alt_blueprint_name if auth_handler else None - ) + self._alt_blueprint_name = self._handler.alt_blueprint_name # TODO async def get_agentic_instance_token(self, context: TurnContext) -> TokenResponse: """Gets the agentic instance token for the current agent instance. @@ -163,17 +163,17 @@ 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. :param context: The context object for the current turn. :type context: TurnContext - :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]] + :param exchange_connection: The name of the connection to use for sign-in. + :type exchange_connection: str | None + :param exchange_scopes: The scopes to request for the token. + :type exchange_scopes: Optional[list[str]] :return: A _SignInResponse containing the token response and flow state tag. :rtype: _SignInResponse """ @@ -189,8 +189,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: """Attempts to get a refreshed token for the user with the given scopes 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 523ee9bff..39c4a7af5 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,7 @@ import logging import jwt from datetime import datetime, timezone -from typing import Optional +from typing import Mapping from microsoft_agents.activity import TokenResponse from microsoft_agents.hosting.core.errors import ErrorResources @@ -15,7 +15,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 +32,11 @@ def __init__( self, storage: Storage, connection_manager: Connections, - auth_handler: Optional[AuthHandler] = None, + settings: Mapping | AuthHandlerSettings | None = None, *, - auth_handler_id: Optional[str] = None, - auth_handler_settings: Optional[dict] = None, + auth_handler_id: str | None = None, + auth_handler: AuthHandlerSettings | None = None, + auth_handler_settings: dict | None = None, **kwargs, ) -> None: """ @@ -46,7 +47,7 @@ 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: , Optional :param auth_handler_id: Optional ID of the auth handler. :type auth_handler_id: str, Optional :param auth_handler_settings: Optional settings dict for the auth handler. @@ -55,7 +56,8 @@ def __init__( 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, @@ -64,8 +66,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 +76,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 +91,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. @@ -148,8 +150,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. @@ -159,9 +161,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 619c36249..b179c5e66 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 @@ -3,15 +3,57 @@ Licensed under the MIT License. """ +from __future__ import annotations + import logging -from typing import Optional +import warnings + +from dataclasses import dataclass, field + +from microsoft_agents.activity.config import ( + _Configurable, + Configuration, + xpath, +) logger = logging.getLogger(__name__) -# name due to compat. -# see AuthorizationHandler for a class that does work. -class AuthHandler: +def _or_default(s: str | None, default: str) -> str: + return (s.strip() if s else None) or default + + +def _create_kwargs( + 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, +) -> dict: + c = Configuration(kwargs) + return dict( + name=_or_default(name, c.get_str(xpath("name"), default="")), + title=_or_default(title, c.get_str(xpath("title"), default="")), + text=_or_default(text, c.get_str(xpath("text"), default="")), + abs_oauth_connection_name=_or_default( + abs_oauth_connection_name, + c.get_str(xpath("azurebotoauthconnectionname"), default=""), + ), + obo_connection_name=_or_default( + obo_connection_name, c.get_str(xpath("oboconnectionname"), default="") + ), + auth_type=_or_default( + auth_type, c.get_str(xpath("type"), default="UserAuthorization") + ).lower(), + scopes=Configuration.format_scopes(scopes or c.get(xpath("scopes"))), + ) + + +@dataclass +class AuthHandlerSettings(_Configurable): """ Interface defining an authorization handler for OAuth flows. """ @@ -22,53 +64,11 @@ class AuthHandler: abs_oauth_connection_name: str obo_connection_name: str auth_type: str - scopes: list[str] - - def __init__( - self, - name: str = "", - title: str = "", - text: str = "", - abs_oauth_connection_name: str = "", - obo_connection_name: str = "", - auth_type: str = "", - scopes: Optional[list[str]] = None, - **kwargs, - ): - """ - Initializes a new instance of AuthHandler. - - :param name: The name of the handler. This is how it is accessed programatically - in this library. - :type name: str - :param title: Title for the OAuth card. - :type title: str - :param text: Text for the OAuth button. - :type text: str - :param abs_oauth_connection_name: The name of the Azure Bot Service OAuth connection. - :type abs_oauth_connection_name: str - :param obo_connection_name: The name of the On-Behalf-Of connection. - :type obo_connection_name: str - :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 - """ - 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") - self.auth_type = self.auth_type.lower() - if scopes: - self.scopes = list(scopes) - else: - self.scopes = AuthHandler._format_scopes(kwargs.get("SCOPES", "")) - self._alt_blueprint_name = kwargs.get("ALT_BLUEPRINT_NAME", None) + scopes: list[str] = field(default_factory=list) + + @classmethod + def _from_configuration(cls, config: Configuration) -> AuthHandlerSettings: + return AuthHandlerSettings(**_create_kwargs(**config)) @staticmethod def _format_scopes(scopes: str | list[str]) -> list[str]: @@ -78,25 +78,16 @@ def _format_scopes(scopes: str | list[str]) -> list[str]: lst = scopes return [s for s in lst if s] - @staticmethod - def _from_settings(settings: dict): - """ - Creates an AuthHandler instance from a settings dictionary. - - :param settings: The settings dictionary containing configuration for the AuthHandler. - :type settings: dict - :return: An instance of AuthHandler configured with the provided settings. - :rtype: AuthHandler - """ - if not settings: - raise ValueError("Settings dictionary is required to create AuthHandler") - - return AuthHandler( - 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", "")), + +class AuthHandler(AuthHandlerSettings): + """Deprecated. Use AuthHandlerSettings instead.""" + + def __init__(self, **kwargs): + """Initialize the deprecated AuthHandler class.""" + + warnings.warn( + "AuthHandler is deprecated. Please use AuthHandlerSettings instead.", + DeprecationWarning, + stacklevel=2, ) + super().__init__(**_create_kwargs(**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 a8b4fcc16..3bf5f14f4 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 @@ -28,7 +28,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 ( @@ -67,7 +67,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, @@ -85,7 +85,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: @@ -111,7 +111,7 @@ def __init__( # get from config handlers_config = auth_configuration.get(xpath("handlers"), default={}) auth_handlers = { - handler_name: AuthHandler( + handler_name: AuthHandlerSettings( name=handler_name, auth_type=_get_str(config, *xpath("type"), default=""), **_get(config, *xpath("settings"), default={}), @@ -136,17 +136,17 @@ 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 + 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 From 7124476c322d07d1013c8743f5a6fe8b1a0b9e3f Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 4 Nov 2026 15:31:52 -0800 Subject: [PATCH 2/5] Improving test coverage --- .../microsoft_agents/hosting/core/__init__.py | 2 + .../hosting/core/app/__init__.py | 3 +- .../hosting/core/app/oauth/__init__.py | 3 +- .../oauth/_handlers/_authorization_handler.py | 9 +- .../_handlers/agentic_user_authorization.py | 8 +- .../hosting/core/app/oauth/auth_handler.py | 8 +- .../hosting/core/app/oauth/authorization.py | 10 +- pytest.ini | 2 + .../app/_oauth/test_auth_handler.py | 112 ++++++++++++++- .../app/_oauth/test_authorization.py | 134 +++++++++++++++++- 10 files changed, 268 insertions(+), 23 deletions(-) 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 b8017aa06..77c14e2f7 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 @@ -42,6 +42,7 @@ # App Auth from .app.oauth import ( Authorization, + AuthHandler, AuthHandlerSettings, AgenticUserAuthorization, ) @@ -155,6 +156,7 @@ "TurnState", "TempState", "Authorization", + "AuthHandler", "AuthHandlerSettings", "AccessTokenProviderBase", "AuthenticationConstants", 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 a8736ed7c..4b9d48df9 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 @@ -17,6 +17,7 @@ # Auth from .oauth import ( Authorization, + AuthHandler, AuthHandlerSettings, AgenticUserAuthorization, ) @@ -44,7 +45,6 @@ "InputFile", "InputFileDownloader", "Query", - "Route", "RouteRank", "RouteHandler", "TypingChannelStrategy", @@ -61,6 +61,7 @@ "AuthHandlerSettings", "AgenticUserAuthorization", # Proactive + "AuthHandler", "Conversation", "ConversationBuilder", "ConversationReferenceBuilder", 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 9ab180070..6eacaed9a 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 @@ -66,16 +66,15 @@ def __init__( DeprecationWarning, stacklevel=2, ) - if not auth_handler and not auth_handler_settings: - raise ValueError( - "At least one of auth_handler or auth_handler_settings is required." - ) self._storage = storage self._connection_manager = connection_manager if settings: - self._handler = AuthHandlerSettings.from_mapping(settings) + if isinstance(settings, AuthHandlerSettings): + self._handler = settings + else: + self._handler = AuthHandlerSettings.from_mapping(settings) elif auth_handler: # deprecated self._handler = auth_handler else: # deprecated 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 3bb69edbd..d51303767 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 @@ -15,7 +15,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__) @@ -31,7 +31,7 @@ def __init__( settings: Mapping | AuthHandlerSettings | None = None, *, auth_handler_id: str | None = None, - auth_handler: AuthHandler | None = None, + auth_handler: AuthHandlerSettings | None = None, auth_handler_settings: dict | None = None, **kwargs, ) -> None: @@ -55,7 +55,7 @@ def __init__( auth_handler_settings=auth_handler_settings, **kwargs, ) - self._alt_blueprint_name = self._handler.alt_blueprint_name # TODO + 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. @@ -211,6 +211,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/auth_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py index b179c5e66..896417c8e 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 @@ -31,6 +31,7 @@ def _create_kwargs( obo_connection_name: str | None = None, auth_type: str | None = None, scopes: list[str] | None = None, + _alt_blueprint_name: str | None = None, **kwargs, ) -> dict: c = Configuration(kwargs) @@ -49,6 +50,8 @@ def _create_kwargs( auth_type, c.get_str(xpath("type"), default="UserAuthorization") ).lower(), scopes=Configuration.format_scopes(scopes or c.get(xpath("scopes"))), + _alt_blueprint_name=_alt_blueprint_name + or c.get_strn(xpath("alt_blueprint_name")), ) @@ -65,6 +68,7 @@ class AuthHandlerSettings(_Configurable): obo_connection_name: str auth_type: str scopes: list[str] = field(default_factory=list) + _alt_blueprint_name: str | None = None @classmethod def _from_configuration(cls, config: Configuration) -> AuthHandlerSettings: @@ -82,7 +86,7 @@ def _format_scopes(scopes: str | list[str]) -> list[str]: class AuthHandler(AuthHandlerSettings): """Deprecated. Use AuthHandlerSettings instead.""" - def __init__(self, **kwargs): + def __init__(self, *args, **kwargs): """Initialize the deprecated AuthHandler class.""" warnings.warn( @@ -90,4 +94,4 @@ def __init__(self, **kwargs): DeprecationWarning, stacklevel=2, ) - super().__init__(**_create_kwargs(**kwargs)) + super().__init__(**_create_kwargs(*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 3bf5f14f4..4e86ece5a 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 @@ -111,10 +111,12 @@ def __init__( # get from config handlers_config = auth_configuration.get(xpath("handlers"), default={}) auth_handlers = { - handler_name: AuthHandlerSettings( - name=handler_name, - auth_type=_get_str(config, *xpath("type"), default=""), - **_get(config, *xpath("settings"), default={}), + handler_name: AuthHandlerSettings.from_mapping( + dict( + name=handler_name, + auth_type=_get_str(config, *xpath("type"), default=""), + **_get(config, *xpath("settings"), default={}), + ) ) for handler_name, config in handlers_config.items() } diff --git a/pytest.ini b/pytest.ini index 2c37d0253..d315d797c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -15,6 +15,8 @@ filterwarnings = ignore:The 'is_authenticated' parameter is deprecated and will be removed in future versions\.:DeprecationWarning ignore:The 'is_authenticated' property is deprecated and will be removed in future versions\.:DeprecationWarning # Legacy authentication APIs remain covered until their removal. + ignore:AuthHandler is deprecated\. Please use AuthHandlerSettings instead\.:DeprecationWarning + ignore:auth_handler and auth_handler_settings are deprecated\. Please use settings instead\.:DeprecationWarning ignore:AgentAuthConfiguration is deprecated and will be removed in .*:DeprecationWarning ignore:AgentAuthCConfiguration is deprecated and will be removed in a future version\.:DeprecationWarning ignore:Accessing the deprecated configuration\. This will be removed in future versions\.:DeprecationWarning diff --git a/tests/hosting_core/app/_oauth/test_auth_handler.py b/tests/hosting_core/app/_oauth/test_auth_handler.py index 4f4235edf..1a305b4a2 100644 --- a/tests/hosting_core/app/_oauth/test_auth_handler.py +++ b/tests/hosting_core/app/_oauth/test_auth_handler.py @@ -1,7 +1,8 @@ import pytest -from microsoft_agents.hosting.core import AuthHandler +from microsoft_agents.hosting.core import AuthHandler, AuthHandlerSettings from microsoft_agents.activity import load_configuration_from_env +from microsoft_agents.activity.config import Configuration from tests._common.data import ( DEFAULT_TEST_VALUES, @@ -15,8 +16,8 @@ class TestAuthHandler: - @pytest.mark.parametrize("from_settings", [False, True]) - def test_indexed_environment_scopes_are_bound_without_mutation(self, from_settings): + @pytest.mark.parametrize("from_mapping", [False, True]) + def test_indexed_environment_scopes_are_bound_without_mutation(self, from_mapping): config = load_configuration_from_env( { "AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__graph__SETTINGS__SCOPES__2": "Mail.Read", @@ -29,8 +30,8 @@ def test_indexed_environment_scopes_are_bound_without_mutation(self, from_settin scopes = settings["SCOPES"] handler = ( - AuthHandler._from_settings(settings) - if from_settings + AuthHandlerSettings.from_mapping(settings) + if from_mapping else AuthHandler("graph", **settings) ) @@ -73,3 +74,104 @@ def test_init_agentic(self, agentic_auth_setting): auth_handler.abs_oauth_connection_name == DEFAULTS.agentic_abs_oauth_connection_name ) + + def test_legacy_constructor_warns_and_preserves_positional_arguments(self): + with pytest.warns(DeprecationWarning, match="AuthHandler is deprecated"): + handler = AuthHandler( + "graph", + "Sign in", + "Continue", + "bot-connection", + "obo-connection", + "AgenticUserAuthorization", + ["User.Read"], + ALT_BLUEPRINT_NAME="blueprint", + ) + + assert handler == AuthHandler( + name="graph", + title="Sign in", + text="Continue", + abs_oauth_connection_name="bot-connection", + obo_connection_name="obo-connection", + auth_type="AgenticUserAuthorization", + scopes=["User.Read"], + ALT_BLUEPRINT_NAME="blueprint", + ) + + +class TestAuthHandlerSettings: + @pytest.mark.parametrize("from_configuration", [False, True]) + @pytest.mark.parametrize( + "scopes", + ["User.Read, Mail.Read\tFiles.Read", ["User.Read", "Mail.Read", "Files.Read"]], + ) + def test_normalizes_environment_settings(self, scopes, from_configuration): + settings = { + "NAME": "graph", + "TITLE": "Sign in", + "TEXT": "Continue", + "AZUREBOTOAUTHCONNECTIONNAME": "bot-connection", + "OBOCONNECTIONNAME": "obo-connection", + "TYPE": "AgenticUserAuthorization", + "SCOPES": scopes, + "ALT_BLUEPRINT_NAME": "blueprint", + } + handler = ( + AuthHandlerSettings.from_configuration(Configuration(settings)) + if from_configuration + else AuthHandlerSettings.from_mapping(settings) + ) + + assert handler == AuthHandlerSettings( + name="graph", + title="Sign in", + text="Continue", + abs_oauth_connection_name="bot-connection", + obo_connection_name="obo-connection", + auth_type="agenticuserauthorization", + scopes=["User.Read", "Mail.Read", "Files.Read"], + _alt_blueprint_name="blueprint", + ) + + def test_from_mapping_applies_optional_defaults(self): + handler = AuthHandlerSettings.from_mapping({"name": "graph"}) + + assert handler == AuthHandlerSettings( + name="graph", + title="", + text="", + abs_oauth_connection_name="", + obo_connection_name="", + auth_type="userauthorization", + ) + + @pytest.mark.parametrize( + "settings", + [ + { + "name": "graph", + "abs_oauth_connection_name": "bot-connection", + "obo_connection_name": "obo-connection", + "auth_type": "ConnectorUserAuthorization", + }, + { + "name": "graph", + "azurebotoauthconnectionname": "bot-connection", + "oboconnectionname": "obo-connection", + "type": "ConnectorUserAuthorization", + }, + ], + ids=["snake-case", "lowercase-environment"], + ) + def test_from_mapping_accepts_different_key_styles(self, settings): + handler = AuthHandlerSettings.from_mapping(settings) + + assert handler == AuthHandlerSettings( + name="graph", + title="", + text="", + abs_oauth_connection_name="bot-connection", + obo_connection_name="obo-connection", + auth_type="connectoruserauthorization", + ) diff --git a/tests/hosting_core/app/_oauth/test_authorization.py b/tests/hosting_core/app/_oauth/test_authorization.py index 50f603daa..605e67d56 100644 --- a/tests/hosting_core/app/_oauth/test_authorization.py +++ b/tests/hosting_core/app/_oauth/test_authorization.py @@ -10,6 +10,7 @@ SignInConstants, TokenResponse, ) +from microsoft_agents.activity.config import Configuration from microsoft_agents.hosting.core.app.oauth import ( _SignInResponse, @@ -17,12 +18,14 @@ _UserAuthorization, Authorization, AgenticUserAuthorization, + ConnectorUserAuthorization, ) from microsoft_agents.hosting.core._oauth import _FlowStateTag from microsoft_agents.hosting.core import ( AuthHandler, + AuthHandlerSettings, MemoryStorage, ) @@ -151,6 +154,134 @@ def auth_handler_id(self, request): class TestAuthorizationSetup(TestEnv): + @pytest.mark.parametrize( + "auth_type, handler_class", + [ + ("UserAuthorization", _UserAuthorization), + ("AgenticUserAuthorization", AgenticUserAuthorization), + ("ConnectorUserAuthorization", ConnectorUserAuthorization), + ], + ) + @pytest.mark.parametrize("as_mapping", [False, True]) + @pytest.mark.parametrize("positional", [False, True]) + def test_init_with_settings( + self, + connection_manager, + storage, + auth_type, + handler_class, + as_mapping, + positional, + ): + settings = AuthHandlerSettings.from_mapping( + { + "name": "graph", + "auth_type": auth_type, + "scopes": ["User.Read"], + "_alt_blueprint_name": "blueprint", + } + ) + value = vars(settings) if as_mapping else settings + handler = ( + handler_class(storage, connection_manager, value) + if positional + else handler_class(storage, connection_manager, settings=value) + ) + + assert handler._id == "graph" + assert handler._handler == settings + if not as_mapping: + assert handler._handler is settings + if isinstance(handler, AgenticUserAuthorization): + assert handler._alt_blueprint_name == "blueprint" + + auth = Authorization( + storage, connection_manager, auth_handlers={"graph": settings} + ) + assert isinstance(auth._resolve_handler("graph"), handler_class) + + @pytest.mark.parametrize( + "auth_type, handler_class", + [ + ("UserAuthorization", _UserAuthorization), + ("AgenticUserAuthorization", AgenticUserAuthorization), + ("ConnectorUserAuthorization", ConnectorUserAuthorization), + ], + ) + @pytest.mark.parametrize("config_argument", ["config", "kwargs"]) + @pytest.mark.parametrize("type_in_settings", [False, True]) + def test_init_from_configuration( + self, + connection_manager, + storage, + auth_type, + handler_class, + config_argument, + type_in_settings, + ): + handler_config = { + "SETTINGS": { + "TITLE": "Sign in", + "TEXT": "Continue", + "AZUREBOTOAUTHCONNECTIONNAME": "bot-connection", + "OBOCONNECTIONNAME": "obo-connection", + "SCOPES": "User.Read, Mail.Read", + "ALT_BLUEPRINT_NAME": "blueprint", + } + } + if type_in_settings: + handler_config["SETTINGS"]["TYPE"] = auth_type + else: + handler_config["TYPE"] = auth_type + config = Configuration( + { + "AGENTAPPLICATION": { + "USERAUTHORIZATION": {"HANDLERS": {"graph": handler_config}} + } + } + ) + + auth = ( + Authorization(storage, connection_manager, config=config) + if config_argument == "config" + else Authorization(storage, connection_manager, **config) + ) + handler = auth._resolve_handler("graph") + + assert isinstance(handler, handler_class) + assert handler._handler == AuthHandlerSettings( + name="graph", + title="Sign in", + text="Continue", + abs_oauth_connection_name="bot-connection", + obo_connection_name="obo-connection", + auth_type=auth_type.lower(), + scopes=["User.Read", "Mail.Read"], + _alt_blueprint_name="blueprint", + ) + + @pytest.mark.parametrize( + "handler_class", + [_UserAuthorization, AgenticUserAuthorization, ConnectorUserAuthorization], + ) + @pytest.mark.parametrize( + "legacy_argument", ["auth_handler", "auth_handler_settings"] + ) + def test_legacy_settings_arguments_warn( + self, connection_manager, storage, legacy_argument, handler_class + ): + settings = AuthHandlerSettings.from_mapping({"name": "graph"}) + value = settings if legacy_argument == "auth_handler" else vars(settings) + + with pytest.warns( + DeprecationWarning, match="auth_handler and auth_handler_settings" + ): + handler = handler_class( + storage, connection_manager, **{legacy_argument: value} + ) + + assert handler._handler == settings + 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 @@ -260,7 +391,8 @@ async def test_sign_out( @pytest.mark.asyncio @pytest.mark.parametrize( - "initial_cache, final_cache, auth_handler_id, expected_auth_handler_id, initial_sign_in_state, final_sign_in_state, sign_in_response", + "initial_cache, final_cache, auth_handler_id, expected_auth_handler_id, " + "initial_sign_in_state, final_sign_in_state, sign_in_response", [ [ {DEFAULTS.auth_handler_id: "old_token"}, From 93c9fb1392505f14b3bb463459b951e42cc2d703 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 4 Nov 2026 15:40:55 -0800 Subject: [PATCH 3/5] Addressing PR feedback --- .../hosting/core/app/oauth/authorization.py | 2 +- .../app/_oauth/test_authorization.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) 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 4e86ece5a..e59fc6ae3 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 @@ -141,7 +141,7 @@ def _init_handlers(self) -> None: :type auth_handlers: dict[str, :class:`microsoft_agents.hosting.core.app.oauth.auth_handler.AuthHandlerSettings`] """ for name, settings in self._handler_settings.items(): - auth_type = settings.auth_type + auth_type = settings.auth_type.lower() if auth_type not in AUTHORIZATION_TYPE_MAP: raise ValueError(f"Auth type {auth_type} not recognized.") diff --git a/tests/hosting_core/app/_oauth/test_authorization.py b/tests/hosting_core/app/_oauth/test_authorization.py index 605e67d56..1818a4cef 100644 --- a/tests/hosting_core/app/_oauth/test_authorization.py +++ b/tests/hosting_core/app/_oauth/test_authorization.py @@ -154,6 +154,33 @@ def auth_handler_id(self, request): class TestAuthorizationSetup(TestEnv): + @pytest.mark.parametrize( + "auth_type, handler_class", + [ + ("UserAuthorization", _UserAuthorization), + ("AgenticUserAuthorization", AgenticUserAuthorization), + ("ConnectorUserAuthorization", ConnectorUserAuthorization), + ], + ) + def test_init_with_directly_constructed_settings( + self, connection_manager, storage, auth_type, handler_class + ): + settings = AuthHandlerSettings( + name="graph", + title="Sign in", + text="Continue", + abs_oauth_connection_name="bot-connection", + obo_connection_name="obo-connection", + auth_type=auth_type, + scopes=["User.Read"], + ) + + auth = Authorization( + storage, connection_manager, auth_handlers={"graph": settings} + ) + + assert isinstance(auth._resolve_handler("graph"), handler_class) + @pytest.mark.parametrize( "auth_type, handler_class", [ From 0573c185784b1d288920b6d44f97bf4dafbf00a6 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Thu, 5 Nov 2026 12:38:03 -0800 Subject: [PATCH 4/5] _RootConfigurable and AuthorizationSettings --- .../activity/config/__init__.py | 7 ++- .../activity/config/_load_configuration.py | 6 +-- .../config/_logging/_logging_configuration.py | 13 +++-- .../activity/config/configuration.py | 47 ++++++++++++++---- .../hosting/core/app/oauth/auth_handler.py | 12 ++--- .../hosting/core/app/oauth/authorization.py | 39 ++++++--------- .../core/app/oauth/authorization_settings.py | 49 +++++++++++++++++++ .../jwt/token_validation_configuration.py | 23 +++++---- 8 files changed, 139 insertions(+), 57 deletions(-) create mode 100644 libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization_settings.py diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/__init__.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/__init__.py index eb774a9b3..2ba4dcbdb 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/__init__.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/__init__.py @@ -2,7 +2,11 @@ # Licensed under the MIT License. from ._load_configuration import load_configuration_from_env -from .configuration import Configuration, _Configurable +from .configuration import ( + Configuration, + _Configurable, + _RootConfigurable +) from ._configuration_protocol import _ConfigurationProtocol, _DictConfigurable from ._resolve import _resolve from ._utils import ( @@ -31,6 +35,7 @@ "_try_set", "_resolve", "_Configurable", + "_RootConfigurable", "_DictConfigurable", "coerce_bool", "coerce_int", diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py index 8ce24f1fc..c2916d5fa 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py @@ -23,8 +23,6 @@ def load_configuration_from_env( local_vars = dict(env_vars) resolved = _resolve(local_vars, defaults or {}) - config = Configuration(resolved) - _configure_logging( - config.get_section(_xpath("logging"), default=None) or Configuration() - ) + config = Configuration(resolved, _is_root_config=True) + _configure_logging(config) return config diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_logging/_logging_configuration.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_logging/_logging_configuration.py index 106aecbc7..288db023c 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_logging/_logging_configuration.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_logging/_logging_configuration.py @@ -7,10 +7,11 @@ from dataclasses import dataclass from typing import Mapping +from typing_extensions import Self from microsoft_agents.activity.errors import configuration_errors -from ..configuration import Configuration, _Configurable +from ..configuration import Configuration, _RootConfigurable from .._utils import _xpath from ._constants import _NAME_TO_LEVEL @@ -35,13 +36,19 @@ def __init__(self, namespace: str, level: str) -> None: @dataclass(frozen=True) -class _LoggingConfiguration(_Configurable): +class _LoggingConfiguration(_RootConfigurable): """Configuration class for logging levels.""" log_levels: list[_LogLevelEntry] @classmethod - def _from_configuration(cls, config: Configuration) -> _LoggingConfiguration: + def _from_root_configuration(cls, config: Configuration) -> _LoggingConfiguration: + return cls._from_scoped_configuration( + config.get_section(_xpath("logging"), default=Configuration()) + ) + + @classmethod + def _from_scoped_configuration(cls, config: Configuration) -> _LoggingConfiguration: raw_log_levels = config.get(_xpath("log_level"), default={}) if not isinstance(raw_log_levels, Mapping): diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/configuration.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/configuration.py index e1ea6e1cc..5de92be08 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/configuration.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/configuration.py @@ -40,7 +40,6 @@ "_ConfigurationProtocolT", bound=_ConfigurationProtocol ) - class Configuration(dict, _ConfigurationProtocol): """Dictionary-backed configuration with path lookup and typed conversion. @@ -50,19 +49,31 @@ class Configuration(dict, _ConfigurationProtocol): """ def __init__( - self, _original: Mapping | None = None, _base_path: str | None = None, **kwargs + self, + original: Mapping | None = None, + _base_path: str | None = None, + _is_root_config: bool | None = None, + **kwargs, ): """Initialize a Configuration instance. - :param _original: The original mapping to initialize the configuration from. + :param original: The original mapping to initialize the configuration from. :param _base_path: The base path for resolving nested configuration keys. """ - if _original is not None and kwargs: - raise ValueError("Cannot provide both '_original' and keyword arguments.") - super().__init__(_original or kwargs) + if original is not None and kwargs: + raise ValueError("Cannot provide both 'original' and keyword arguments.") + super().__init__(original or kwargs) self._base_path = _base_path or "" + if _is_root_config is None and _base_path == "": + self._is_root_config = True + else: + self._is_root_config = _is_root_config or False self._key = None + @property + def is_root_config(self) -> bool: + return self._is_root_config + @property def base_path(self) -> str: return self._base_path @@ -79,7 +90,10 @@ def key(self) -> str: @classmethod def from_mapping( - cls, _mapping: Mapping | None = None, _base_path: str | None = None, **kwargs + cls, + _mapping: Mapping | None = None, + _base_path: str | None = None, + **kwargs ) -> Configuration: """Create configuration, using keyword arguments as fallback values. @@ -108,7 +122,7 @@ def from_config(self, base: Configuration | None, **kwargs) -> Configuration: return Configuration(**kwargs) new_config = base.copy() return Configuration.from_mapping( - new_config, _base_path=base._base_path, **kwargs + new_config, _base_path=base._base_path, _is_root_config=base._is_root_config, **kwargs ) def _resolve_path(self, key: str) -> str: @@ -427,7 +441,6 @@ def format_scopes(scopes: str | list[str] | None) -> list[str]: return [s for s in lst if s] return scopes - class _Configurable(ABC): @classmethod @@ -445,3 +458,19 @@ def from_configuration(cls, config: Configuration | Self) -> Self: if isinstance(config, Configuration): return cls._from_configuration(config) raise TypeError(f"Cannot create {cls.__name__} from {type(config).__name__}") + +class _RootConfigurable(_Configurable, ABC): + + @classmethod + def _from_configuration(cls, config: Configuration) -> Self: + if config._is_root_config: + return cls._from_root_configuration(config) + return cls._from_scoped_configuration(config) + + @classmethod + @abstractmethod + def _from_scoped_configuration(cls, config: Configuration) -> Self: ... + + @classmethod + @abstractmethod + def _from_root_configuration(cls, config: Configuration) -> Self: ... 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 896417c8e..f052032b7 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 @@ -61,12 +61,12 @@ class AuthHandlerSettings(_Configurable): Interface defining an authorization handler for OAuth flows. """ - name: str - title: str - text: str - abs_oauth_connection_name: str - obo_connection_name: str - auth_type: str + name: str = "" + title: str = "" + text: str = "" + abs_oauth_connection_name: str = "" + obo_connection_name: str = "" + auth_type: str = "userauthorization" scopes: list[str] = field(default_factory=list) _alt_blueprint_name: str | None = None 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 e59fc6ae3..369375e32 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 @@ -3,8 +3,16 @@ Licensed under the MIT License. """ +from __future__ import annotations + import logging -from typing import Optional, Callable, Awaitable, cast +from typing import ( + Awaitable, + Callable, + cast, + Optional, + Mapping, +) from dataclasses import dataclass from azure.core.credentials_async import AsyncTokenCredential @@ -17,7 +25,6 @@ TokenResponse, ) from microsoft_agents.activity.activity_types import ActivityTypes -from microsoft_agents.activity.config import Configuration, xpath, _get, _get_str from microsoft_agents.hosting.core.authorization._helpers import ( _CallableTokenCredential, @@ -37,6 +44,7 @@ _UserAuthorization, _AuthorizationHandler, ) +from .authorization_settings import AuthorizationSettings logger = logging.getLogger(__name__) @@ -55,7 +63,6 @@ class _AuthInterceptResult: should_replay: bool continuation_activity: Activity | None = None - class Authorization: """Class responsible for managing authorization flows.""" @@ -70,8 +77,8 @@ def __init__( auth_handlers: dict[str, AuthHandlerSettings] | None = None, auto_sign_in: bool = False, *, + settings: Mapping | None = None, use_cache: bool = False, - config: Configuration | None = None, **kwargs, ): """ @@ -103,28 +110,10 @@ def __init__( self._handlers = {} - auth_configuration = Configuration.from_mapping(config, **kwargs).get_section( - xpath("agent_application", "user_authorization"), default=Configuration() - ) - - if not auth_handlers: - # get from config - handlers_config = auth_configuration.get(xpath("handlers"), default={}) - auth_handlers = { - handler_name: AuthHandlerSettings.from_mapping( - dict( - name=handler_name, - auth_type=_get_str(config, *xpath("type"), default=""), - **_get(config, *xpath("settings"), default={}), - ) - ) - for handler_name, config in handlers_config.items() - } + auth_settings = AuthorizationSettings.from_mapping(settings, **kwargs) - self._handler_settings = auth_handlers - self._auto_sign_in = auto_sign_in or auth_configuration.get_bool( - xpath("auto_sign_in"), default=False - ) + self._handler_settings = auth_handlers or auth_settings.handlers + self._auto_sign_in = auto_sign_in or auth_settings.auto_sign_in # operations default to the first handler if none specified if self._handler_settings: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization_settings.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization_settings.py new file mode 100644 index 000000000..d642c3015 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization_settings.py @@ -0,0 +1,49 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from microsoft_agents.activity.config import ( + Configuration, + _RootConfigurable, + xpath, + _get, + _get_str, +) +from .auth_handler import AuthHandlerSettings + +@dataclass(frozen=True) +class AuthorizationSettings(_RootConfigurable): + + auto_sign_in: bool = False + handlers: dict[str, AuthHandlerSettings] = field(default_factory=dict) + + @classmethod + def _from_scoped_configuration(cls, config: Configuration) -> AuthorizationSettings: + handlers_config = config.get(xpath("handlers"), default={}) + handlers = { + handler_name: AuthHandlerSettings.from_mapping( + dict( + name=handler_name, + auth_type=_get_str(config, *xpath("type"), default=""), + **_get(config, *xpath("settings"), default={}), + ) + ) + for handler_name, config in handlers_config.items() + } + return AuthorizationSettings( + auto_sign_in=config.get_bool(xpath("auto_sign_in"), default=False), + handlers=handlers, + ) + + @classmethod + def _from_root_configuration(cls, config: Configuration) -> AuthorizationSettings: + return cls._from_scoped_configuration( + config.get_section( + xpath("agent_application", "authorization"), default=Configuration() + ) + ) \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/token_validation_configuration.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/token_validation_configuration.py index b71cb2d8a..843e6cef5 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/token_validation_configuration.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/token_validation_configuration.py @@ -6,13 +6,13 @@ from dataclasses import dataclass, field from typing import Any -from microsoft_agents.activity.config import Configuration, _Configurable, xpath +from microsoft_agents.activity.config import Configuration, _RootConfigurable, xpath from .jwt_connection_settings import JwtConnectionSettings @dataclass(frozen=True) -class TokenValidationConfiguration(_Configurable): +class TokenValidationConfiguration(_RootConfigurable): tenant_id: str | None = None audiences: list[str] = field(default_factory=list) @@ -21,16 +21,21 @@ class TokenValidationConfiguration(_Configurable): connections: dict[str, JwtConnectionSettings] = field(default_factory=dict) @classmethod - def _from_configuration( + def _from_root_configuration(cls, configuration: Configuration) -> TokenValidationConfiguration: + return cls._from_scoped_configuration( + configuration.get_section( + xpath("token_validation"), default=Configuration() + ) + ) + + @classmethod + def _from_scoped_configuration( cls, configuration: Configuration ) -> TokenValidationConfiguration: - token_validation = configuration.get_section( - xpath("token_validation"), default=Configuration() - ) - audiences = token_validation.get_str_list(xpath("audiences")) - tenant_id = token_validation.get_strn(xpath("tenant_id")) - anonymous_allowed = token_validation.get_booln(xpath("anonymous_allowed")) + audiences = configuration.get_str_list(xpath("audiences")) + tenant_id = configuration.get_strn(xpath("tenant_id")) + anonymous_allowed = configuration.get_booln(xpath("anonymous_allowed")) connections: dict[str, JwtConnectionSettings] = {} for name, conf in configuration.get_items(xpath("connections")): From 1dcb87ed68532b9dba9b18e885df46d7a697a4e1 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Thu, 5 Nov 2026 14:42:41 -0800 Subject: [PATCH 5/5] root configuration handling --- .../activity/config/__init__.py | 6 +-- .../activity/config/_load_configuration.py | 7 +-- .../activity/config/configuration.py | 48 ++++++++++++++----- .../hosting/core/app/oauth/authorization.py | 29 +++++++++-- .../core/app/oauth/authorization_settings.py | 6 ++- .../authorization/jwt/jwt_token_validator.py | 7 ++- .../jwt/token_validation_configuration.py | 31 ++++++------ 7 files changed, 91 insertions(+), 43 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/__init__.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/__init__.py index 2ba4dcbdb..c0c93f1a0 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/__init__.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/__init__.py @@ -2,11 +2,7 @@ # Licensed under the MIT License. from ._load_configuration import load_configuration_from_env -from .configuration import ( - Configuration, - _Configurable, - _RootConfigurable -) +from .configuration import Configuration, _Configurable, _RootConfigurable from ._configuration_protocol import _ConfigurationProtocol, _DictConfigurable from ._resolve import _resolve from ._utils import ( diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py index c2916d5fa..83694f614 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py @@ -6,8 +6,6 @@ from .configuration import Configuration from ._logging import _configure_logging -from ._utils import _xpath -from ._resolve import _resolve def load_configuration_from_env( @@ -20,9 +18,6 @@ def load_configuration_from_env( :param defaults: A mapping of default configuration values. :return: A Configuration object containing the resolved configuration. """ - local_vars = dict(env_vars) - - resolved = _resolve(local_vars, defaults or {}) - config = Configuration(resolved, _is_root_config=True) + config = Configuration.resolve(env_vars, defaults or {}, _is_root_config=True) _configure_logging(config) return config diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/configuration.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/configuration.py index 5de92be08..db5f09e09 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/configuration.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/configuration.py @@ -6,7 +6,6 @@ from abc import ABC, abstractmethod from typing import ( Any, - Callable, Iterable, Mapping, TypeVar, @@ -40,6 +39,7 @@ "_ConfigurationProtocolT", bound=_ConfigurationProtocol ) + class Configuration(dict, _ConfigurationProtocol): """Dictionary-backed configuration with path lookup and typed conversion. @@ -64,10 +64,9 @@ def __init__( raise ValueError("Cannot provide both 'original' and keyword arguments.") super().__init__(original or kwargs) self._base_path = _base_path or "" - if _is_root_config is None and _base_path == "": - self._is_root_config = True - else: - self._is_root_config = _is_root_config or False + if isinstance(original, Configuration) and _is_root_config is None: + _is_root_config = original._is_root_config + self._is_root_config = _is_root_config or False self._key = None @property @@ -88,12 +87,27 @@ def key(self) -> str: ) return self._key + @classmethod + def resolve( + cls, + primary: Mapping | None = None, + defaults: Mapping | None = None, + *, + _base_path: str | None = None, + _is_root_config: bool | None = None, + ) -> Configuration: + resolved = _resolve(primary or {}, defaults or {}) + return Configuration( + resolved, _base_path=_base_path, _is_root_config=_is_root_config + ) + @classmethod def from_mapping( cls, _mapping: Mapping | None = None, _base_path: str | None = None, - **kwargs + _is_root_config: bool | None = None, + **kwargs, ) -> Configuration: """Create configuration, using keyword arguments as fallback values. @@ -104,6 +118,7 @@ def from_mapping( isinstance(_mapping, Configuration) and not kwargs and (_base_path is None or _base_path == _mapping._base_path) + and (_is_root_config is None or _is_root_config == _mapping._is_root_config) ): return _mapping if kwargs: @@ -113,16 +128,23 @@ def from_mapping( resolved = _resolve(_mapping, kwargs, path_transformer=lambda x: x) else: resolved = _mapping - return cls(resolved, _base_path=_base_path) + + if _is_root_config is None: + if isinstance(_mapping, Configuration): + _is_root_config = _mapping._is_root_config + return cls(resolved, _base_path=_base_path, _is_root_config=_is_root_config) @classmethod - def from_config(self, base: Configuration | None, **kwargs) -> Configuration: + def from_config(cls, base: Configuration | None, **kwargs) -> Configuration: """Create a new Configuration instance by forking the current configuration with additional key-value pairs.""" if base is None: - return Configuration(**kwargs) + return Configuration(**kwargs, _is_root_config=False) new_config = base.copy() return Configuration.from_mapping( - new_config, _base_path=base._base_path, _is_root_config=base._is_root_config, **kwargs + new_config, + _base_path=base._base_path, + _is_root_config=base._is_root_config, + **kwargs, ) def _resolve_path(self, key: str) -> str: @@ -441,6 +463,7 @@ def format_scopes(scopes: str | list[str] | None) -> list[str]: return [s for s in lst if s] return scopes + class _Configurable(ABC): @classmethod @@ -452,13 +475,16 @@ def from_mapping(cls, _mapping: Mapping | None = None, **kwargs) -> Self: def _from_configuration(cls, config: Configuration) -> Self: ... @classmethod - def from_configuration(cls, config: Configuration | Self) -> Self: + def from_configuration(cls, config: Configuration | Self | None) -> Self: + if config is None: + config = Configuration() if isinstance(config, cls): return config if isinstance(config, Configuration): return cls._from_configuration(config) raise TypeError(f"Cannot create {cls.__name__} from {type(config).__name__}") + class _RootConfigurable(_Configurable, ABC): @classmethod 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 369375e32..d5bbc1f12 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 @@ -6,6 +6,8 @@ from __future__ import annotations import logging +import warnings + from typing import ( Awaitable, Callable, @@ -24,6 +26,7 @@ SignInConstants, TokenResponse, ) +from microsoft_agents.activity.config import Configuration from microsoft_agents.activity.activity_types import ActivityTypes from microsoft_agents.hosting.core.authorization._helpers import ( @@ -63,6 +66,7 @@ class _AuthInterceptResult: should_replay: bool continuation_activity: Activity | None = None + class Authorization: """Class responsible for managing authorization flows.""" @@ -77,7 +81,7 @@ def __init__( auth_handlers: dict[str, AuthHandlerSettings] | None = None, auto_sign_in: bool = False, *, - settings: Mapping | None = None, + settings: Configuration | AuthorizationSettings | None = None, use_cache: bool = False, **kwargs, ): @@ -108,9 +112,28 @@ def __init__( Callable[[TurnContext, TurnState, str | None], Awaitable[None]] ] = None - self._handlers = {} + if kwargs: + warnings.warn( + "Passing configuration via kwargs is deprecated. " + "Use the 'settings' parameter instead.", + DeprecationWarning, + ) + if settings is not None: + logger.warning( + "Both 'settings' and 'kwargs' are provided. 'kwargs' will be ignored in favor of 'settings'." + ) + + auth_settings: AuthorizationSettings + if settings is None: + auth_settings = AuthorizationSettings.from_configuration( + Configuration(kwargs, _is_root_config=True) + ) + elif isinstance(settings, Mapping): + auth_settings = AuthorizationSettings.from_mapping(settings) + elif isinstance(settings, AuthorizationSettings): + auth_settings = settings - auth_settings = AuthorizationSettings.from_mapping(settings, **kwargs) + self._handlers = {} self._handler_settings = auth_handlers or auth_settings.handlers self._auto_sign_in = auto_sign_in or auth_settings.auto_sign_in diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization_settings.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization_settings.py index d642c3015..9add5bdbf 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization_settings.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization_settings.py @@ -16,6 +16,7 @@ ) from .auth_handler import AuthHandlerSettings + @dataclass(frozen=True) class AuthorizationSettings(_RootConfigurable): @@ -44,6 +45,7 @@ def _from_scoped_configuration(cls, config: Configuration) -> AuthorizationSetti def _from_root_configuration(cls, config: Configuration) -> AuthorizationSettings: return cls._from_scoped_configuration( config.get_section( - xpath("agent_application", "authorization"), default=Configuration() + xpath("agent_application", "user_authorization"), + default=Configuration(), ) - ) \ No newline at end of file + ) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/jwt_token_validator.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/jwt_token_validator.py index 3de9c92e7..7c64e18c1 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/jwt_token_validator.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/jwt_token_validator.py @@ -79,7 +79,12 @@ class JwtTokenValidator: _jwk_client_manager = _JwkClientManager() - def __init__(self, configuration: AgentAuthConfiguration | Configuration): + def __init__( + self, + configuration: ( + AgentAuthConfiguration | Configuration | TokenValidationConfiguration + ), + ): """Initializes the JwtTokenValidator with the given configuration. :param configuration: Application Configuration used to bind diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/token_validation_configuration.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/token_validation_configuration.py index 843e6cef5..6d83129ea 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/token_validation_configuration.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt/token_validation_configuration.py @@ -6,13 +6,13 @@ from dataclasses import dataclass, field from typing import Any -from microsoft_agents.activity.config import Configuration, _RootConfigurable, xpath +from microsoft_agents.activity.config import Configuration, _Configurable, xpath from .jwt_connection_settings import JwtConnectionSettings @dataclass(frozen=True) -class TokenValidationConfiguration(_RootConfigurable): +class TokenValidationConfiguration(_Configurable): tenant_id: str | None = None audiences: list[str] = field(default_factory=list) @@ -21,24 +21,25 @@ class TokenValidationConfiguration(_RootConfigurable): connections: dict[str, JwtConnectionSettings] = field(default_factory=dict) @classmethod - def _from_root_configuration(cls, configuration: Configuration) -> TokenValidationConfiguration: - return cls._from_scoped_configuration( - configuration.get_section( - xpath("token_validation"), default=Configuration() - ) - ) - - @classmethod - def _from_scoped_configuration( + def _from_configuration( cls, configuration: Configuration ) -> TokenValidationConfiguration: - audiences = configuration.get_str_list(xpath("audiences")) - tenant_id = configuration.get_strn(xpath("tenant_id")) - anonymous_allowed = configuration.get_booln(xpath("anonymous_allowed")) + token_validation: Configuration = configuration + connection_raw_items = {} + + if configuration._is_root_config: + token_validation = configuration.get_section( + xpath("token_validation"), default=Configuration() + ) + connection_raw_items = configuration.get_items(xpath("connections")) + + audiences = token_validation.get_str_list(xpath("audiences")) + tenant_id = token_validation.get_strn(xpath("tenant_id")) + anonymous_allowed = token_validation.get_booln(xpath("anonymous_allowed")) connections: dict[str, JwtConnectionSettings] = {} - for name, conf in configuration.get_items(xpath("connections")): + for name, conf in connection_raw_items: connections[name] = JwtConnectionSettings.from_mapping(conf) update_audiences = not audiences