From a02e35c0dba564a44509fb30638bc7d77f7588d5 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Wed, 28 May 2025 12:17:53 -0500 Subject: [PATCH] Teams SSO and OAuth fixes (#2226) * Fixed Teams SSO & OAuth * Formatting --------- Co-authored-by: Tracy Boehrer --- .../requirements.txt | 2 +- libraries/botbuilder-ai/setup.py | 2 +- .../botbuilder/core/__init__.py | 2 ++ .../botbuilder/core/activity_handler.py | 11 +++++-- .../core/teams/teams_activity_handler.py | 11 ++----- .../teams_sso_token_exchange_middleware.py | 19 ++++++++++-- .../dialogs/prompts/oauth_prompt.py | 8 +++++ .../integration/aiohttp/cloud_adapter.py | 6 +++- .../requirements.txt | 2 +- .../botbuilder-integration-aiohttp/setup.py | 2 +- .../setup.py | 2 +- .../botbuilder/schema/_models_py3.py | 3 ++ .../connector/token_api/models/__init__.py | 3 ++ .../connector/token_api/models/_models.py | 24 ++++++++++++++ .../connector/token_api/models/_models_py3.py | 31 ++++++++++++++++++- .../skills-buffered/child/requirements.txt | 2 +- .../skills-buffered/parent/requirements.txt | 2 +- 17 files changed, 110 insertions(+), 22 deletions(-) diff --git a/libraries/botbuilder-adapters-slack/requirements.txt b/libraries/botbuilder-adapters-slack/requirements.txt index 8dd02c6be..50f1af767 100644 --- a/libraries/botbuilder-adapters-slack/requirements.txt +++ b/libraries/botbuilder-adapters-slack/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.10.11 +aiohttp pyslack botbuilder-core==4.17.0 slackclient diff --git a/libraries/botbuilder-ai/setup.py b/libraries/botbuilder-ai/setup.py index 707369967..10bc3ee5c 100644 --- a/libraries/botbuilder-ai/setup.py +++ b/libraries/botbuilder-ai/setup.py @@ -8,7 +8,7 @@ "azure-cognitiveservices-language-luis==0.2.0", "botbuilder-schema==4.17.0", "botbuilder-core==4.17.0", - "aiohttp==3.10.11", + "aiohttp>=3.10,<4.0", ] TESTS_REQUIRES = ["aiounittest>=1.1.0"] diff --git a/libraries/botbuilder-core/botbuilder/core/__init__.py b/libraries/botbuilder-core/botbuilder/core/__init__.py index c5c038353..0769d9100 100644 --- a/libraries/botbuilder-core/botbuilder/core/__init__.py +++ b/libraries/botbuilder-core/botbuilder/core/__init__.py @@ -48,6 +48,7 @@ from .user_state import UserState from .register_class_middleware import RegisterClassMiddleware from .adapter_extensions import AdapterExtensions +from .serializer_helper import serializer_helper __all__ = [ "ActivityHandler", @@ -100,5 +101,6 @@ "TurnContext", "UserState", "UserTokenProvider", + "serializer_helper", "__version__", ] diff --git a/libraries/botbuilder-core/botbuilder/core/activity_handler.py b/libraries/botbuilder-core/botbuilder/core/activity_handler.py index 51aefb245..4dbf04f0b 100644 --- a/libraries/botbuilder-core/botbuilder/core/activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/activity_handler.py @@ -478,12 +478,19 @@ async def on_invoke_activity( # pylint: disable=unused-argument if ( turn_context.activity.name == SignInConstants.verify_state_operation_name - or turn_context.activity.name - == SignInConstants.token_exchange_operation_name ): await self.on_sign_in_invoke(turn_context) return self._create_invoke_response() + # This is for back-compat with previous versions of Python SDK. This method does not + # exist in the C# SDK, and is not used in the Python SDK. + if ( + turn_context.activity.name + == SignInConstants.token_exchange_operation_name + ): + await self.on_teams_signin_token_exchange(turn_context) + return self._create_invoke_response() + if turn_context.activity.name == "adaptiveCard/action": invoke_value = self._get_adaptive_card_invoke_value( turn_context.activity diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 9344e343e..2cc2e6fb6 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -56,13 +56,6 @@ async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: ): return await self.on_teams_card_action_invoke(turn_context) - if ( - turn_context.activity.name - == SignInConstants.token_exchange_operation_name - ): - await self.on_teams_signin_token_exchange(turn_context) - return self._create_invoke_response() - if turn_context.activity.name == "fileConsent/invoke": return await self.on_teams_file_consent( turn_context, @@ -250,7 +243,9 @@ async def on_teams_signin_verify_state(self, turn_context: TurnContext): raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) async def on_teams_signin_token_exchange(self, turn_context: TurnContext): - raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + # This is for back-compat with previous versions of Python SDK. This method does not + # exist in the C# SDK, and is not used in the Python SDK. + return await self.on_teams_signin_verify_state(turn_context) async def on_teams_file_consent( self, diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py index 1dec1210a..5a6fa5de6 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py @@ -26,6 +26,7 @@ StoreItem, TurnContext, ) +from botframework.connector.auth.user_token_client import UserTokenClient class _TokenStoreItem(StoreItem): @@ -147,17 +148,29 @@ async def _exchanged_token(self, turn_context: TurnContext) -> bool: token_exchange_response: TokenResponse = None aux_dict = {} if turn_context.activity.value: - for prop in ["id", "connection_name", "token", "properties"]: + for prop in ["id", "connectionName", "token", "properties"]: aux_dict[prop] = turn_context.activity.value.get(prop) token_exchange_request = TokenExchangeInvokeRequest( id=aux_dict["id"], - connection_name=aux_dict["connection_name"], + connection_name=aux_dict["connectionName"], token=aux_dict["token"], properties=aux_dict["properties"], ) try: adapter = turn_context.adapter - if isinstance(turn_context.adapter, ExtendedUserTokenProvider): + + user_token_client: UserTokenClient = turn_context.turn_state.get( + UserTokenClient.__name__, None + ) + if user_token_client: + # If the adapter has UserTokenClient, use it to exchange the token. + token_exchange_response = await user_token_client.exchange_token( + turn_context.activity.from_property.id, + token_exchange_request.connection_name, + turn_context.activity.channel_id, + TokenExchangeRequest(token=token_exchange_request.token), + ) + elif isinstance(turn_context.adapter, ExtendedUserTokenProvider): token_exchange_response = await adapter.exchange_token( turn_context, self._oauth_connection_name, diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py index d31a0b56a..270d4f324 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py @@ -341,6 +341,13 @@ async def _send_oauth_card( if sign_in_resource.token_exchange_resource else None ) + + json_token_ex_post = ( + sign_in_resource.token_post_resource.as_dict() + if sign_in_resource.token_post_resource + else None + ) + prompt.attachments.append( CardFactory.oauth_card( OAuthCard( @@ -355,6 +362,7 @@ async def _send_oauth_card( ) ], token_exchange_resource=json_token_ex_resource, + token_post_resource=json_token_ex_post, ) ) ) diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py index 0f9131871..576c5125c 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json + from typing import Awaitable, Callable, Optional from aiohttp.web import ( @@ -17,6 +19,7 @@ Bot, CloudAdapterBase, InvokeResponse, + serializer_helper, TurnContext, ) from botbuilder.core.streaming import ( @@ -102,7 +105,8 @@ async def process( # Write the response, serializing the InvokeResponse if invoke_response: return json_response( - data=invoke_response.body, status=invoke_response.status + data=serializer_helper(invoke_response.body), + status=invoke_response.status, ) return Response(status=201) else: diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt index 4344965ca..d66ba0327 100644 --- a/libraries/botbuilder-integration-aiohttp/requirements.txt +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -1,4 +1,4 @@ msrest== 0.7.* botframework-connector==4.17.0 botbuilder-schema==4.17.0 -aiohttp==3.10.11 +aiohttp==3.*.* diff --git a/libraries/botbuilder-integration-aiohttp/setup.py b/libraries/botbuilder-integration-aiohttp/setup.py index 635e26fc1..2624c9dc8 100644 --- a/libraries/botbuilder-integration-aiohttp/setup.py +++ b/libraries/botbuilder-integration-aiohttp/setup.py @@ -10,7 +10,7 @@ "botframework-connector==4.17.0", "botbuilder-core==4.17.0", "yarl>=1.8.1", - "aiohttp==3.10.11", + "aiohttp>=3.10,<4.0", ] root = os.path.abspath(os.path.dirname(__file__)) diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py index 3ef0ca426..78c32e5eb 100644 --- a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py +++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py @@ -6,7 +6,7 @@ REQUIRES = [ "applicationinsights>=0.11.9", - "aiohttp==3.10.11", + "aiohttp>=3.10,<4.0", "botbuilder-schema==4.17.0", "botframework-connector==4.17.0", "botbuilder-core==4.17.0", diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py index 1b6a631c6..e7dd1f789 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py @@ -1909,6 +1909,7 @@ class OAuthCard(Model): "connection_name": {"key": "connectionName", "type": "str"}, "buttons": {"key": "buttons", "type": "[CardAction]"}, "token_exchange_resource": {"key": "tokenExchangeResource", "type": "object"}, + "token_post_resource": {"key": "tokenPostResource", "type": "object"}, } def __init__( @@ -1918,6 +1919,7 @@ def __init__( connection_name: str = None, buttons=None, token_exchange_resource=None, + token_post_resource=None, **kwargs ) -> None: super(OAuthCard, self).__init__(**kwargs) @@ -1925,6 +1927,7 @@ def __init__( self.connection_name = connection_name self.buttons = buttons self.token_exchange_resource = token_exchange_resource + self.token_post_resource = token_post_resource class PagedMembersResult(Model): diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py b/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py index f4593e21a..0f1f158da 100644 --- a/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py +++ b/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py @@ -13,6 +13,7 @@ from ._models_py3 import SignInUrlResponse from ._models_py3 import TokenExchangeRequest from ._models_py3 import TokenExchangeResource + from ._models_py3 import TokenPostResource from ._models_py3 import TokenResponse from ._models_py3 import TokenStatus except (SyntaxError, ImportError): @@ -23,6 +24,7 @@ from ._models import SignInUrlResponse from ._models import TokenExchangeRequest from ._models import TokenExchangeResource + from ._models import TokenPostResource from ._models import TokenResponse from ._models import TokenStatus @@ -35,6 +37,7 @@ "SignInUrlResponse", "TokenExchangeRequest", "TokenExchangeResource", + "TokenPostResource", "TokenResponse", "TokenStatus", ] diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/_models.py b/libraries/botframework-connector/botframework/connector/token_api/models/_models.py index 5f69104cd..8b526324a 100644 --- a/libraries/botframework-connector/botframework/connector/token_api/models/_models.py +++ b/libraries/botframework-connector/botframework/connector/token_api/models/_models.py @@ -104,6 +104,9 @@ class SignInUrlResponse(Model): :param token_exchange_resource: :type token_exchange_resource: ~botframework.tokenapi.models.TokenExchangeResource + :param token_post_resource: + :type token_post_resource: + ~botframework.tokenapi.models.TokenPostResource """ _attribute_map = { @@ -112,12 +115,17 @@ class SignInUrlResponse(Model): "key": "tokenExchangeResource", "type": "TokenExchangeResource", }, + "token_post_resource": { + "key": "tokenPostResource", + "type": "TokenPostResource", + }, } def __init__(self, **kwargs): super(SignInUrlResponse, self).__init__(**kwargs) self.sign_in_link = kwargs.get("sign_in_link", None) self.token_exchange_resource = kwargs.get("token_exchange_resource", None) + self.token_exchange_resource = kwargs.get("token_post_resource", None) class TokenExchangeRequest(Model): @@ -164,6 +172,22 @@ def __init__(self, **kwargs): self.provider_id = kwargs.get("provider_id", None) +class TokenPostResource(Model): + """TokenPostResource. + + :param sas_url: + :type id: str + """ + + _attribute_map = { + "sas_url": {"key": "sasUrl", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TokenPostResource, self).__init__(**kwargs) + self.sas_url = kwargs.get("sas_url", None) + + class TokenResponse(Model): """TokenResponse. diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py b/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py index 60ab62c92..512e85356 100644 --- a/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py +++ b/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py @@ -106,6 +106,9 @@ class SignInUrlResponse(Model): :param token_exchange_resource: :type token_exchange_resource: ~botframework.tokenapi.models.TokenExchangeResource + :param token_post_resource: + :type token_post_resource: + ~botframework.tokenapi.models.TokenPostResource """ _attribute_map = { @@ -114,14 +117,24 @@ class SignInUrlResponse(Model): "key": "tokenExchangeResource", "type": "TokenExchangeResource", }, + "token_post_resource": { + "key": "tokenPostResource", + "type": "TokenPostResource", + }, } def __init__( - self, *, sign_in_link: str = None, token_exchange_resource=None, **kwargs + self, + *, + sign_in_link: str = None, + token_exchange_resource=None, + token_post_resource=None, + **kwargs ) -> None: super(SignInUrlResponse, self).__init__(**kwargs) self.sign_in_link = sign_in_link self.token_exchange_resource = token_exchange_resource + self.token_post_resource = token_post_resource class TokenExchangeRequest(Model): @@ -170,6 +183,22 @@ def __init__( self.provider_id = provider_id +class TokenPostResource(Model): + """TokenPostResource. + + :param sas_url: + :type id: str + """ + + _attribute_map = { + "sas_url": {"key": "sasUrl", "type": "str"}, + } + + def __init__(self, *, sas_url: str = None, **kwargs) -> None: + super(TokenPostResource, self).__init__(**kwargs) + self.sas_url = sas_url + + class TokenResponse(Model): """TokenResponse. diff --git a/tests/skills/skills-buffered/child/requirements.txt b/tests/skills/skills-buffered/child/requirements.txt index 20f8f8fe5..9e79c8115 100644 --- a/tests/skills/skills-buffered/child/requirements.txt +++ b/tests/skills/skills-buffered/child/requirements.txt @@ -1,2 +1,2 @@ botbuilder-core>=4.7.1 -aiohttp +aiohttp==3.*.* diff --git a/tests/skills/skills-buffered/parent/requirements.txt b/tests/skills/skills-buffered/parent/requirements.txt index 20f8f8fe5..9e79c8115 100644 --- a/tests/skills/skills-buffered/parent/requirements.txt +++ b/tests/skills/skills-buffered/parent/requirements.txt @@ -1,2 +1,2 @@ botbuilder-core>=4.7.1 -aiohttp +aiohttp==3.*.*