From f3392d6081de1db77092c69dc9cc716e61204c90 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Thu, 20 Feb 2020 10:23:04 -0600 Subject: [PATCH] Fixes #525: Additional auth flow --- .../botbuilder/core/adapters/test_adapter.py | 37 +- .../botbuilder/core/bot_framework_adapter.py | 131 +++--- .../botbuilder/core/user_token_provider.py | 175 +++++--- .../tests/test_test_adapter.py | 384 +++++++++++------- .../dialogs/prompts/oauth_prompt.py | 34 +- .../dialogs/prompts/oauth_prompt_settings.py | 51 ++- 6 files changed, 533 insertions(+), 279 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py index 0ff9f16b6..77e8cd54c 100644 --- a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py @@ -20,7 +20,7 @@ ResourceResponse, TokenResponse, ) -from botframework.connector.auth import ClaimsIdentity +from botframework.connector.auth import ClaimsIdentity, AppCredentials from ..bot_adapter import BotAdapter from ..turn_context import TurnContext from ..user_token_provider import UserTokenProvider @@ -269,7 +269,11 @@ def add_user_token( self._magic_codes.append(code) async def get_user_token( - self, context: TurnContext, connection_name: str, magic_code: str = None + self, + context: TurnContext, + connection_name: str, + magic_code: str = None, + oauth_app_credentials: AppCredentials = None, ) -> TokenResponse: key = UserToken() key.channel_id = context.activity.channel_id @@ -305,7 +309,11 @@ async def get_user_token( return None async def sign_out_user( - self, context: TurnContext, connection_name: str, user_id: str = None + self, + context: TurnContext, + connection_name: str = None, + user_id: str = None, + oauth_app_credentials: AppCredentials = None, ): channel_id = context.activity.channel_id user_id = context.activity.from_property.id @@ -321,15 +329,34 @@ async def sign_out_user( self._user_tokens = new_records async def get_oauth_sign_in_link( - self, context: TurnContext, connection_name: str + self, + context: TurnContext, + connection_name: str, + final_redirect: str = None, + oauth_app_credentials: AppCredentials = None, ) -> str: return ( f"https://fake.com/oauthsignin" f"/{connection_name}/{context.activity.channel_id}/{context.activity.from_property.id}" ) + async def get_token_status( + self, + context: TurnContext, + connection_name: str = None, + user_id: str = None, + include_filter: str = None, + oauth_app_credentials: AppCredentials = None, + ) -> Dict[str, TokenResponse]: + return None + async def get_aad_tokens( - self, context: TurnContext, connection_name: str, resource_urls: List[str] + self, + context: TurnContext, + connection_name: str, + resource_urls: List[str], + user_id: str = None, + oauth_app_credentials: AppCredentials = None, ) -> Dict[str, TokenResponse]: return None diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py index 3f248147f..37b22baaf 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py @@ -25,9 +25,9 @@ SimpleCredentialProvider, SkillValidation, CertificateAppCredentials, + AppCredentials, ) from botframework.connector.token_api import TokenApiClient -from botframework.connector.token_api.models import TokenStatus from botbuilder.schema import ( Activity, ActivityTypes, @@ -730,7 +730,11 @@ async def get_conversations(self, service_url: str, continuation_token: str = No return await client.conversations.get_conversations(continuation_token) async def get_user_token( - self, context: TurnContext, connection_name: str, magic_code: str = None + self, + context: TurnContext, + connection_name: str, + magic_code: str = None, + oauth_app_credentials: AppCredentials = None, ) -> TokenResponse: """ @@ -742,6 +746,8 @@ async def get_user_token( :type connection_name: str :param magic_code" (Optional) user entered code to validate :str magic_code" str + :param oauth_app_credentials: (Optional) AppCredentials for OAuth. + :type oauth_app_credentials: :class:`botframework.connector.auth.AppCredential` :raises: An exception error @@ -762,24 +768,27 @@ async def get_user_token( "get_user_token() requires a connection_name but none was provided." ) - self.check_emulating_oauth_cards(context) - user_id = context.activity.from_property.id - url = self.oauth_api_url(context) - client = self.create_token_api_client(url) + client = self._create_token_api_client(context, oauth_app_credentials) result = client.user_token.get_token( - user_id, connection_name, context.activity.channel_id, magic_code + context.activity.from_property.id, + connection_name, + context.activity.channel_id, + magic_code, ) - # TODO check form of response if result is None or result.token is None: return None return result async def sign_out_user( - self, context: TurnContext, connection_name: str = None, user_id: str = None - ) -> str: + self, + context: TurnContext, + connection_name: str = None, + user_id: str = None, + oauth_app_credentials: AppCredentials = None, + ): """ Signs the user out with the token server. @@ -789,8 +798,8 @@ async def sign_out_user( :type connection_name: str :param user_id: User id of user to sign out :type user_id: str - - :returns: A task that represents the work queued to execute + :param oauth_app_credentials: (Optional) AppCredentials for OAuth. + :type oauth_app_credentials: :class:`botframework.connector.auth.AppCredential` """ if not context.activity.from_property or not context.activity.from_property.id: raise Exception( @@ -799,15 +808,17 @@ async def sign_out_user( if not user_id: user_id = context.activity.from_property.id - self.check_emulating_oauth_cards(context) - url = self.oauth_api_url(context) - client = self.create_token_api_client(url) + client = self._create_token_api_client(context, oauth_app_credentials) client.user_token.sign_out( user_id, connection_name, context.activity.channel_id ) async def get_oauth_sign_in_link( - self, context: TurnContext, connection_name: str + self, + context: TurnContext, + connection_name: str, + final_redirect: str = None, + oauth_app_credentials: AppCredentials = None, ) -> str: """ Gets the raw sign-in link to be sent to the user for sign-in for a connection name. @@ -816,17 +827,16 @@ async def get_oauth_sign_in_link( :type context: :class:`botbuilder.core.TurnContext` :param connection_name: Name of the auth connection to use :type connection_name: str + :param final_redirect: The final URL that the OAuth flow will redirect to. + :param oauth_app_credentials: (Optional) AppCredentials for OAuth. + :type oauth_app_credentials: :class:`botframework.connector.auth.AppCredential` - :returns: A task that represents the work queued to execute + :return: If the task completes successfully, the result contains the raw sign-in link + """ - .. note:: + client = self._create_token_api_client(context, oauth_app_credentials) - If the task completes successfully, the result contains the raw sign-in link - """ - self.check_emulating_oauth_cards(context) conversation = TurnContext.get_conversation_reference(context.activity) - url = self.oauth_api_url(context) - client = self.create_token_api_client(url) state = TokenExchangeState( connection_name=connection_name, conversation=conversation, @@ -840,19 +850,27 @@ async def get_oauth_sign_in_link( return client.bot_sign_in.get_sign_in_url(final_state) async def get_token_status( - self, context: TurnContext, user_id: str = None, include_filter: str = None - ) -> List[TokenStatus]: - + self, + context: TurnContext, + connection_name: str = None, + user_id: str = None, + include_filter: str = None, + oauth_app_credentials: AppCredentials = None, + ) -> Dict[str, TokenResponse]: """ Retrieves the token status for each configured connection for the given user. :param context: Context for the current turn of conversation with the user :type context: :class:`botbuilder.core.TurnContext` - :param user_id: The user Id for which token status is retrieved + :param connection_name: Name of the auth connection to use + :type connection_name: str + :param user_id: The user Id for which tokens are retrieved. If passing in None the userId is taken :type user_id: str :param include_filter: (Optional) Comma separated list of connection's to include. Blank will return token status for all configured connections. :type include_filter: str + :param oauth_app_credentials: (Optional) AppCredentials for OAuth. + :type oauth_app_credentials: :class:`botframework.connector.auth.AppCredential` :returns: Array of :class:`botframework.connector.token_api.modelsTokenStatus` """ @@ -864,18 +882,20 @@ async def get_token_status( "BotFrameworkAdapter.get_token_status(): missing from_property or from_property.id" ) - self.check_emulating_oauth_cards(context) - user_id = user_id or context.activity.from_property.id - url = self.oauth_api_url(context) - client = self.create_token_api_client(url) + client = self._create_token_api_client(context, oauth_app_credentials) - # TODO check form of response + user_id = user_id or context.activity.from_property.id return client.user_token.get_token_status( user_id, context.activity.channel_id, include_filter ) async def get_aad_tokens( - self, context: TurnContext, connection_name: str, resource_urls: List[str] + self, + context: TurnContext, + connection_name: str, + resource_urls: List[str], + user_id: str = None, + oauth_app_credentials: AppCredentials = None, ) -> Dict[str, TokenResponse]: """ Retrieves Azure Active Directory tokens for particular resources on a configured connection. @@ -886,6 +906,12 @@ async def get_aad_tokens( :type connection_name: str :param resource_urls: The list of resource URLs to retrieve tokens for :type resource_urls: :class:`typing.List` + :param user_id: The user Id for which tokens are retrieved. If passing in null the userId is taken + from the Activity in the TurnContext. + :type user_id: str + :param oauth_app_credentials: (Optional) AppCredentials for OAuth. + :type oauth_app_credentials: :class:`botframework.connector.auth.AppCredential` + :returns: Dictionary of resource Urls to the corresponding :class:'botbuilder.schema.TokenResponse` :rtype: :class:`typing.Dict` """ @@ -894,14 +920,12 @@ async def get_aad_tokens( "BotFrameworkAdapter.get_aad_tokens(): missing from_property or from_property.id" ) - self.check_emulating_oauth_cards(context) - user_id = context.activity.from_property.id - url = self.oauth_api_url(context) - client = self.create_token_api_client(url) - - # TODO check form of response + client = self._create_token_api_client(context, oauth_app_credentials) return client.user_token.get_aad_tokens( - user_id, connection_name, context.activity.channel_id, resource_urls + context.activity.from_property.id, + connection_name, + context.activity.channel_id, + resource_urls, ) async def create_connector_client( @@ -959,20 +983,31 @@ async def create_connector_client( return client - def create_token_api_client(self, service_url: str) -> TokenApiClient: - client = TokenApiClient(self._credentials, service_url) - client.config.add_user_agent(USER_AGENT) + def _create_token_api_client( + self, + url_or_context: Union[TurnContext, str], + oauth_app_credentials: AppCredentials = None, + ) -> TokenApiClient: + if isinstance(url_or_context, str): + app_credentials = ( + oauth_app_credentials if oauth_app_credentials else self._credentials + ) + client = TokenApiClient(app_credentials, url_or_context) + client.config.add_user_agent(USER_AGENT) + return client - return client + self.__check_emulating_oauth_cards(url_or_context) + url = self.__oauth_api_url(url_or_context) + return self._create_token_api_client(url) - async def emulate_oauth_cards( + async def __emulate_oauth_cards( self, context_or_service_url: Union[TurnContext, str], emulate: bool ): self._is_emulating_oauth_cards = emulate - url = self.oauth_api_url(context_or_service_url) + url = self.__oauth_api_url(context_or_service_url) await EmulatorApiClient.emulate_oauth_cards(self._credentials, url, emulate) - def oauth_api_url(self, context_or_service_url: Union[TurnContext, str]) -> str: + def __oauth_api_url(self, context_or_service_url: Union[TurnContext, str]) -> str: url = None if self._is_emulating_oauth_cards: url = ( @@ -992,7 +1027,7 @@ def oauth_api_url(self, context_or_service_url: Union[TurnContext, str]) -> str: return url - def check_emulating_oauth_cards(self, context: TurnContext): + def __check_emulating_oauth_cards(self, context: TurnContext): if ( not self._is_emulating_oauth_cards and context.activity.channel_id == "emulator" diff --git a/libraries/botbuilder-core/botbuilder/core/user_token_provider.py b/libraries/botbuilder-core/botbuilder/core/user_token_provider.py index 4316a2f88..735af1e7a 100644 --- a/libraries/botbuilder-core/botbuilder/core/user_token_provider.py +++ b/libraries/botbuilder-core/botbuilder/core/user_token_provider.py @@ -1,62 +1,113 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from abc import ABC, abstractmethod -from typing import Dict, List - -from botbuilder.schema import TokenResponse - -from .turn_context import TurnContext - - -class UserTokenProvider(ABC): - @abstractmethod - async def get_user_token( - self, context: TurnContext, connection_name: str, magic_code: str = None - ) -> TokenResponse: - """ - Retrieves the OAuth token for a user that is in a sign-in flow. - :param context: - :param connection_name: - :param magic_code: - :return: - """ - raise NotImplementedError() - - @abstractmethod - async def sign_out_user( - self, context: TurnContext, connection_name: str, user_id: str = None - ): - """ - Signs the user out with the token server. - :param context: - :param connection_name: - :param user_id: - :return: - """ - raise NotImplementedError() - - @abstractmethod - async def get_oauth_sign_in_link( - self, context: TurnContext, connection_name: str - ) -> str: - """ - Get the raw signin link to be sent to the user for signin for a connection name. - :param context: - :param connection_name: - :return: - """ - raise NotImplementedError() - - @abstractmethod - async def get_aad_tokens( - self, context: TurnContext, connection_name: str, resource_urls: List[str] - ) -> Dict[str, TokenResponse]: - """ - Retrieves Azure Active Directory tokens for particular resources on a configured connection. - :param context: - :param connection_name: - :param resource_urls: - :return: - """ - raise NotImplementedError() +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from abc import ABC, abstractmethod +from typing import Dict, List + +from botbuilder.schema import TokenResponse +from botframework.connector.auth import AppCredentials + +from .turn_context import TurnContext + + +class UserTokenProvider(ABC): + @abstractmethod + async def get_user_token( + self, + context: TurnContext, + connection_name: str, + magic_code: str = None, + oauth_app_credentials: AppCredentials = None, + ) -> TokenResponse: + """ + Retrieves the OAuth token for a user that is in a sign-in flow. + :param context: Context for the current turn of conversation with the user. + :param connection_name: Name of the auth connection to use. + :param magic_code: (Optional) Optional user entered code to validate. + :param oauth_app_credentials: (Optional) AppCredentials for OAuth. If None is supplied, the + Bots credentials are used. + :return: + """ + raise NotImplementedError() + + @abstractmethod + async def sign_out_user( + self, + context: TurnContext, + connection_name: str = None, + user_id: str = None, + oauth_app_credentials: AppCredentials = None, + ): + """ + Signs the user out with the token server. + :param context: Context for the current turn of conversation with the user. + :param connection_name: Name of the auth connection to use. + :param user_id: User id of user to sign out. + :param oauth_app_credentials: (Optional) AppCredentials for OAuth. If None is supplied, the + Bots credentials are used. + :return: + """ + raise NotImplementedError() + + @abstractmethod + async def get_oauth_sign_in_link( + self, + context: TurnContext, + connection_name: str, + final_redirect: str = None, + oauth_app_credentials: AppCredentials = None, + ) -> str: + """ + Get the raw signin link to be sent to the user for signin for a connection name. + :param context: Context for the current turn of conversation with the user. + :param connection_name: Name of the auth connection to use. + :param final_redirect: The final URL that the OAuth flow will redirect to. + :param oauth_app_credentials: (Optional) AppCredentials for OAuth. If None is supplied, the + Bots credentials are used. + :return: + """ + raise NotImplementedError() + + @abstractmethod + async def get_token_status( + self, + context: TurnContext, + connection_name: str = None, + user_id: str = None, + include_filter: str = None, + oauth_app_credentials: AppCredentials = None, + ) -> Dict[str, TokenResponse]: + """ + Retrieves Azure Active Directory tokens for particular resources on a configured connection. + :param context: Context for the current turn of conversation with the user. + :param connection_name: Name of the auth connection to use. + :param user_id: The user Id for which token status is retrieved. + :param include_filter: Optional comma separated list of connection's to include. Blank will return token status + for all configured connections. + :param oauth_app_credentials: (Optional) AppCredentials for OAuth. If None is supplied, the + Bots credentials are used. + :return: + """ + raise NotImplementedError() + + @abstractmethod + async def get_aad_tokens( + self, + context: TurnContext, + connection_name: str, + resource_urls: List[str], + user_id: str = None, + oauth_app_credentials: AppCredentials = None, + ) -> Dict[str, TokenResponse]: + """ + Retrieves Azure Active Directory tokens for particular resources on a configured connection. + :param context: Context for the current turn of conversation with the user. + :param connection_name: Name of the auth connection to use. + :param resource_urls: The list of resource URLs to retrieve tokens for. + :param user_id: The user Id for which tokens are retrieved. If passing in None the userId is taken + from the Activity in the TurnContext. + :param oauth_app_credentials: (Optional) AppCredentials for OAuth. If None is supplied, the + Bots credentials are used. + :return: + """ + raise NotImplementedError() diff --git a/libraries/botbuilder-core/tests/test_test_adapter.py b/libraries/botbuilder-core/tests/test_test_adapter.py index 1d095c222..4312ca352 100644 --- a/libraries/botbuilder-core/tests/test_test_adapter.py +++ b/libraries/botbuilder-core/tests/test_test_adapter.py @@ -1,137 +1,247 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import aiounittest -from botbuilder.schema import Activity, ConversationReference -from botbuilder.core import TurnContext -from botbuilder.core.adapters import TestAdapter - -RECEIVED_MESSAGE = Activity(type="message", text="received") -UPDATED_ACTIVITY = Activity(type="message", text="update") -DELETED_ACTIVITY_REFERENCE = ConversationReference(activity_id="1234") - - -class TestTestAdapter(aiounittest.AsyncTestCase): - async def test_should_call_bog_logic_when_receive_activity_is_called(self): - async def logic(context: TurnContext): - assert context - assert context.activity - assert context.activity.type == "message" - assert context.activity.text == "test" - assert context.activity.id - assert context.activity.from_property - assert context.activity.recipient - assert context.activity.conversation - assert context.activity.channel_id - assert context.activity.service_url - - adapter = TestAdapter(logic) - await adapter.receive_activity("test") - - async def test_should_support_receive_activity_with_activity(self): - async def logic(context: TurnContext): - assert context.activity.type == "message" - assert context.activity.text == "test" - - adapter = TestAdapter(logic) - await adapter.receive_activity(Activity(type="message", text="test")) - - async def test_should_set_activity_type_when_receive_activity_receives_activity_without_type( - self, - ): - async def logic(context: TurnContext): - assert context.activity.type == "message" - assert context.activity.text == "test" - - adapter = TestAdapter(logic) - await adapter.receive_activity(Activity(text="test")) - - async def test_should_support_custom_activity_id_in_receive_activity(self): - async def logic(context: TurnContext): - assert context.activity.id == "myId" - assert context.activity.type == "message" - assert context.activity.text == "test" - - adapter = TestAdapter(logic) - await adapter.receive_activity(Activity(type="message", text="test", id="myId")) - - async def test_should_call_bot_logic_when_send_is_called(self): - async def logic(context: TurnContext): - assert context.activity.text == "test" - - adapter = TestAdapter(logic) - await adapter.send("test") - - async def test_should_send_and_receive_when_test_is_called(self): - async def logic(context: TurnContext): - await context.send_activity(RECEIVED_MESSAGE) - - adapter = TestAdapter(logic) - await adapter.test("test", "received") - - async def test_should_send_and_throw_assertion_error_when_test_is_called(self): - async def logic(context: TurnContext): - await context.send_activity(RECEIVED_MESSAGE) - - adapter = TestAdapter(logic) - try: - await adapter.test("test", "foobar") - except AssertionError: - pass - else: - raise AssertionError("Assertion error should have been raised") - - async def test_tests_should_call_test_for_each_tuple(self): - counter = 0 - - async def logic(context: TurnContext): - nonlocal counter - counter += 1 - await context.send_activity(Activity(type="message", text=str(counter))) - - adapter = TestAdapter(logic) - await adapter.tests(("test", "1"), ("test", "2"), ("test", "3")) - assert counter == 3 - - async def test_tests_should_call_test_for_each_list(self): - counter = 0 - - async def logic(context: TurnContext): - nonlocal counter - counter += 1 - await context.send_activity(Activity(type="message", text=str(counter))) - - adapter = TestAdapter(logic) - await adapter.tests(["test", "1"], ["test", "2"], ["test", "3"]) - assert counter == 3 - - async def test_should_assert_reply_after_send(self): - async def logic(context: TurnContext): - await context.send_activity(RECEIVED_MESSAGE) - - adapter = TestAdapter(logic) - test_flow = await adapter.send("test") - await test_flow.assert_reply("received") - - async def test_should_support_context_update_activity_call(self): - async def logic(context: TurnContext): - await context.update_activity(UPDATED_ACTIVITY) - await context.send_activity(RECEIVED_MESSAGE) - - adapter = TestAdapter(logic) - await adapter.test("test", "received") - assert len(adapter.updated_activities) == 1 - assert adapter.updated_activities[0].text == UPDATED_ACTIVITY.text - - async def test_should_support_context_delete_activity_call(self): - async def logic(context: TurnContext): - await context.delete_activity(DELETED_ACTIVITY_REFERENCE) - await context.send_activity(RECEIVED_MESSAGE) - - adapter = TestAdapter(logic) - await adapter.test("test", "received") - assert len(adapter.deleted_activities) == 1 - assert ( - adapter.deleted_activities[0].activity_id - == DELETED_ACTIVITY_REFERENCE.activity_id - ) +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest + +from botbuilder.core import TurnContext +from botbuilder.core.adapters import TestAdapter +from botbuilder.schema import Activity, ConversationReference, ChannelAccount +from botframework.connector.auth import MicrosoftAppCredentials + +RECEIVED_MESSAGE = Activity(type="message", text="received") +UPDATED_ACTIVITY = Activity(type="message", text="update") +DELETED_ACTIVITY_REFERENCE = ConversationReference(activity_id="1234") + + +class TestTestAdapter(aiounittest.AsyncTestCase): + async def test_should_call_bog_logic_when_receive_activity_is_called(self): + async def logic(context: TurnContext): + assert context + assert context.activity + assert context.activity.type == "message" + assert context.activity.text == "test" + assert context.activity.id + assert context.activity.from_property + assert context.activity.recipient + assert context.activity.conversation + assert context.activity.channel_id + assert context.activity.service_url + + adapter = TestAdapter(logic) + await adapter.receive_activity("test") + + async def test_should_support_receive_activity_with_activity(self): + async def logic(context: TurnContext): + assert context.activity.type == "message" + assert context.activity.text == "test" + + adapter = TestAdapter(logic) + await adapter.receive_activity(Activity(type="message", text="test")) + + async def test_should_set_activity_type_when_receive_activity_receives_activity_without_type( + self, + ): + async def logic(context: TurnContext): + assert context.activity.type == "message" + assert context.activity.text == "test" + + adapter = TestAdapter(logic) + await adapter.receive_activity(Activity(text="test")) + + async def test_should_support_custom_activity_id_in_receive_activity(self): + async def logic(context: TurnContext): + assert context.activity.id == "myId" + assert context.activity.type == "message" + assert context.activity.text == "test" + + adapter = TestAdapter(logic) + await adapter.receive_activity(Activity(type="message", text="test", id="myId")) + + async def test_should_call_bot_logic_when_send_is_called(self): + async def logic(context: TurnContext): + assert context.activity.text == "test" + + adapter = TestAdapter(logic) + await adapter.send("test") + + async def test_should_send_and_receive_when_test_is_called(self): + async def logic(context: TurnContext): + await context.send_activity(RECEIVED_MESSAGE) + + adapter = TestAdapter(logic) + await adapter.test("test", "received") + + async def test_should_send_and_throw_assertion_error_when_test_is_called(self): + async def logic(context: TurnContext): + await context.send_activity(RECEIVED_MESSAGE) + + adapter = TestAdapter(logic) + try: + await adapter.test("test", "foobar") + except AssertionError: + pass + else: + raise AssertionError("Assertion error should have been raised") + + async def test_tests_should_call_test_for_each_tuple(self): + counter = 0 + + async def logic(context: TurnContext): + nonlocal counter + counter += 1 + await context.send_activity(Activity(type="message", text=str(counter))) + + adapter = TestAdapter(logic) + await adapter.tests(("test", "1"), ("test", "2"), ("test", "3")) + assert counter == 3 + + async def test_tests_should_call_test_for_each_list(self): + counter = 0 + + async def logic(context: TurnContext): + nonlocal counter + counter += 1 + await context.send_activity(Activity(type="message", text=str(counter))) + + adapter = TestAdapter(logic) + await adapter.tests(["test", "1"], ["test", "2"], ["test", "3"]) + assert counter == 3 + + async def test_should_assert_reply_after_send(self): + async def logic(context: TurnContext): + await context.send_activity(RECEIVED_MESSAGE) + + adapter = TestAdapter(logic) + test_flow = await adapter.send("test") + await test_flow.assert_reply("received") + + async def test_should_support_context_update_activity_call(self): + async def logic(context: TurnContext): + await context.update_activity(UPDATED_ACTIVITY) + await context.send_activity(RECEIVED_MESSAGE) + + adapter = TestAdapter(logic) + await adapter.test("test", "received") + assert len(adapter.updated_activities) == 1 + assert adapter.updated_activities[0].text == UPDATED_ACTIVITY.text + + async def test_should_support_context_delete_activity_call(self): + async def logic(context: TurnContext): + await context.delete_activity(DELETED_ACTIVITY_REFERENCE) + await context.send_activity(RECEIVED_MESSAGE) + + adapter = TestAdapter(logic) + await adapter.test("test", "received") + assert len(adapter.deleted_activities) == 1 + assert ( + adapter.deleted_activities[0].activity_id + == DELETED_ACTIVITY_REFERENCE.activity_id + ) + + async def test_get_user_token_returns_null(self): + adapter = TestAdapter() + activity = Activity( + channel_id="directline", from_property=ChannelAccount(id="testuser") + ) + + turn_context = TurnContext(adapter, activity) + + token_response = await adapter.get_user_token(turn_context, "myConnection") + assert not token_response + + oauth_app_credentials = MicrosoftAppCredentials(None, None) + token_response = await adapter.get_user_token( + turn_context, "myConnection", oauth_app_credentials=oauth_app_credentials + ) + assert not token_response + + async def test_get_user_token_returns_null_with_code(self): + adapter = TestAdapter() + activity = Activity( + channel_id="directline", from_property=ChannelAccount(id="testuser") + ) + + turn_context = TurnContext(adapter, activity) + + token_response = await adapter.get_user_token( + turn_context, "myConnection", "abc123" + ) + assert not token_response + + oauth_app_credentials = MicrosoftAppCredentials(None, None) + token_response = await adapter.get_user_token( + turn_context, + "myConnection", + "abc123", + oauth_app_credentials=oauth_app_credentials, + ) + assert not token_response + + async def test_get_user_token_returns_token(self): + adapter = TestAdapter() + connection_name = "myConnection" + channel_id = "directline" + user_id = "testUser" + token = "abc123" + activity = Activity( + channel_id=channel_id, from_property=ChannelAccount(id=user_id) + ) + + turn_context = TurnContext(adapter, activity) + + adapter.add_user_token(connection_name, channel_id, user_id, token) + + token_response = await adapter.get_user_token(turn_context, connection_name) + assert token_response + assert token == token_response.token + assert connection_name == token_response.connection_name + + oauth_app_credentials = MicrosoftAppCredentials(None, None) + token_response = await adapter.get_user_token( + turn_context, connection_name, oauth_app_credentials=oauth_app_credentials + ) + assert token_response + assert token == token_response.token + assert connection_name == token_response.connection_name + + async def test_get_user_token_returns_token_with_magice_code(self): + adapter = TestAdapter() + connection_name = "myConnection" + channel_id = "directline" + user_id = "testUser" + token = "abc123" + magic_code = "888999" + activity = Activity( + channel_id=channel_id, from_property=ChannelAccount(id=user_id) + ) + + turn_context = TurnContext(adapter, activity) + + adapter.add_user_token(connection_name, channel_id, user_id, token, magic_code) + + # First no magic_code + token_response = await adapter.get_user_token(turn_context, connection_name) + assert not token_response + + # Can be retrieved with magic code + token_response = await adapter.get_user_token( + turn_context, connection_name, magic_code + ) + assert token_response + assert token == token_response.token + assert connection_name == token_response.connection_name + + # Then can be retrieved without magic code + token_response = await adapter.get_user_token(turn_context, connection_name) + assert token_response + assert token == token_response.token + assert connection_name == token_response.connection_name + + # Then can be retrieved using customized AppCredentials + oauth_app_credentials = MicrosoftAppCredentials(None, None) + token_response = await adapter.get_user_token( + turn_context, connection_name, oauth_app_credentials=oauth_app_credentials + ) + assert token_response + assert token == token_response.token + assert connection_name == token_response.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 a8cd05048..0f9a82453 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py @@ -143,7 +143,10 @@ async def begin_dialog( ) output = await dialog_context.context.adapter.get_user_token( - dialog_context.context, self._settings.connection_name, None + dialog_context.context, + self._settings.connection_name, + None, + self._settings.oath_app_credentials, ) if output is not None: @@ -220,6 +223,8 @@ async def get_user_token( :param context: Context for the current turn of conversation with the user :type context: :class:`TurnContext` + :param code: (Optional) Optional user entered code to validate. + :type code: str :return: A response that includes the user's token :rtype: :class:`TokenResponse` @@ -237,7 +242,10 @@ async def get_user_token( ) return await adapter.get_user_token( - context, self._settings.connection_name, code + context, + self._settings.connection_name, + code, + self._settings.oath_app_credentials, ) async def sign_out_user(self, context: TurnContext): @@ -260,7 +268,12 @@ async def sign_out_user(self, context: TurnContext): "OAuthPrompt.sign_out_user(): not supported for the current adapter." ) - return await adapter.sign_out_user(context, self._settings.connection_name) + return await adapter.sign_out_user( + context, + self._settings.connection_name, + None, + self._settings.oath_app_credentials, + ) async def _send_oauth_card( self, context: TurnContext, prompt: Union[Activity, str] = None @@ -288,13 +301,19 @@ async def _send_oauth_card( "OAuthPrompt: get_oauth_sign_in_link() not supported by the current adapter" ) link = await context.adapter.get_oauth_sign_in_link( - context, self._settings.connection_name + context, + self._settings.connection_name, + None, + self._settings.oath_app_credentials, ) elif bot_identity and SkillValidation.is_skill_claim( bot_identity.claims ): link = await context.adapter.get_oauth_sign_in_link( - context, self._settings.connection_name + context, + self._settings.connection_name, + None, + self._settings.oath_app_credentials, ) card_action_type = ActionTypes.open_url @@ -325,7 +344,10 @@ async def _send_oauth_card( ) link = await context.adapter.get_oauth_sign_in_link( - context, self._settings.connection_name + context, + self._settings.connection_name, + None, + self._settings.oath_app_credentials, ) prompt.attachments.append( CardFactory.signin_card( diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt_settings.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt_settings.py index 4eec4881a..1d8f04eca 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt_settings.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt_settings.py @@ -1,21 +1,30 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class OAuthPromptSettings: - def __init__( - self, connection_name: str, title: str, text: str = None, timeout: int = None - ): - """ - Settings used to configure an `OAuthPrompt` instance. - Parameters: - connection_name (str): Name of the OAuth connection being used. - title (str): The title of the cards signin button. - text (str): (Optional) additional text included on the signin card. - timeout (int): (Optional) number of milliseconds the prompt will wait for the user to authenticate. - `OAuthPrompt` defaults value to `900,000` ms (15 minutes). - """ - self.connection_name = connection_name - self.title = title - self.text = text - self.timeout = timeout +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from botframework.connector.auth import AppCredentials + + +class OAuthPromptSettings: + def __init__( + self, + connection_name: str, + title: str, + text: str = None, + timeout: int = None, + oauth_app_credentials: AppCredentials = None, + ): + """ + Settings used to configure an `OAuthPrompt` instance. + Parameters: + connection_name (str): Name of the OAuth connection being used. + title (str): The title of the cards signin button. + text (str): (Optional) additional text included on the signin card. + timeout (int): (Optional) number of milliseconds the prompt will wait for the user to authenticate. + `OAuthPrompt` defaults value to `900,000` ms (15 minutes). + oauth_app_credentials (AppCredentials): (Optional) AppCredentials to use for OAuth. If None, + the Bots credentials are used. + """ + self.connection_name = connection_name + self.title = title + self.text = text + self.timeout = timeout + self.oath_app_credentials = oauth_app_credentials