This blogpost is also covered in an episode of SelectedTech about authenticated webhooks in Teams. Which I regularly do with my partners in crime Appie, Thomy and Stephan.
One of the extensibility scenarios of Microsoft Teams are Incoming webhooks. You can find more information about this in the Microsoft docs. In a nutshell: when you create an incoming webhook you get a ‘random’ URL from Microsoft, and to this URL you can do an HTTP POST call with a special formatted JSON.
The fact is that it’s not that random at all. When we create 2 webhooks for the same Team and channel we get these URL’s to post to:
https://outlook.office.com/webhook/de03efec-5890-4f48-b4f8-895a8dcda222@341e98ec-f170-4e8d-b9aa-0467c8ebd243/IncomingWebhook/819b07104713444d95c8882e982252a6/4bc32719-91a2-456e-96fb-0c2e198af535
https://outlook.office.com/webhook/de03efec-5890-4f48-b4f8-895a8dcda222@341e98ec-f170-4e8d-b9aa-0467c8ebd243/IncomingWebhook/46720956c5524742b37aaaba794cfb07/4bc32719-91a2-456e-96fb-0c2e198af535
As we immediately notice, there are a lot of similarities in these ‘random’ GUIDs. Let’s break them down:
de03efec-5890-4f48-b4f8-895a8dcda222 : This is the ID of our Team
341e98ec-f170-4e8d-b9aa-0467c8ebd243 : This is the ID of our Tenant
46720956c5524742b37aaaba794cfb07 : This one is always different. So this must be the unique ID for the webhook. If somebody knows then let me know 😉
4bc32719-91a2-456e-96fb-0c2e198af535 : This is the ID of the user who created the webhook.
Jep, I"m a huge fan of incoming webhooks in Microsoft Teams. They can really enable a lot of use cases. Want your servers to notify you if something is going on? Want to get notified by Flow if you have a task or if something happens in your Tenant? The list is endless.
But… In some enterprises, this is not acceptable. It took me a while to acknowledge but this model has some serious security flaws. I mean… Your Tenant ID, Channel ID, and User ID. That is something, that if you know what you are doing, you can easily find out. The remaining ID is something that you will need to brute force, which is what hackers do all the time. And to be honest, brute-forcing a password can take a lot of time and also there are measures against it. But this is just an HTTP POST. So if you scale out your machines in Azure you could already do a lot of requests a minute and it would only be a matter of time before you don’t get an error message back. This type of security is called Security through obscurity, meaning that the security lies in hiding the information. Which is considered bad practice.
Now, what can a person with malicious intents do wrong then if they guess your incoming webhook? Well, for instance, they could create an almost identical Adaptive Card to a task approval from Flow in Teams. (Which I know nothing off so thanks to Jon & Daniel for this easy YouTube video). So I created a simple flow to test this out.
When running this I get a nice poll message in Teams.
When I click the submit button this data is sent back to Flow (I should actually say Power Automate). Now let’s imagine that I’m a hacker and I’ve just figured out the URL to your custom webhook. Then I could use the same JSON from an adaptive card but instead of just submitting the information somewhere I could even open up a URL to a fake Microsoft Login page and get your login credentials that way (by the way if you have not activated MFA then stop reading and do that NOW).
To tackle this problem we could turn off Incoming webhooks in the Teams admin center. This is done in numerous enterprise organizations.
But then you lose such awesome functionality. One solution might be to create your own ‘Incoming webhook’. We could (mis)use the Bot Framework in Teams for this. Let’s start by creating a new bot. This bot will be the base for our incoming webhook. Head over to Azure and create a new ‘web app bot’ and take the V4 echo bot template. Fill in all the required information and wait for deployment. After everything is deployed you can download the source code of this bot to start adjusting it.
First, we will remove the logic that the bot has to enable him to respond. We don’t want people to talk to this bot. In the OnMessageActivityAsync method, we comment out everything
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
//var replyText = $"Echo: {turnContext.Activity.Text}";
//await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken);
}
We now have a bot that doesn’t talk back. But we do need the bot to save information about all the users that are in a channel when it’s added to a channel. This is in fact the trick of a pro-active bot inside of Teams. A bot cannot start a conversation with a user by itself. It needs some information about that user. This is obtained by adding the bot to a channel or by adding the Teams app as a personal app to a user. Save all this much-needed information in a Storage Table when you add the bot to a channel in the OnMembersAddedAsync method
Since this is only a proof of concept I don’t really save the information very nicely in the table storage. It always has rowkey and partitionkey set to 1. But the serviceUrl, userId, botId, and botName are the things I need. Now the magic is in the next piece of code. Because the bot is protected with it’s own Azure AD app registration we need to authenticate the call to Teams if we initiate it. Otherwise, it would be taken care of by the boilerplate code.
Now the bot can initiate conversations, and since a bot is just an API we can add our own API’s to it. And with these API’s we can bring our own security. Do you want to protect it with Azure AD, with Azure API Management or any other way? Then you can do this.
There are some benefits and some drawbacks to this solution. First off you do need to add custom code. It’s not as easy as configuring a webhook, but then again. The people consuming your webhook should not notice anything except that they would add some kind of authentication before they call your endpoint. Also, you would need to add a bot to a channel or as a personal app. This can in some cases be annoying because users can @-mention the bot (nothing will happen but still, difficult to explain to an end-user) If you have some alternative ideas be sure to let me know, I would love to discuss this further.