3
votes

I am attempting to build a simple MS Teams bot using the Python bot framework SDK. When testing my bot locally using the emulator, everything works fine. I registered the bot using the legacy portal here https://dev.botframework.com/bots as I do not want to create an Azure subscription.

I added the app id and app secret to the bot and deployed it on an EC2 machine using API Gateway (with HTTP proxy integration) to get an HTTPS url for the messaging endpoint.

When deployed, the code is able to receive and parse messages from both the testing functionality on the dev framework page and from an actual deployed app on Teams. However, when attempting to respond to the message, I get an Unauthorized error message.

Here is the stack trace:

[on_turn_error] unhandled error: Operation returned an invalid status code 'Unauthorized'
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/botbuilder/core/bot_adapter.py", line 103, in run_pipeline
    context, callback
  File "/usr/local/lib/python3.7/site-packages/botbuilder/core/middleware_set.py", line 69, in receive_activity_with_status
    return await self.receive_activity_internal(context, callback)
  File "/usr/local/lib/python3.7/site-packages/botbuilder/core/middleware_set.py", line 79, in receive_activity_internal
    return await callback(context)
  File "/usr/local/lib/python3.7/site-packages/botbuilder/core/activity_handler.py", line 28, in on_turn
    await self.on_message_activity(turn_context)
  File "/home/ec2-user/bot/bot.py", line 24, in on_message_activity
    return await turn_context.send_activity(response)
  File "/usr/local/lib/python3.7/site-packages/botbuilder/core/turn_context.py", line 165, in send_activity
    result = await self.send_activities([activity_or_text])
  File "/usr/local/lib/python3.7/site-packages/botbuilder/core/turn_context.py", line 198, in send_activities
    return await self._emit(self._on_send_activities, output, logic())
  File "/usr/local/lib/python3.7/site-packages/botbuilder/core/turn_context.py", line 276, in _emit
    return await logic
  File "/usr/local/lib/python3.7/site-packages/botbuilder/core/turn_context.py", line 193, in logic
    responses = await self.adapter.send_activities(self, output)
  File "/usr/local/lib/python3.7/site-packages/botbuilder/core/bot_framework_adapter.py", line 444, in send_activities
    raise error
  File "/usr/local/lib/python3.7/site-packages/botbuilder/core/bot_framework_adapter.py", line 431, in send_activities
    activity.conversation.id, activity.reply_to_id, activity
  File "/usr/local/lib/python3.7/site-packages/botframework/connector/aio/operations_async/_conversations_operations_async.py", line 533, in reply_to_activity
    raise models.ErrorResponseException(self._deserialize, response)
botbuilder.schema._models_py3.ErrorResponseException: Operation returned an invalid status code 'Unauthorized'

My app code is:

CONFIG = DefaultConfig()
SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD,
                                       CONFIG.APP_AUTH_TENANT, CONFIG.APP_OAUTH_ENDPOINT)
ADAPTER = BotFrameworkAdapter(SETTINGS)


# Catch-all for errors.
async def on_error(context: TurnContext, error: Exception):
    print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
    traceback.print_exc()

    # Send a message to the user
    await context.send_activity("The bot encountered an error or bug.")
    await context.send_activity("To continue to run this bot, please fix the bot source code.")
    # Send a trace activity if we're talking to the Bot Framework Emulator
    if context.activity.channel_id == "emulator":
        # Create a trace activity that contains the error object
        trace_activity = Activity(
            label="TurnError",
            name="on_turn_error Trace",
            timestamp=datetime.utcnow(),
            type=ActivityTypes.trace,
            value=f"{error}",
            value_type="https://www.botframework.com/schemas/error",
        )
        # Send a trace activity, which will be displayed in Bot Framework Emulator
        await context.send_activity(trace_activity)


ADAPTER.on_turn_error = on_error
APP_ID = SETTINGS.app_id
dynamodb = boto3.resource("dynamodb")
CONVERSATION_REFERENCES = dynamodb.Table("ConversationReferences")

# Create the Bot
BOT = MyBot(CONVERSATION_REFERENCES)


# Listen for incoming requests on /api/messages
async def messages(req):
    print(f"Message Received - {str(datetime.now())}")
    json_request = await req.json()
    print(f"Request Body: {json_request}")
    activity = Activity().deserialize(json_request)
    print("Request successfully deserialized")
    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
    try:
        print("Sending activity to adapter")
        response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
        if response:
            return Response(status=response.status, text=response.body)
        return Response(status=201)
    except Exception as exception:
        raise exception

async def health(req):
    return Response(status=200, text="Working")

APP = web.Application(middlewares=[aiohttp_error_middleware])
APP.router.add_post("/api/messages", messages)
APP.router.add_get("/health", health)

if __name__ == "__main__":
    web.run_app(APP)

And my bot code is:

