In the last post we got a little fancy using Adaptive cards in the Bot. We talked about workarounds to allow AC usage in prompts
, how to collect the user input, especially when it comes to Microsoft Teams
.
In this post we will have a high level look at proactive messages, and also get a little creative with them. We will validate the token, with the purpose of making sure the messages are coming from a trusted source.
This is the finished source code: https://github.com/simonagren/simon-blog-bot-v8
Proactive messages could look a bit different. They could, as in the onMembersAdded
give a predefined welcome message. It could be a notification
from another system. And we could mention
people, create new conversations
with people and even continue conversations
.
We will look at continuing a conversation.
In the proactive messages
Bot builder sample, they are saving all the conversations references, and they add a restify
API endpoint /api/notify
. When someone does a GET
request to - http://localhost:3978/api/notify - the Bot sends a predefined message to all the previous conversations.
for (const conversationReference of Object.values(conversationReferences)) {await adapter.continueConversation(conversationReference, async turnContext => {// If you encounter permission-related errors when sending this message, see// https://aka.ms/BotTrustServiceUrlawait turnContext.sendActivity('proactive hello');});}
Sometimes we want to message a specific user, and continue a specific conversation. To continue a specific conversation we need the Conversation Id. In reality, the Id must be saved to somewhere from the Bot. In this example we “just have it”.
We receive the referenceId
in the body of a POST, then we get that reference and continue the conversation. The user then gets a message
, sent via the Bot.
const reference = conversationReferences[req.body.refId];// Proactively notify the user.if (reference) {await adapter.continueConversation(reference, async (context) => {await context.sendActivity(req.body.message);});
Just a brief walkthrough and the rest will be explained in the other sections.
We import crypto
and some additional things from botbuilder
import * as crypto from 'crypto';...import { BotFrameworkAdapter, ConversationReference, ConversationState, MemoryStorage, UserState } from 'botbuilder';
We add a constant of conversationReferences
that will hold the conversation reference to the user. This is injected to the Bot.
const conversationReferences: Array<Partial<ConversationReference>> = [];const myBot = new SimonBot(conversationState, userState, dialog, conversationReferences);
A new restify
API endpoint has been added which allows POST
. We will go over it in details further down.
server.post('/api/notify', async (req, res) => {...In order to parse the body with restify, this line of code has also been added:```typescriptserver.use(restify.plugins.bodyParser());
A private variable for conversationReferences
, added a @param
, and also a check to make sure that we actually receive the references. And just as before we initialize the private variable with what we got via the constructor.
We have added a method for adding conversation references, and it looks like this:
private addConversationReference(activity: Activity) {const conversationReference = TurnContext.getConversationReference(activity);this.conversationReferences[conversationReference.conversation.id] = conversationReference;}
The onMessage
now utilizes that method
this.onMessage(async (context, next) => {this.addConversationReference(context.activity);// If result comes from an Adaptive Cardif (context.activity.text === undefined && context.activity.value ) {context.activity.text = JSON.stringify(context.activity.value);}// Run the Dialog with the new message Activity.await (this.dialog as MainDialog).run(context, this.dialogState);// By calling next() you ensure that the next BotHandler is run.await next();});
The problem is that anyone could post something to that endpoint now, so we need to do something in order to make sure we only receive the messages we want.
Looking at the normal api/messages we could see that the adapter
has a method (processActivity
) that parses and authenticates the incoming request. Inspects the bearer token in the auth header, and compares this with our credentials (appId and secret).
server.post('/api/messages', (req, res) => {adapter.processActivity(req, res, async (context) => {await bot.run(context);});});
This method works similarly as it creates the context, runs through the middleware, and then continues the conversation. There is No token validation included in this method.
We resort to the HMAC concept that is utilized in Microsoft Teams Outgoing Web hooks.
It revolves around creating a shared secret
(and rotate this now and then). I will store the secret in Azure Key Vault
and use it as an environment variable. We generate a HMAC token based on the message we want to send, and the shared secret. Using standard SHA256
cryptography and UTF8
.
Imagine we have an Azure Function in PowerShell. The function is provisioning/administering a SharePoint site, and we receive messages from the Azure Function.
We use the secret
environment variables that has been fetched from Azure Key Vault.
We generate the HMAC token from the message we want to send, containing the refId
and message
.
$message = '{"refId":"adf1b490-3315-11ea-8b32-436f2f76cf33|livechat","message":"Hello!"}'$msgBuf = [Text.Encoding]::UTF8.GetBytes($message)
$secret = $env:hmacsecret$secretBuf = [Text.Encoding]::UTF8.GetBytes($secret)
$hmac = New-Object System.Security.Cryptography.HMACSHA256$hmac.Key = $bufSecret
$msgHash = 'HMAC ' + [Convert]::ToBase64String($hmac.ComputeHash($msgBuf))
$header = @{"Accept"="application/json""Authorization"=$msgHash"Content-Type"="application/json"}Invoke-WebRequest -Uri "http://localhost:3978/api/notify" -Method POST -Headers $header -Body $message
We will generate the HMAC token from the request body of the message using Crypto. See the comments in the code. Since we in PowerShell based the hash on the same message we receive and the shared key, the result of the hash we create here should be the same.
server.post('/api/notify', async (req, res) => {if (req.body.refId && req.body.message) {try {const authHeader = req.headers.authorization || '';// Get message from the request body and convert to a byte array in UTF8const messageStr = JSON.stringify(req.body);const msgBuf = Buffer.from(messageStr, 'utf8');// Use shared secret and convert to byte array i UTF8const sharedSecret = process.env.hmacSecret;const secretBuf = Buffer.from(sharedSecret, 'utf8');// create a SHA256 HMAC and convert to base64 stringconst msgHash = 'HMAC ' + crypto.createHmac('sha256', secretBuf).update(msgBuf).digest('base64');// Compare is the created message hash is the same is the one in the auth headerif (msgHash === authHeader) {const reference = conversationReferences[req.body.refId];// Proactively notify the user.if (reference) {await adapter.continueConversation(reference, async (context) => {await context.sendActivity(req.body.message);});// Everything went okres.send(200);} else {// Couldn't find referenceres.send(404);}} else {res.send(401);}} catch (err) {res.send(404, err);}}});
In this post, we looked at using proactive messaging in a secure way.
I hope I get to finish the last post about implementing LUIS. I also have some ideas on creating a provisioning project including all of these concepts, along with Azure Functions, PnP templates etc. We will see.