Repository: microsoft/botbuilder-python

Issue:
[BotFrameworkAdapter] Process_activity returns HTTP 412 error when exchanging a token
## Version
4.12.0

## Describe the bug
When performing a token exchange operation between a Host and a Skill, the Host intercepts the oAuthCard sent from the Skill and sends back the Activity with the TokenExchangeInvokeRequest value, returning a response of **412** status and a body of `The bot is unable to exchange token. Proceed with regular login.`. 
This issue occurs in `BotFrameworkAdapter.exchange_token_from_credentials` method when the `UserTokenOperation.exchange_async` method from `BotFramework.Connector` gets executed returning the `BotFramework.Connector.TokenResponse`, followed by a comparison against `BotBuilder.Schema.TokenResponse` using the `isinstance` function causing to differ and raise the error ([Line#1270](https://github.com/microsoft/botbuilder-python/blob/main/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py#L1270)).

Another behavior that came up as part of this issue discovered, when the `Adapter.process_activity` returns the `InvokeResponse `([Line#512](https://github.com/microsoft/botbuilder-python/blob/main/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py#L512)), the body (`TokenExchangeInvokeResponse`) is not serialized leaving the user to do it as part of the bot.

## To Reproduce
Steps to reproduce the behavior:
1. Use the [DotNet Waterfall Host](https://github.com/microsoft/BotFramework-FunctionalTests/tree/main/Bots/DotNet/Consumers/CodeFirst/WaterfallHostBot) and the [Python Waterfall Skill](https://github.com/microsoft/BotFramework-FunctionalTests/tree/main/Bots/Python/Skills/CodeFirst/WaterfallSkillBot) bot samples from the BotFramework-FunctionalTests repo.
2. Configure the App Registrations required to test Sso. [link](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication-sso?view=azure-bot-service-4.0&tabs=csharp%2Ceml).
3. Use WebChat or Emulator for the host. 
4. Select `normal` delivery mode.
5. Select `Waterfall` group.
6. Select `WaterfallSkillBotPython` skill.
7. Select the `Sso` option.
8. Login in the Host.
9. Call the Skill with Sso.
10. Select Login in the skill.
11. It will not show the token and ask to SignIn.
12. The ProcessActivityAsync executed in the [SendTokenExchangeInvokeToSkillAsync](https://github.com/microsoft/BotFramework-FunctionalTests/blob/main/Bots/DotNet/Consumers/CodeFirst/WaterfallHostBot/TokenExchangeSkillHandler.cs#L172) will return a response of **412** status.

## Expected behavior
- Resolve the correct `TokenResponse` instance comparing the `exchange_async` method result against `BotFramework.Connector.TokenResponse` and not `BotBuilder.Schema.TokenResponse`.
- Serialize the `InvokeResponse` body when the `process_activity` method gets executed.

## Screenshots
Host and Skill Sso communication failure.
![image](https://user-images.githubusercontent.com/62260472/113896016-ec995c80-979f-11eb-8770-8a832b128444.png)
![image](https://user-images.githubusercontent.com/62260472/113913432-b5807680-97b2-11eb-9d90-479ae8019c24.png)

InvokeResponse body isn't serialized.
![image](https://user-images.githubusercontent.com/62260472/113913395-abf70e80-97b2-11eb-90c6-8da5af578c3c.png)



Discussion:

Resolving patch:
diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
--- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
+++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
@@ -36,6 +36,7 @@
     TokenStatus,
     TokenExchangeRequest,
     SignInUrlResponse,
+    TokenResponse as ConnectorTokenResponse,
 )
 from botbuilder.schema import (
     Activity,
@@ -509,7 +510,10 @@ async def process_activity_with_identity(
             )
             if invoke_response is None:
                 return InvokeResponse(status=int(HTTPStatus.NOT_IMPLEMENTED))
-            return invoke_response.value
+            return InvokeResponse(
+                status=invoke_response.value.status,
+                body=invoke_response.value.body.serialize(),
+            )
 
         return None
 
@@ -1267,8 +1271,13 @@ async def exchange_token_from_credentials(
             exchange_request.token,
         )
 
-        if isinstance(result, TokenResponse):
-            return result
+        if isinstance(result, ConnectorTokenResponse):
+            return TokenResponse(
+                channel_id=result.channel_id,
+                connection_name=result.connection_name,
+                token=result.token,
+                expiration=result.expiration,
+            )
         raise TypeError(f"exchange_async returned improper result: {type(result)}")
 
     @staticmethod


Test patch:
diff --git a/libraries/botbuilder-core/tests/test_bot_framework_adapter.py b/libraries/botbuilder-core/tests/test_bot_framework_adapter.py
--- a/libraries/botbuilder-core/tests/test_bot_framework_adapter.py
+++ b/libraries/botbuilder-core/tests/test_bot_framework_adapter.py
@@ -12,6 +12,7 @@
     BotFrameworkAdapterSettings,
     TurnContext,
 )
+from botbuilder.core.invoke_response import InvokeResponse
 from botbuilder.schema import (
     Activity,
     ActivityTypes,
@@ -22,6 +23,13 @@
     DeliveryModes,
     ExpectedReplies,
     CallerIdConstants,
+    SignInConstants,
+    TokenExchangeInvokeRequest,
+    TokenExchangeInvokeResponse,
+)
+from botframework.connector.token_api.models import (
+    TokenExchangeRequest,
+    TokenResponse as ConnectorTokenResponse,
 )
 from botframework.connector.aio import ConnectorClient
 from botframework.connector.auth import (
@@ -189,6 +197,31 @@ async def mock_create_conversation(parameters):
 
         return self.connector_client_mock
 
+    async def _create_token_api_client(
+        self, context: TurnContext, oauth_app_credentials: AppCredentials = None
+    ):
+        client = await super()._create_token_api_client(context, oauth_app_credentials)
+
+        def mock_exchange_async(
+            user_id,  # pylint: disable=unused-argument
+            connection_name,
+            channel_id,
+            uri=None,  # pylint: disable=unused-argument
+            token=None,
+            custom_headers=None,  # pylint: disable=unused-argument
+            raw=False,  # pylint: disable=unused-argument
+            **operation_config,  # pylint: disable=unused-argument
+        ):
+            return ConnectorTokenResponse(
+                channel_id=channel_id,
+                connection_name=connection_name,
+                token=token,
+                expiration=None,
+            )
+
+        client.user_token.exchange_async = mock_exchange_async
+        return client
+
 
 async def process_activity(
     channel_id: str, channel_data_tenant_id: str, conversation_tenant_id: str
@@ -731,3 +764,124 @@ async def callback(context: TurnContext):
             adapter.connector_client_mock.conversations.send_to_conversation.call_count
             == 3
         )
+
+    async def test_process_activity_with_identity_token_exchange_invoke_response(self):
+        mock_credential_provider = unittest.mock.create_autospec(CredentialProvider)
+
+        settings = BotFrameworkAdapterSettings(
+            app_id="bot_id", credential_provider=mock_credential_provider,
+        )
+        adapter = AdapterUnderTest(settings)
+
+        identity = ClaimsIdentity(
+            claims={
+                AuthenticationConstants.AUDIENCE_CLAIM: "bot_id",
+                AuthenticationConstants.APP_ID_CLAIM: "bot_id",
+                AuthenticationConstants.VERSION_CLAIM: "1.0",
+            },
+            is_authenticated=True,
+        )
+
+        inbound_activity = Activity(
+            type=ActivityTypes.invoke,
+            name=SignInConstants.token_exchange_operation_name,
+            service_url="http://tempuri.org/whatever",
+            delivery_mode=DeliveryModes.normal,
+            conversation=ConversationAccount(id="conversationId"),
+            value=TokenExchangeInvokeRequest(
+                id="token_exchange_id",
+                token="token",
+                connection_name="connection_name",
+            ),
+        )
+
+        async def callback(context: TurnContext):
+            activity = Activity(
+                type=ActivityTypes.invoke_response,
+                value=InvokeResponse(
+                    status=200,
+                    body=TokenExchangeInvokeResponse(
+                        id=context.activity.value.id,
+                        connection_name=context.activity.value.connection_name,
+                    ),
+                ),
+            )
+
+            await context.send_activity(activity)
+
+        invoke_response = await adapter.process_activity_with_identity(
+            inbound_activity, identity, callback,
+        )
+
+        assert invoke_response
+        assert invoke_response.status == 200
+        assert invoke_response.body["id"] == inbound_activity.value.id
+        assert (
+            invoke_response.body["connectionName"]
+            == inbound_activity.value.connection_name
+        )
+
+    async def test_exchange_token_from_credentials(self):
+        mock_credential_provider = unittest.mock.create_autospec(CredentialProvider)
+
+        settings = BotFrameworkAdapterSettings(
+            app_id="bot_id", credential_provider=mock_credential_provider,
+        )
+        adapter = AdapterUnderTest(settings)
+
+        identity = ClaimsIdentity(
+            claims={
+                AuthenticationConstants.AUDIENCE_CLAIM: "bot_id",
+                AuthenticationConstants.APP_ID_CLAIM: "bot_id",
+                AuthenticationConstants.VERSION_CLAIM: "1.0",
+            },
+            is_authenticated=True,
+        )
+
+        inbound_activity = Activity(
+            type=ActivityTypes.invoke,
+            name=SignInConstants.token_exchange_operation_name,
+            service_url="http://tempuri.org/whatever",
+            conversation=ConversationAccount(id="conversationId"),
+            value=TokenExchangeInvokeRequest(
+                id="token_exchange_id",
+                token="token",
+                connection_name="connection_name",
+            ),
+        )
+
+        async def callback(context):
+            result = await adapter.exchange_token_from_credentials(
+                turn_context=context,
+                oauth_app_credentials=None,
+                connection_name=context.activity.value.connection_name,
+                exchange_request=TokenExchangeRequest(
+                    token=context.activity.value.token, uri=context.activity.service_url
+                ),
+                user_id="user_id",
+            )
+
+            activity = Activity(
+                type=ActivityTypes.invoke_response,
+                value=InvokeResponse(
+                    status=200,
+                    body=TokenExchangeInvokeResponse(
+                        id=context.activity.value.id,
+                        connection_name=result.connection_name,
+                    ),
+                ),
+            )
+
+            await context.send_activity(activity)
+
+        invoke_response = await adapter.process_activity_with_identity(
+            inbound_activity, identity, callback,
+        )
+
+        assert invoke_response
+        assert invoke_response.status == 200
+        assert invoke_response.body["id"] == inbound_activity.value.id
+        assert (
+            invoke_response.body["connectionName"]
+            == inbound_activity.value.connection_name
+        )