class MyBot(ActivityHandler):

    def __init__(self, conversation_references):
        self.conversation_references = conversation_references

    async def on_message_activity(self, turn_context: TurnContext):
        print("Message received by bot adapter")
        # The next two lines also cause an unauthorized error. I commented them out to try and simplify
        # team_details = await teams.TeamsInfo.get_members(turn_context)
        # user = team_details[1].email
        user = "test@test.com"
        conversation = self.conversation_references.get_item(Key={"user": user})
        if "Item" in conversation:
            response = "You are already registered"
        else:
            conversation = TurnContext.get_conversation_reference(turn_context.activity)
            item = {"user": user, "conversation": conversation.as_dict()}
            self.conversation_references.put_item(Item=item)
            response = "You have been successfully registered!"
        return await turn_context.send_activity(response)

Update: When testing locally, I did not add the app id and password to the emulator. When I do, in debug mode I get the following error message: "An error occurred while POSTing "/INSPECT open" command to conversation xxxxxx|livechat: 400: The bot's Microsoft App ID or Microsoft App Password is incorrect."

I am 100% sure the id and password are correct, though, as I've manually used the token endpoint from the registration page to get an access token with those credentials. It may be related to the fact that in the code and using the endpoint manually I can specify the Directory (tenant) ID, while I cannot do so using the emulator.

Another strange point is that when the emulator returns that response it doesn't actually seem to be making a request to my local endpoint, so I'm not sure where the 400 response is even coming from.

2
I can see that you're configuring the adapter to use an app ID and password, but does your bot configuration actually contain an app ID and password to provide to the adapter? When you test the bot locally, do you supply the app ID and password to the Emulator? Try testing on Teams or Web Chat while debugging locally using ngrok. Also, are we to assume that's really your whole bot or are you actually using those conversation references to send proactive messages? Teams requires you to call TrustServiceUrl.Kyle Delaney
Thanks for the response, when testing locally I did not supply the app ID and password to the Emulator. When I do it tells me my app id or password is wrong, but I have triple checked these values and even requested a new password. I'll add the error message from debug mode to the question. I also used the token endpoint manually in postman and got an access token. I will be sending proactive messages with the conversation references, but for now I am just trying to get the initial call and response to work.ncv
Are your app ID and password in your bot's config.py? The Emulator communicates with your bot directly, so any configurations you make to your bot registration or your AAD app registration will not affect the Emulator. If your bot is unauthenticated (meaning it either doesn't have credentials or isn't programmed to use credentials) then that would be why you can only connect to it in the Emulator without specifying credentials. You can read about that hereKyle Delaney
@KyleDelany I think the emulator does actually connect to the app registration data in addition to the locally hosted bot, as the initial conversation request was being sent to a different endpoint. I took the advice of the info in the link you provided though, which was to switch from single tenant to multi tenant in the app registration and that resolved the authentication issues. Thanks so much for your help, if you want to throw that link into an answer, I'd be happy to accept it.ncv

2 Answers

2
votes

You will need to make sure you use multi-tenant AAD app credentials, as described here.

0
votes

Below is the content which you will get from the teams. It has the key serviceURL which will be used to connect back to your bot which is deployed in your Teams using the other parameters which are available in the below json, which I have replaced with logical names between << and >>.

    { text: 'help',
  textFormat: 'plain',
  type: 'message',
  timestamp: 2020-03-05T12:29:26.830Z,
  localTimestamp: 2020-03-05T12:29:26.830Z,
  id: '1583411366810',
  channelId: 'msteams',
  serviceUrl: 'https://smba.trafficmanager.net/emea/',
  from:
   { id:
      '<<Use ID>>',
     name: '<<Display Name>>',
     aadObjectId: '<<objectID>>' },
  conversation:
   { conversationType: 'personal',
     tenantId: '<<Microsoft Tenant ID>>',
     id:
      '<<Unique Conversation ID>>' },
  recipient:
   { id: '<<BotID>>', name: 'Sybot' },
  entities:
   [ { locale: 'en-US',
       country: 'US',
       platform: 'Windows',
       type: 'clientInfo' } ],
  channelData: { tenant: { id: '<<Microsoft Tenant ID>>' } },
  locale: 'en-US' }

Once you receive this you have to use the below code to make the service url a trusted url so that when you send the message back to the user you will not get the unauthorized error.

const { MicrosoftAppCredentials } = require('botbuilder/node_modules/botframework-connector'); 
const { BotFrameworkAdapter } = require('botbuilder');
const { TurnContext } = require('botbuilder');

    const adapter = new BotFrameworkAdapter({
        appId: <<Your App ID>>,
        appPassword: <<Your App Password>>,
    });
    turnContext = new TurnContext(adapter, contextActivity);

    if (!MicrosoftAppCredentials.isTrustedServiceUrl(serviceUrl)) {
        MicrosoftAppCredentials.trustServiceUrl(serviceUrl);
    }
   await context.sendActivity(`Hello World`);