From 7bcb5b9a14cc395c8eecdc92dfc78926794076ca Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 12 Aug 2026 13:28:16 -0700 Subject: [PATCH 1/9] | None typing --- .../microsoft_agents/activity/activity.py | 144 +++++++++--------- .../microsoft_agents/activity/agents_model.py | 2 +- 2 files changed, 76 insertions(+), 70 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 224c43fb5..a6cb4c2b7 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -156,47 +156,47 @@ class Activity(AgentsModel): """ type: NonEmptyString - channel_id: Optional[ChannelId] = None - id: Optional[NonEmptyString] = None - timestamp: datetime = None - local_timestamp: datetime = None - local_timezone: NonEmptyString = None - service_url: NonEmptyString = None - from_property: Annotated[ChannelAccount, Field(alias="from")] = None - conversation: ConversationAccount = None - recipient: ChannelAccount = None - text_format: NonEmptyString = None - attachment_layout: NonEmptyString = None - members_added: list[ChannelAccount] = None - members_removed: list[ChannelAccount] = None - reactions_added: list[MessageReaction] = None - reactions_removed: list[MessageReaction] = None - topic_name: NonEmptyString = None - history_disclosed: bool = None - locale: NonEmptyString = None - text: str = None - speak: str = None - input_hint: NonEmptyString = None - summary: NonEmptyString = None - suggested_actions: SuggestedActions = None - attachments: list[Attachment] = None - entities: list[SerializeAsAny[Entity]] = None + channel_id: ChannelId | None = None + id: NonEmptyString | None = None + timestamp: datetime | None = None + local_timestamp: datetime | None = None + local_timezone: NonEmptyString | None = None + service_url: NonEmptyString | None = None + from_property: Annotated[ChannelAccount | None, Field(alias="from")] = None + conversation: ConversationAccount | None = None + recipient: ChannelAccount | None = None + text_format: NonEmptyString | None = None + attachment_layout: NonEmptyString | None = None + members_added: list[ChannelAccount] = Field(default_factory=list) + members_removed: list[ChannelAccount] = Field(default_factory=list) + reactions_added: list[MessageReaction] = Field(default_factory=list) + reactions_removed: list[MessageReaction] = Field(default_factory=list) + topic_name: NonEmptyString | None = None + history_disclosed: bool | None = None + locale: NonEmptyString | None = None + text: str = "" + speak: str = "" + input_hint: NonEmptyString | None = None + summary: NonEmptyString | None = None + suggested_actions: SuggestedActions | None = None + attachments: list[Attachment] = Field(default_factory=list) + entities: list[SerializeAsAny[Entity]] = Field(default_factory=list) channel_data: object = None - action: NonEmptyString = None - reply_to_id: NonEmptyString = None - label: NonEmptyString = None - value_type: NonEmptyString = None + action: NonEmptyString | None = None + reply_to_id: NonEmptyString | None = None + label: NonEmptyString | None = None + value_type: NonEmptyString | None = None value: object = None - name: NonEmptyString = None - relates_to: ConversationReference = None - code: NonEmptyString = None - expiration: datetime = None - importance: NonEmptyString = None - delivery_mode: NonEmptyString = None - listen_for: list[NonEmptyString] = None - text_highlights: list[TextHighlight] = None - semantic_action: SemanticAction = None - caller_id: NonEmptyString = None + name: NonEmptyString | None = None + relates_to: ConversationReference | None = None + code: NonEmptyString | None = None + expiration: datetime | None = None + importance: NonEmptyString | None = None + delivery_mode: NonEmptyString | None = None + listen_for: list[NonEmptyString] = Field(default_factory=list) + text_highlights: list[TextHighlight] = Field(default_factory=list) + semantic_action: SemanticAction | None = None + caller_id: NonEmptyString | None = None @model_validator(mode="wrap") @classmethod @@ -880,36 +880,42 @@ def create_reply( .. remarks:: The new activity sets up routing information based on this activity. """ - return cast( - Self, - pick_model( - self.__class__, - type=ActivityTypes.message, - timestamp=datetime.now(timezone.utc), - from_property=SkipNone( - ChannelAccount.pick_properties(self.recipient, ["id", "name"]) - ), - recipient=SkipNone( - ChannelAccount.pick_properties(self.from_property, ["id", "name"]) - ), - reply_to_id=( - SkipNone(self.id) - if self.type != ActivityTypes.conversation_update - or self.channel_id not in ["directline", "webchat"] - else None - ), - service_url=self.service_url, - channel_id=self.channel_id, - conversation=SkipNone( - ConversationAccount.pick_properties( - self.conversation, ["is_group", "id", "name"] - ) - ), - text=text if text else "", - locale=locale if locale else SkipNone(self.locale), - attachments=[], - entities=[], - ), + reply_to_id: NonEmptyString | None = None + conversation: ConversationAccount | None = None + from_property: ChannelAccount | None = None + recipient: ChannelAccount | None = None + + + if self.type != ActivityTypes.conversation_update or self.channel_id not in ["directline", "webchat"]: + reply_to_id = self.id + + if self.conversation: + conversation = ConversationAccount( + is_group=self.conversation.is_group, + id=self.conversation.id, + name=self.conversation.name, + ) + + if self.from_property: + ChannelAccount( + id=self.from_property.id, name=self.from_property.name + ) + if self.recipient: + ChannelAccount( + id=self.recipient.id, name=self.recipient.name + ) + + return self.__class__( + type=ActivityTypes.message, + timestamp=datetime.now(timezone.utc), + from_property=from_property, + recipient=recipient, + reply_to_id=reply_to_id, + service_url=self.service_url, + channel_id=self.channel_id, + conversation=conversation, + text=text or "", + locale=locale or self.locale, ) def create_trace( diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py index a7465ed39..4231ece33 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py @@ -25,7 +25,7 @@ def _serialize(self): """ @classmethod - def pick_properties(cls, original: AgentsModel, fields_to_copy=None, **kwargs): + def pick_properties(cls, original: AgentsModel | None, fields_to_copy=None, **kwargs): """Picks properties from the original model and returns a new instance (of a possibly different AgentsModel) with those properties. This method preserves unset values. From 9fb901ac77cf03ab2cb1bbdf8efa80e24e44f87c Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Thu, 13 Aug 2026 11:13:08 -0700 Subject: [PATCH 2/9] Another commit --- .../microsoft_agents/activity/activity.py | 68 ++++++++----------- .../core/app/proactive/conversation.py | 2 + 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index a6cb4c2b7..b708222e2 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -938,36 +938,26 @@ def create_trace( if not value_type and value: value_type = type(value).__name__ - return cast( - Self, - pick_model( - self.__class__, - type=ActivityTypes.trace, - timestamp=datetime.now(timezone.utc), - from_property=SkipNone( - ChannelAccount.pick_properties(self.recipient, ["id", "name"]) - ), - recipient=SkipNone( - ChannelAccount.pick_properties(self.from_property, ["id", "name"]) - ), - reply_to_id=( - SkipNone(self.id) # preserve unset - if self.type != ActivityTypes.conversation_update - or self.channel_id not in ["directline", "webchat"] - else None - ), - service_url=self.service_url, - channel_id=self.channel_id, - conversation=SkipNone( - ConversationAccount.pick_properties( - self.conversation, ["is_group", "id", "name"] - ) - ), - name=SkipNone(name), - label=SkipNone(label), - value_type=SkipNone(value_type), - value=SkipNone(value), + reply_to_id: NonEmptyString | None = None + + if self.type != ActivityTypes.conversation_update or self.channel_id not in ["directline", "webchat"]: + reply_to_id = self.id + + return self.__class__( + type=ActivityTypes.trace, + timestamp=datetime.now(timezone.utc), + from_property=ChannelAccount.pick_properties(self.recipient, ["id", "name"]), + recipient=ChannelAccount.pick_properties(self.from_property, ["id", "name"]), + reply_to_id=reply_to_id, + service_url=self.service_url, + channel_id=self.channel_id, + conversation=ConversationAccount.pick_properties( + self.conversation, ["is_group", "id", "name"] ), + name=name, + label=label, + value_type=value_type, + value=value ).as_trace_activity() @staticmethod @@ -990,16 +980,12 @@ def create_trace_activity( if not value_type and value: value_type = type(value).__name__ - return cast( - Activity, - pick_model( - Activity, - type=ActivityTypes.trace, - name=name, - label=SkipNone(label), - value_type=SkipNone(value_type), - value=SkipNone(value), - ), + return Activity( + type=ActivityTypes.trace, + name=name, + label=label, + value_type=value_type, + value=value, ) @staticmethod @@ -1022,6 +1008,10 @@ def get_conversation_reference( Composite values are split only on the first ``:``. :returns: A conversation reference for the conversation that contains this activity. """ + activity_id: str | None + return ConversationReference( + activity_id=activity_id + ) return cast( ConversationReference, pick_model( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py index 079136dcc..0fe6b742e 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py @@ -97,6 +97,8 @@ def identity_from_claims(claims: dict[str, str]) -> ClaimsIdentity: :return: Reconstituted claims identity. :rtype: :class:`~microsoft_agents.hosting.core.authorization.ClaimsIdentity` """ + if not claims: + return ClaimsIdentity(claims={}, is_authenticated=False) return ClaimsIdentity(claims=dict(claims), is_authenticated=True) # ------------------------------------------------------------------ From 2213716eb5321e657a2f7514cb1e3a54637751c4 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Thu, 13 Aug 2026 15:58:42 -0700 Subject: [PATCH 3/9] ClaimsIdentity improvements --- .../microsoft_agents/activity/activity.py | 35 ++++++----- .../microsoft_agents/activity/agents_model.py | 4 +- .../hosting/core/_http_adapter_base.py | 6 +- .../core/app/proactive/conversation.py | 3 +- .../core/authorization/claims_identity.py | 58 +++++++++++++++---- .../authorization/jwt/jwt_token_validator.py | 4 +- .../hosting/core/channel_service_adapter.py | 18 +++--- 7 files changed, 84 insertions(+), 44 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index b708222e2..af5db53ff 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -884,11 +884,13 @@ def create_reply( conversation: ConversationAccount | None = None from_property: ChannelAccount | None = None recipient: ChannelAccount | None = None - - if self.type != ActivityTypes.conversation_update or self.channel_id not in ["directline", "webchat"]: + if self.type != ActivityTypes.conversation_update or self.channel_id not in [ + "directline", + "webchat", + ]: reply_to_id = self.id - + if self.conversation: conversation = ConversationAccount( is_group=self.conversation.is_group, @@ -897,13 +899,9 @@ def create_reply( ) if self.from_property: - ChannelAccount( - id=self.from_property.id, name=self.from_property.name - ) + ChannelAccount(id=self.from_property.id, name=self.from_property.name) if self.recipient: - ChannelAccount( - id=self.recipient.id, name=self.recipient.name - ) + ChannelAccount(id=self.recipient.id, name=self.recipient.name) return self.__class__( type=ActivityTypes.message, @@ -940,14 +938,21 @@ def create_trace( reply_to_id: NonEmptyString | None = None - if self.type != ActivityTypes.conversation_update or self.channel_id not in ["directline", "webchat"]: + if self.type != ActivityTypes.conversation_update or self.channel_id not in [ + "directline", + "webchat", + ]: reply_to_id = self.id return self.__class__( type=ActivityTypes.trace, timestamp=datetime.now(timezone.utc), - from_property=ChannelAccount.pick_properties(self.recipient, ["id", "name"]), - recipient=ChannelAccount.pick_properties(self.from_property, ["id", "name"]), + from_property=ChannelAccount.pick_properties( + self.recipient, ["id", "name"] + ), + recipient=ChannelAccount.pick_properties( + self.from_property, ["id", "name"] + ), reply_to_id=reply_to_id, service_url=self.service_url, channel_id=self.channel_id, @@ -957,7 +962,7 @@ def create_trace( name=name, label=label, value_type=value_type, - value=value + value=value, ).as_trace_activity() @staticmethod @@ -1009,9 +1014,7 @@ def get_conversation_reference( :returns: A conversation reference for the conversation that contains this activity. """ activity_id: str | None - return ConversationReference( - activity_id=activity_id - ) + return ConversationReference(activity_id=activity_id) return cast( ConversationReference, pick_model( diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py index 4231ece33..1fc153b33 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py @@ -25,7 +25,9 @@ def _serialize(self): """ @classmethod - def pick_properties(cls, original: AgentsModel | None, fields_to_copy=None, **kwargs): + def pick_properties( + cls, original: AgentsModel | None, fields_to_copy=None, **kwargs + ): """Picks properties from the original model and returns a new instance (of a possibly different AgentsModel) with those properties. This method preserves unset values. diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_http_adapter_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_http_adapter_base.py index 8add7c0ca..5d6cfaa15 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_http_adapter_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_http_adapter_base.py @@ -112,10 +112,8 @@ async def process_request( span.share(activity=activity) # Get claims identity (default to anonymous if not set by middleware) - claims_identity: ( - ClaimsIdentity - ) = request.get_claims_identity() or ClaimsIdentity( - {}, False, authentication_type="Anonymous" + claims_identity: ClaimsIdentity = ( + request.get_claims_identity() or ClaimsIdentity() ) # Validate required activity fields diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py index 0fe6b742e..bc3411e9a 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py @@ -44,6 +44,7 @@ def __init__( ) -> None: if isinstance(claims, ClaimsIdentity): self.claims: dict[str, str] = Conversation.claims_from_identity(claims) + self.identity else: self.claims = { k: v for k, v in claims.items() if k in _PERSISTED_CLAIM_KEYS @@ -98,7 +99,7 @@ def identity_from_claims(claims: dict[str, str]) -> ClaimsIdentity: :rtype: :class:`~microsoft_agents.hosting.core.authorization.ClaimsIdentity` """ if not claims: - return ClaimsIdentity(claims={}, is_authenticated=False) + return ClaimsIdentity() return ClaimsIdentity(claims=dict(claims), is_authenticated=True) # ------------------------------------------------------------------ diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py index dff15a25a..8173a5e89 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py @@ -1,27 +1,63 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Optional + +import logging from .authentication_constants import AuthenticationConstants +logger = logging.getLogger(__name__) + class ClaimsIdentity: + """Represents an identity with associated claims and authentication information. + + For context, this class merges the functionality of ClaimsIdentity and AgentClaims from .NET + """ + + claims: dict[str, str] + authentication_type: str | None + security_token: str | None # deprecated, will be removed in future versions + is_authenticated: bool | None # deprecated, will be removed in future versions + def __init__( self, - claims: dict[str, str], - is_authenticated: bool, - authentication_type: Optional[str] = None, - security_token: Optional[str] = None, + claims: dict[str, str] | None = None, + is_authenticated: bool | None = None, + authentication_type: str | None = None, + security_token: str | None = None, ): - self.claims = claims - self.is_authenticated = is_authenticated + """Creates a new instance of the ClaimsIdentity class. + + :param claims: A dictionary of claims associated with the identity. + :param is_authenticated: A boolean indicating whether the identity is authenticated. (Deprecated) + :param authentication_type: A string representing the type of authentication used. + None values indicate that the identity is not authenticated. + :param security_token: The security token associated with the identity. + """ + self.claims = claims or {} + if is_authenticated is not None: + logger.warning( + "The 'is_authenticated' parameter is deprecated and will be removed in future versions. Please use 'authentication_type' instead." + ) + self.authentication_type = authentication_type self.security_token = security_token + self.is_authenticated = is_authenticated - def get_claim_value(self, claim_type: str) -> Optional[str]: + def get_claim_value(self, claim_type: str) -> str | None: + """Gets the value of a specific claim type from the claims dictionary. + + :param claim_type: The type of claim to retrieve. + :return: The value of the claim if found, otherwise None. + """ return self.claims.get(claim_type) - def get_app_id(self) -> Optional[str]: + @property + def allow_anonymous(self) -> bool: + """Returns True if the identity allows anonymous access, otherwise False.""" + return not self.is_authenticated and not self.claims + + def get_app_id(self) -> str | None: """ Gets the AppId from the current ClaimsIdentity. @@ -32,7 +68,7 @@ def get_app_id(self) -> Optional[str]: AuthenticationConstants.AUDIENCE_CLAIM, None ) or self.claims.get(AuthenticationConstants.APP_ID_CLAIM, None) - def get_outgoing_app_id(self) -> Optional[str]: + def get_outgoing_app_id(self) -> str | None: """ Gets the outgoing AppId from current claims. @@ -74,7 +110,7 @@ def is_agent_claim(self) -> bool: return app_id != audience - def get_token_audience(self) -> str: + def get_token_audience(self) -> str | None: """ Gets the token audience from current claims. 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 1a9a14d66..00265a9e6 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 @@ -154,12 +154,12 @@ async def validate_token(self, token: str) -> ClaimsIdentity: ) logger.debug("JWT token validated successfully.") - return ClaimsIdentity(decoded_token, True, security_token=token) + return ClaimsIdentity(decoded_token, security_token=token) def get_anonymous_claims(self) -> ClaimsIdentity: """Returns a ClaimsIdentity for an anonymous user.""" logger.debug("Returning anonymous claims identity.") - return ClaimsIdentity({}, False, authentication_type="Anonymous") + return ClaimsIdentity() def _build_jwks_uri( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 77fa371f1..6e16865f6 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -252,10 +252,16 @@ async def create_conversation( # pylint: disable=arguments-differ claims_identity = self.create_claims_identity(agent_app_id) claims_identity.claims[AuthenticationConstants.SERVICE_URL_CLAIM] = service_url + use_anonymous_auth_callback = claims_identity.allow_anonymous + # Create the connector client to use for outbound requests. connector_client = ( await self._channel_service_client_factory.create_connector_client( - None, claims_identity, service_url, audience + None, + claims_identity, + service_url, + audience, + use_anonymous=use_anonymous_auth_callback, ) ) @@ -285,7 +291,7 @@ async def create_conversation( # pylint: disable=arguments-differ # Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) user_token_client = ( await self._channel_service_client_factory.create_user_token_client( - context, claims_identity + context, claims_identity, use_anonymous_auth_callback ) ) context.services.set(UserTokenClientBase, user_token_client) @@ -390,12 +396,7 @@ async def process_activity( else: outgoing_audience = AuthenticationConstants.AGENTS_SDK_SCOPE - use_anonymous_auth_callback = False - if ( - not claims_identity.is_authenticated - and claims_identity.authentication_type == "Anonymous" - ): - use_anonymous_auth_callback = True + use_anonymous_auth_callback = claims_identity.allow_anonymous # Create a turn context and run the pipeline. context = self._create_turn_context( @@ -456,7 +457,6 @@ def create_claims_identity(self, agent_app_id: str = "") -> ClaimsIdentity: AuthenticationConstants.AUDIENCE_CLAIM: agent_app_id, AuthenticationConstants.APP_ID_CLAIM: agent_app_id, }, - False, ) @staticmethod From 24739c7b05dc44743c127aad999d4e50c1783066 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 14 Aug 2026 09:33:43 -0700 Subject: [PATCH 4/9] reverting changes --- .../microsoft_agents/activity/activity.py | 215 +++++++++--------- .../microsoft_agents/activity/agents_model.py | 4 +- 2 files changed, 109 insertions(+), 110 deletions(-) diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index af5db53ff..224c43fb5 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -156,47 +156,47 @@ class Activity(AgentsModel): """ type: NonEmptyString - channel_id: ChannelId | None = None - id: NonEmptyString | None = None - timestamp: datetime | None = None - local_timestamp: datetime | None = None - local_timezone: NonEmptyString | None = None - service_url: NonEmptyString | None = None - from_property: Annotated[ChannelAccount | None, Field(alias="from")] = None - conversation: ConversationAccount | None = None - recipient: ChannelAccount | None = None - text_format: NonEmptyString | None = None - attachment_layout: NonEmptyString | None = None - members_added: list[ChannelAccount] = Field(default_factory=list) - members_removed: list[ChannelAccount] = Field(default_factory=list) - reactions_added: list[MessageReaction] = Field(default_factory=list) - reactions_removed: list[MessageReaction] = Field(default_factory=list) - topic_name: NonEmptyString | None = None - history_disclosed: bool | None = None - locale: NonEmptyString | None = None - text: str = "" - speak: str = "" - input_hint: NonEmptyString | None = None - summary: NonEmptyString | None = None - suggested_actions: SuggestedActions | None = None - attachments: list[Attachment] = Field(default_factory=list) - entities: list[SerializeAsAny[Entity]] = Field(default_factory=list) + channel_id: Optional[ChannelId] = None + id: Optional[NonEmptyString] = None + timestamp: datetime = None + local_timestamp: datetime = None + local_timezone: NonEmptyString = None + service_url: NonEmptyString = None + from_property: Annotated[ChannelAccount, Field(alias="from")] = None + conversation: ConversationAccount = None + recipient: ChannelAccount = None + text_format: NonEmptyString = None + attachment_layout: NonEmptyString = None + members_added: list[ChannelAccount] = None + members_removed: list[ChannelAccount] = None + reactions_added: list[MessageReaction] = None + reactions_removed: list[MessageReaction] = None + topic_name: NonEmptyString = None + history_disclosed: bool = None + locale: NonEmptyString = None + text: str = None + speak: str = None + input_hint: NonEmptyString = None + summary: NonEmptyString = None + suggested_actions: SuggestedActions = None + attachments: list[Attachment] = None + entities: list[SerializeAsAny[Entity]] = None channel_data: object = None - action: NonEmptyString | None = None - reply_to_id: NonEmptyString | None = None - label: NonEmptyString | None = None - value_type: NonEmptyString | None = None + action: NonEmptyString = None + reply_to_id: NonEmptyString = None + label: NonEmptyString = None + value_type: NonEmptyString = None value: object = None - name: NonEmptyString | None = None - relates_to: ConversationReference | None = None - code: NonEmptyString | None = None - expiration: datetime | None = None - importance: NonEmptyString | None = None - delivery_mode: NonEmptyString | None = None - listen_for: list[NonEmptyString] = Field(default_factory=list) - text_highlights: list[TextHighlight] = Field(default_factory=list) - semantic_action: SemanticAction | None = None - caller_id: NonEmptyString | None = None + name: NonEmptyString = None + relates_to: ConversationReference = None + code: NonEmptyString = None + expiration: datetime = None + importance: NonEmptyString = None + delivery_mode: NonEmptyString = None + listen_for: list[NonEmptyString] = None + text_highlights: list[TextHighlight] = None + semantic_action: SemanticAction = None + caller_id: NonEmptyString = None @model_validator(mode="wrap") @classmethod @@ -880,40 +880,36 @@ def create_reply( .. remarks:: The new activity sets up routing information based on this activity. """ - reply_to_id: NonEmptyString | None = None - conversation: ConversationAccount | None = None - from_property: ChannelAccount | None = None - recipient: ChannelAccount | None = None - - if self.type != ActivityTypes.conversation_update or self.channel_id not in [ - "directline", - "webchat", - ]: - reply_to_id = self.id - - if self.conversation: - conversation = ConversationAccount( - is_group=self.conversation.is_group, - id=self.conversation.id, - name=self.conversation.name, - ) - - if self.from_property: - ChannelAccount(id=self.from_property.id, name=self.from_property.name) - if self.recipient: - ChannelAccount(id=self.recipient.id, name=self.recipient.name) - - return self.__class__( - type=ActivityTypes.message, - timestamp=datetime.now(timezone.utc), - from_property=from_property, - recipient=recipient, - reply_to_id=reply_to_id, - service_url=self.service_url, - channel_id=self.channel_id, - conversation=conversation, - text=text or "", - locale=locale or self.locale, + return cast( + Self, + pick_model( + self.__class__, + type=ActivityTypes.message, + timestamp=datetime.now(timezone.utc), + from_property=SkipNone( + ChannelAccount.pick_properties(self.recipient, ["id", "name"]) + ), + recipient=SkipNone( + ChannelAccount.pick_properties(self.from_property, ["id", "name"]) + ), + reply_to_id=( + SkipNone(self.id) + if self.type != ActivityTypes.conversation_update + or self.channel_id not in ["directline", "webchat"] + else None + ), + service_url=self.service_url, + channel_id=self.channel_id, + conversation=SkipNone( + ConversationAccount.pick_properties( + self.conversation, ["is_group", "id", "name"] + ) + ), + text=text if text else "", + locale=locale if locale else SkipNone(self.locale), + attachments=[], + entities=[], + ), ) def create_trace( @@ -936,33 +932,36 @@ def create_trace( if not value_type and value: value_type = type(value).__name__ - reply_to_id: NonEmptyString | None = None - - if self.type != ActivityTypes.conversation_update or self.channel_id not in [ - "directline", - "webchat", - ]: - reply_to_id = self.id - - return self.__class__( - type=ActivityTypes.trace, - timestamp=datetime.now(timezone.utc), - from_property=ChannelAccount.pick_properties( - self.recipient, ["id", "name"] - ), - recipient=ChannelAccount.pick_properties( - self.from_property, ["id", "name"] - ), - reply_to_id=reply_to_id, - service_url=self.service_url, - channel_id=self.channel_id, - conversation=ConversationAccount.pick_properties( - self.conversation, ["is_group", "id", "name"] + return cast( + Self, + pick_model( + self.__class__, + type=ActivityTypes.trace, + timestamp=datetime.now(timezone.utc), + from_property=SkipNone( + ChannelAccount.pick_properties(self.recipient, ["id", "name"]) + ), + recipient=SkipNone( + ChannelAccount.pick_properties(self.from_property, ["id", "name"]) + ), + reply_to_id=( + SkipNone(self.id) # preserve unset + if self.type != ActivityTypes.conversation_update + or self.channel_id not in ["directline", "webchat"] + else None + ), + service_url=self.service_url, + channel_id=self.channel_id, + conversation=SkipNone( + ConversationAccount.pick_properties( + self.conversation, ["is_group", "id", "name"] + ) + ), + name=SkipNone(name), + label=SkipNone(label), + value_type=SkipNone(value_type), + value=SkipNone(value), ), - name=name, - label=label, - value_type=value_type, - value=value, ).as_trace_activity() @staticmethod @@ -985,12 +984,16 @@ def create_trace_activity( if not value_type and value: value_type = type(value).__name__ - return Activity( - type=ActivityTypes.trace, - name=name, - label=label, - value_type=value_type, - value=value, + return cast( + Activity, + pick_model( + Activity, + type=ActivityTypes.trace, + name=name, + label=SkipNone(label), + value_type=SkipNone(value_type), + value=SkipNone(value), + ), ) @staticmethod @@ -1013,8 +1016,6 @@ def get_conversation_reference( Composite values are split only on the first ``:``. :returns: A conversation reference for the conversation that contains this activity. """ - activity_id: str | None - return ConversationReference(activity_id=activity_id) return cast( ConversationReference, pick_model( diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py index 1fc153b33..a7465ed39 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/agents_model.py @@ -25,9 +25,7 @@ def _serialize(self): """ @classmethod - def pick_properties( - cls, original: AgentsModel | None, fields_to_copy=None, **kwargs - ): + def pick_properties(cls, original: AgentsModel, fields_to_copy=None, **kwargs): """Picks properties from the original model and returns a new instance (of a possibly different AgentsModel) with those properties. This method preserves unset values. From 31bc17db031108f7cb10577f6f5cdec78dd7b13f Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 14 Aug 2026 09:58:20 -0700 Subject: [PATCH 5/9] Updating tests and small improvements --- .../adapter/test_aiohttp_cloud_adapter.py | 2 +- .../adapter/test_fastapi_cloud_adapter.py | 2 +- .../test_aiohttp_jwt_validation.py | 12 +- .../test_fastapi_jwt_validation.py | 12 +- .../core/app/proactive/conversation.py | 1 - .../core/authorization/claims_identity.py | 25 +++- .../hosting/core/channel_service_adapter.py | 10 +- .../app/proactive/test_conversation.py | 6 + .../authorization/test_claims_identity.py | 107 ++++++++++++++++++ .../test_channel_service_adapter.py | 43 +++++++ 10 files changed, 199 insertions(+), 21 deletions(-) create mode 100644 tests/hosting_core/authorization/test_claims_identity.py diff --git a/dev/integration/tests/adapter/test_aiohttp_cloud_adapter.py b/dev/integration/tests/adapter/test_aiohttp_cloud_adapter.py index 154b65298..d1a21492f 100644 --- a/dev/integration/tests/adapter/test_aiohttp_cloud_adapter.py +++ b/dev/integration/tests/adapter/test_aiohttp_cloud_adapter.py @@ -30,7 +30,7 @@ async def test_cloud_adapter_configures_user_token_client_endpoint( "token_service_endpoint": token_service_endpoint }, ) - identity = ClaimsIdentity({"aud": "test-app-id"}, True) + identity = ClaimsIdentity({"aud": "test-app-id"}) context = TurnContext( adapter, Activity( diff --git a/dev/integration/tests/adapter/test_fastapi_cloud_adapter.py b/dev/integration/tests/adapter/test_fastapi_cloud_adapter.py index 4c79c1a73..54bf339fd 100644 --- a/dev/integration/tests/adapter/test_fastapi_cloud_adapter.py +++ b/dev/integration/tests/adapter/test_fastapi_cloud_adapter.py @@ -30,7 +30,7 @@ async def test_cloud_adapter_configures_user_token_client_endpoint( "token_service_endpoint": token_service_endpoint }, ) - identity = ClaimsIdentity({"aud": "test-app-id"}, True) + identity = ClaimsIdentity({"aud": "test-app-id"}) context = TurnContext( adapter, Activity( diff --git a/dev/integration/tests/jwt_validation/test_aiohttp_jwt_validation.py b/dev/integration/tests/jwt_validation/test_aiohttp_jwt_validation.py index a3a4c73ef..842206ffd 100644 --- a/dev/integration/tests/jwt_validation/test_aiohttp_jwt_validation.py +++ b/dev/integration/tests/jwt_validation/test_aiohttp_jwt_validation.py @@ -41,7 +41,7 @@ async def _claims_handler(request): identity = request["claims_identity"] return web.json_response( { - "authenticated": identity.is_authenticated, + "allow_anonymous": identity.allow_anonymous, "authentication_type": identity.authentication_type, } ) @@ -73,8 +73,8 @@ async def test_aiohttp_global_middleware_allows_anonymous_request_from_env_confi assert response.status == 200 assert await response.json() == { - "authenticated": False, - "authentication_type": "Anonymous", + "allow_anonymous": True, + "authentication_type": None, } @@ -105,7 +105,7 @@ async def test_aiohttp_global_middleware_accepts_real_service_connection_token( response = await client.get("/", headers={"Authorization": f"Bearer {token}"}) assert response.status == 200 - assert (await response.json())["authenticated"] is True + assert (await response.json())["allow_anonymous"] is False @_requires_real_service_connection @@ -138,8 +138,8 @@ async def test_aiohttp_decorator_allows_anonymous_request_from_env_config( assert response.status == 200 assert await response.json() == { - "authenticated": False, - "authentication_type": "Anonymous", + "allow_anonymous": True, + "authentication_type": None, } diff --git a/dev/integration/tests/jwt_validation/test_fastapi_jwt_validation.py b/dev/integration/tests/jwt_validation/test_fastapi_jwt_validation.py index c1ce1133d..b5f4c8f40 100644 --- a/dev/integration/tests/jwt_validation/test_fastapi_jwt_validation.py +++ b/dev/integration/tests/jwt_validation/test_fastapi_jwt_validation.py @@ -41,7 +41,7 @@ def _claims_payload(request: Request): identity = request.state.claims_identity return { - "authenticated": identity.is_authenticated, + "allow_anonymous": identity.allow_anonymous, "authentication_type": identity.authentication_type, } @@ -78,8 +78,8 @@ def test_fastapi_global_middleware_allows_anonymous_request_from_env_config(): assert response.status_code == 200 assert response.json() == { - "authenticated": False, - "authentication_type": "Anonymous", + "allow_anonymous": True, + "authentication_type": None, } @@ -105,7 +105,7 @@ async def test_fastapi_global_middleware_accepts_real_service_connection_token() response = client.get("/", headers={"Authorization": f"Bearer {token}"}) assert response.status_code == 200 - assert response.json()["authenticated"] is True + assert response.json()["allow_anonymous"] is False @_requires_real_service_connection @@ -134,8 +134,8 @@ def test_fastapi_decorator_allows_anonymous_request_from_env_config(): assert response.status_code == 200 assert response.json() == { - "authenticated": False, - "authentication_type": "Anonymous", + "allow_anonymous": True, + "authentication_type": None, } diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py index bc3411e9a..65d3087e8 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py @@ -44,7 +44,6 @@ def __init__( ) -> None: if isinstance(claims, ClaimsIdentity): self.claims: dict[str, str] = Conversation.claims_from_identity(claims) - self.identity else: self.claims = { k: v for k, v in claims.items() if k in _PERSISTED_CLAIM_KEYS diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py index 8173a5e89..5aede1bef 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py @@ -17,7 +17,6 @@ class ClaimsIdentity: claims: dict[str, str] authentication_type: str | None security_token: str | None # deprecated, will be removed in future versions - is_authenticated: bool | None # deprecated, will be removed in future versions def __init__( self, @@ -37,12 +36,12 @@ def __init__( self.claims = claims or {} if is_authenticated is not None: logger.warning( - "The 'is_authenticated' parameter is deprecated and will be removed in future versions. Please use 'authentication_type' instead." + "The 'is_authenticated' parameter is deprecated and will be removed in future versions." ) self.authentication_type = authentication_type self.security_token = security_token - self.is_authenticated = is_authenticated + self._is_authenticated = is_authenticated def get_claim_value(self, claim_type: str) -> str | None: """Gets the value of a specific claim type from the claims dictionary. @@ -55,7 +54,25 @@ def get_claim_value(self, claim_type: str) -> str | None: @property def allow_anonymous(self) -> bool: """Returns True if the identity allows anonymous access, otherwise False.""" - return not self.is_authenticated and not self.claims + return ( + not self.authentication_type + and not self.claims + ) + + @property + def is_authenticated(self) -> bool: + """Returns True if the identity is authenticated, otherwise False.""" + logger.warning( + "The 'is_authenticated' property is deprecated and will be removed in future versions." + ) + return bool(self.claims) + + @is_authenticated.setter + def is_authenticated(self, value: bool) -> None: + """(Deprecated). This is now a no-op.""" + logger.warning( + "The 'is_authenticated' property is deprecated and will be removed in future versions." + ) def get_app_id(self) -> str | None: """ diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 6e16865f6..6cbc700f9 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -313,6 +313,8 @@ async def process_proactive( callback: Callable[[TurnContext], Awaitable], ): + use_anonymous_auth_callback = claims_identity.allow_anonymous + # Create a turn context and run the pipeline. context = self._create_turn_context( claims_identity, @@ -322,7 +324,7 @@ async def process_proactive( user_token_client = ( await self._channel_service_client_factory.create_user_token_client( - context, claims_identity + context, claims_identity, use_anonymous_auth_callback ) ) context.services.set(UserTokenClientBase, user_token_client) @@ -333,7 +335,11 @@ async def process_proactive( # Create the connector client to use for outbound requests. connector_client = ( await self._channel_service_client_factory.create_connector_client( - context, claims_identity, continuation_activity.service_url, audience + context, + claims_identity, + continuation_activity.service_url, + audience, + use_anonymous=use_anonymous_auth_callback, ) ) context.services.set(ConnectorClientBase, connector_client) diff --git a/tests/hosting_core/app/proactive/test_conversation.py b/tests/hosting_core/app/proactive/test_conversation.py index d6ca5adf9..64814ad5a 100644 --- a/tests/hosting_core/app/proactive/test_conversation.py +++ b/tests/hosting_core/app/proactive/test_conversation.py @@ -113,6 +113,12 @@ def test_identity_from_claims_is_authenticated(self): identity = Conversation.identity_from_claims(claims) assert identity.is_authenticated is True + def test_identity_from_empty_claims_allows_anonymous(self): + identity = Conversation.identity_from_claims({}) + + assert identity.claims == {} + assert identity.allow_anonymous is True + def test_identity_from_claims_preserves_values(self): claims = {"aud": "app-id", "tid": "tenant", "ver": "2.0"} identity = Conversation.identity_from_claims(claims) diff --git a/tests/hosting_core/authorization/test_claims_identity.py b/tests/hosting_core/authorization/test_claims_identity.py new file mode 100644 index 000000000..63fbf4f4c --- /dev/null +++ b/tests/hosting_core/authorization/test_claims_identity.py @@ -0,0 +1,107 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + +import pytest + +from microsoft_agents.hosting.core.authorization import ClaimsIdentity + + +class TestClaimsIdentityConstructor: + def test_default_identity_is_anonymous(self): + identity = ClaimsIdentity() + + assert identity.claims == {} + assert identity.authentication_type is None + assert identity.security_token is None + assert identity.allow_anonymous is True + assert identity.is_authenticated is False + + def test_default_claims_are_not_shared(self): + first = ClaimsIdentity() + second = ClaimsIdentity() + + first.claims["aud"] = "app-id" + + assert second.claims == {} + + def test_constructor_preserves_values(self): + claims = {"aud": "app-id"} + + identity = ClaimsIdentity( + claims=claims, + authentication_type="Bearer", + security_token="token", + ) + + assert identity.claims is claims + assert identity.authentication_type == "Bearer" + assert identity.security_token == "token" + assert identity.is_authenticated is True + + def test_is_authenticated_parameter_is_deprecated(self, caplog): + with caplog.at_level(logging.WARNING): + identity = ClaimsIdentity(is_authenticated=True) + + assert identity.allow_anonymous is False + assert "is_authenticated" in caplog.text + assert "deprecated" in caplog.text + + +class TestClaimsIdentityAnonymousAccess: + @pytest.mark.parametrize( + ("claims", "is_authenticated", "authentication_type", "expected"), + [ + (None, None, None, True), + ({}, False, None, True), + ({}, True, None, False), + ({"aud": "app-id"}, None, None, False), + ({}, None, "Bearer", False), + ], + ) + def test_allow_anonymous( + self, + claims, + is_authenticated, + authentication_type, + expected, + ): + identity = ClaimsIdentity( + claims=claims, + is_authenticated=is_authenticated, + authentication_type=authentication_type, + ) + + assert identity.allow_anonymous is expected + + +class TestClaimsIdentityAuthenticationCompatibility: + @pytest.mark.parametrize( + ("claims", "expected"), + [ + ({}, False), + ({"aud": "app-id"}, True), + ], + ) + def test_is_authenticated_is_derived_from_claims(self, claims, expected): + identity = ClaimsIdentity(claims=claims) + + assert identity.is_authenticated is expected + + def test_is_authenticated_setter_is_deprecated_no_op(self, caplog): + identity = ClaimsIdentity(claims={"aud": "app-id"}) + + with caplog.at_level(logging.WARNING): + identity.is_authenticated = False + + assert identity.is_authenticated is True + assert "is_authenticated" in caplog.text + assert "deprecated" in caplog.text + + +def test_get_claim_value_returns_matching_claim(): + identity = ClaimsIdentity(claims={"aud": "app-id"}) + + assert identity.get_claim_value("aud") == "app-id" + assert identity.get_claim_value("missing") is None diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index aa7267f27..805a0782c 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -240,3 +240,46 @@ async def callback(context: TurnContext): assert context_arg.activity.service_url == "service_url" assert context_arg.services.get(UserTokenClientBase) is user_token_client assert context_arg.services.get(ConnectorClientBase) is connector_client + + @pytest.mark.asyncio + async def test_process_proactive_uses_anonymous_clients(self, mocker): + factory = mocker.Mock(spec=ChannelServiceClientFactoryBase) + user_token_client = mocker.Mock(spec=UserTokenClient) + user_token_client.close = mocker.AsyncMock() + connector_client = mocker.Mock(spec=TeamsConnectorClient) + connector_client.close = mocker.AsyncMock() + factory.create_user_token_client = mocker.AsyncMock( + return_value=user_token_client + ) + factory.create_connector_client = mocker.AsyncMock( + return_value=connector_client + ) + adapter = MyChannelServiceAdapter(factory) + adapter.run_pipeline = mocker.AsyncMock() + identity = ClaimsIdentity() + activity = Activity( + type="message", + conversation={"id": "conversation123"}, + channel_id="channel_id", + service_url="service_url", + ) + callback = mocker.AsyncMock() + + await adapter.process_proactive( + identity, + activity, + "audience", + callback, + ) + + context = adapter.run_pipeline.await_args.args[0] + factory.create_user_token_client.assert_awaited_once_with( + context, identity, True + ) + factory.create_connector_client.assert_awaited_once_with( + context, + identity, + "service_url", + "audience", + use_anonymous=True, + ) From 7de1d15eb423f30285086f3304d1699e228fe4a7 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 14 Aug 2026 09:59:58 -0700 Subject: [PATCH 6/9] Formatting --- .../hosting/core/authorization/claims_identity.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py index 5aede1bef..84423fd3b 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py @@ -54,10 +54,7 @@ def get_claim_value(self, claim_type: str) -> str | None: @property def allow_anonymous(self) -> bool: """Returns True if the identity allows anonymous access, otherwise False.""" - return ( - not self.authentication_type - and not self.claims - ) + return not self.authentication_type and not self.claims @property def is_authenticated(self) -> bool: From 476e81cf9e1a6df0641907cfb013070d666ad2bd Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 14 Aug 2026 10:18:55 -0700 Subject: [PATCH 7/9] Fixing legacy tests --- .../core/authorization/claims_identity.py | 22 +++++++------ .../authorization/test_claims_identity.py | 32 +++++++++++-------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py index 84423fd3b..5cf763b4c 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py @@ -1,12 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import logging +import warnings from .authentication_constants import AuthenticationConstants -logger = logging.getLogger(__name__) - class ClaimsIdentity: """Represents an identity with associated claims and authentication information. @@ -35,8 +33,10 @@ def __init__( """ self.claims = claims or {} if is_authenticated is not None: - logger.warning( - "The 'is_authenticated' parameter is deprecated and will be removed in future versions." + warnings.warn( + "The 'is_authenticated' parameter is deprecated and will be removed in future versions.", + DeprecationWarning, + stacklevel=2, ) self.authentication_type = authentication_type @@ -59,16 +59,20 @@ def allow_anonymous(self) -> bool: @property def is_authenticated(self) -> bool: """Returns True if the identity is authenticated, otherwise False.""" - logger.warning( - "The 'is_authenticated' property is deprecated and will be removed in future versions." + warnings.warn( + "The 'is_authenticated' property is deprecated and will be removed in future versions.", + DeprecationWarning, + stacklevel=2, ) return bool(self.claims) @is_authenticated.setter def is_authenticated(self, value: bool) -> None: """(Deprecated). This is now a no-op.""" - logger.warning( - "The 'is_authenticated' property is deprecated and will be removed in future versions." + warnings.warn( + "The 'is_authenticated' property is deprecated and will be removed in future versions.", + DeprecationWarning, + stacklevel=2, ) def get_app_id(self) -> str | None: diff --git a/tests/hosting_core/authorization/test_claims_identity.py b/tests/hosting_core/authorization/test_claims_identity.py index 63fbf4f4c..e29725f34 100644 --- a/tests/hosting_core/authorization/test_claims_identity.py +++ b/tests/hosting_core/authorization/test_claims_identity.py @@ -1,8 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import logging - import pytest from microsoft_agents.hosting.core.authorization import ClaimsIdentity @@ -40,13 +38,11 @@ def test_constructor_preserves_values(self): assert identity.security_token == "token" assert identity.is_authenticated is True - def test_is_authenticated_parameter_is_deprecated(self, caplog): - with caplog.at_level(logging.WARNING): + def test_is_authenticated_parameter_is_deprecated(self): + with pytest.warns(DeprecationWarning, match="is_authenticated"): identity = ClaimsIdentity(is_authenticated=True) - assert identity.allow_anonymous is False - assert "is_authenticated" in caplog.text - assert "deprecated" in caplog.text + assert identity.allow_anonymous is True class TestClaimsIdentityAnonymousAccess: @@ -55,7 +51,7 @@ class TestClaimsIdentityAnonymousAccess: [ (None, None, None, True), ({}, False, None, True), - ({}, True, None, False), + ({}, True, None, True), ({"aud": "app-id"}, None, None, False), ({}, None, "Bearer", False), ], @@ -75,6 +71,17 @@ def test_allow_anonymous( assert identity.allow_anonymous is expected + @pytest.mark.parametrize("is_authenticated", [False, True]) + def test_deprecated_is_authenticated_does_not_affect_allow_anonymous( + self, is_authenticated + ): + identity = ClaimsIdentity( + claims={}, + is_authenticated=is_authenticated, + ) + + assert identity.allow_anonymous is True + class TestClaimsIdentityAuthenticationCompatibility: @pytest.mark.parametrize( @@ -89,15 +96,14 @@ def test_is_authenticated_is_derived_from_claims(self, claims, expected): assert identity.is_authenticated is expected - def test_is_authenticated_setter_is_deprecated_no_op(self, caplog): + def test_is_authenticated_setter_is_deprecated_no_op(self): identity = ClaimsIdentity(claims={"aud": "app-id"}) - with caplog.at_level(logging.WARNING): + with pytest.warns(DeprecationWarning, match="is_authenticated"): identity.is_authenticated = False - assert identity.is_authenticated is True - assert "is_authenticated" in caplog.text - assert "deprecated" in caplog.text + with pytest.warns(DeprecationWarning, match="is_authenticated"): + assert identity.is_authenticated is True def test_get_claim_value_returns_matching_claim(): From 2c6551ebf101410ca4f7303abdad246a4746b16a Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 14 Aug 2026 10:23:23 -0700 Subject: [PATCH 8/9] Updating pytest.ini to ignore DeprecationWarnings in tests --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index f2a7ece8d..b26925549 100644 --- a/pytest.ini +++ b/pytest.ini @@ -11,6 +11,8 @@ filterwarnings = ignore::DeprecationWarning:setuptools.* ignore::PendingDeprecationWarning ignore:The bot client is deprecated and will be removed in a future release\.:DeprecationWarning + 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 # pytest-asyncio warnings that are safe to ignore ignore:.*deprecated.*asyncio.*:DeprecationWarning:pytest_asyncio.* From 681b488ded3f489545c1d378e8f67495ceb8f622 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 14 Aug 2026 11:33:23 -0700 Subject: [PATCH 9/9] Small improvements --- .../hosting/core/app/proactive/conversation.py | 2 +- .../hosting/core/authorization/claims_identity.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py index 65d3087e8..95eb579ec 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py @@ -99,7 +99,7 @@ def identity_from_claims(claims: dict[str, str]) -> ClaimsIdentity: """ if not claims: return ClaimsIdentity() - return ClaimsIdentity(claims=dict(claims), is_authenticated=True) + return ClaimsIdentity(claims=dict(claims)) # ------------------------------------------------------------------ # Validation diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py index 5cf763b4c..0a6b26f27 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py @@ -31,7 +31,9 @@ def __init__( None values indicate that the identity is not authenticated. :param security_token: The security token associated with the identity. """ - self.claims = claims or {} + if claims is None: + claims = {} + self.claims = claims if is_authenticated is not None: warnings.warn( "The 'is_authenticated' parameter is deprecated and will be removed in future versions.", @@ -54,7 +56,10 @@ def get_claim_value(self, claim_type: str) -> str | None: @property def allow_anonymous(self) -> bool: """Returns True if the identity allows anonymous access, otherwise False.""" - return not self.authentication_type and not self.claims + return ( + not self.authentication_type + or self.authentication_type.lower() == "anonymous" + ) and not self.claims @property def is_authenticated(self) -> bool: @@ -128,7 +133,7 @@ def is_agent_claim(self) -> bool: return app_id != audience - def get_token_audience(self) -> str | None: + def get_token_audience(self) -> str: """ Gets the token audience from current claims.