
That is the second weblog in our collection about writing software program for ChatOps. Within the first publish of this ChatOps collection, we constructed a Webex bot that acquired and logged messages to its operating console. In this publish, we’ll stroll via how one can safe your Webex bot with authentication and authorization. Securing a Webex bot on this method will permit us to really feel extra assured in our deployment as we transfer on to including extra advanced options.
[Access the complete code for this post on GitHub here.]
Crucial: This publish picks up proper the place the primary weblog on this ChatOps collection left off. You should definitely learn the primary publish of our ChatOps collection to learn to make your native improvement surroundings publicly accessible in order that Webex webhook occasions can attain your API. Make certain your tunnel is up and operating and webhook occasions can move via to your API efficiently earlier than continuing on to the subsequent part. From right here on out, this publish assumes that you just’ve taken these steps and have a profitable end-to-end information move. [You can find the code from the first post on how to build a Webex bot here.]
Have to catch up? Learn the primary a part of Colin Lacy’s ChatOps collection: “ChatOps: Tips on how to Construct Your First Webex Bot”
Tips on how to safe your Webex bot with an authentication examine
Webex employs HMAC-SHA1 encryption primarily based on a secret key that you could present, so as to add safety to your service endpoint. For the needs of this weblog publish, I’ll embody that within the net service code as an Categorical middleware operate, which might be utilized to all routes. This manner, it is going to be checked earlier than some other route handler is named. In your surroundings, you would possibly add this to your API gateway (or no matter is powering your surroundings’s ingress, e.g. Nginx, or an OPA coverage).
Tips on how to add a secret to the Webhook
Use your most well-liked software to generate a random, distinctive, and sophisticated string. Guarantee that it’s lengthy and advanced sufficient to be tough to guess. There are many instruments out there to create a key. Since I’m on a Mac, I used the next command:
$ cat /dev/urandom | base64 | tr -dc '0-9a-zA-Z' | head -c30
The ensuing string was printed into my Shell window. You should definitely maintain onto it. You’ll use it in just a few locations within the subsequent few steps.
Now you need to use that string to replace your Webhook with a PUT request. You can even add it to a brand new Webhook in case you’d prefer to DELETE your previous one:
Webex will now ship an extra header with every notification request beneath the header key x-spark-spark-signature. The header worth might be a one-way encryption of the POST physique, executed with the key worth that you just supplied. On the server facet, we are able to try the identical one-way encryption. If the API shopper sending the POST request (ideally Webex) used the identical encryption secret that we used, then our ensuing string ought to match the x-spark-spark-signature header worth.
Tips on how to add an software configuration
Now that issues are beginning to get extra advanced, let’s construct out an software alongside the strains of what we are able to anticipate to see in the true world. First, we create a easy (however extensible) AppConfig class in config/appConfig.js. We’ll use this to tug in surroundings variables after which reference these values in different components of our code. For now, it’ll simply embody the three variables wanted to energy authentication:
- the key that we added to our Webhook
- the header key the place we’ll study the encrypted worth of the POST physique
- the encryption algorithm used, which on this case is ”sha1”
Right here’s the code for the AppConfig class, which we’ll add as our code will get extra advanced:
// in config/appConfig.js import course of from 'course of'; export class AppConfig { constructor() { this.encryptionSecret = course of.env['WEBEX_ENCRYPTION_SECRET']; this.encryptionAlgorithm = course of.env['WEBEX_ENCRYPTION_ALGO']; this.encryptionHeader = course of.env['WEBEX_ENCRYPTION_HEADER']; } }
Tremendous necessary: You should definitely populate these surroundings variables in your improvement surroundings. Skipping this step can lead to a couple minutes of frustration earlier than remembering to populate these values.
Now we are able to create an Auth service class that may expose a way to run our encrypted string comparability:
// in providers/Auth.js import crypto from "crypto"; export class Auth { constructor(appConfig) { this.encryptionSecret = appConfig.encryptionSecret; this.encryptionAlgorithm = appConfig.encryptionAlgorithm; } isProperlyEncrypted(signedValue, messsageBody) { // create an encryption stream const hmac = crypto.createHmac(this.encryptionAlgorithm, this.encryptionSecret); // write the POST physique into the encryption stream hmac.write(JSON.stringify(messsageBody)); // shut the stream to make its ensuing string readable hmac.finish(); // learn the encrypted worth const hash = hmac.learn().toString('hex'); // examine the freshly encrypted worth to the POST header worth, // and return the outcome return hash === signedValue; } }
Fairly easy, proper? Now we have to leverage this methodology in a router middleware that may examine all incoming requests for authentication. If the authentication examine doesn’t cross, the service will return a 401 and reply instantly. I do that in a brand new file known as routes/auth.js:
// in routes/auth.js import categorical from 'categorical' import {AppConfig} from '../config/AppConfig.js'; import {Auth} from "../providers/Auth.js"; const router = categorical.Router(); const config = new AppConfig(); const auth = new Auth(config); router.all('/*', async (req, res, subsequent) => { // a comfort reference to the POST physique const messageBody = req.physique; // a comfort reference to the encrypted string, with a fallback if the worth isn’t set const signedValue = req.headers[config.encryptionHeader] || ""; // name the authentication examine const isProperlyEncrypted = auth.isProperlyEncrypted(signedValue, messageBody); if(!isProperlyEncrypted) { res.statusCode = 401; res.ship("Entry denied"); } subsequent(); }); export default router;
All that’s left to do is so as to add this router into the Categorical software, simply earlier than the handler that we outlined earlier. Failing the authentication examine will finish the request’s move via the service logic earlier than it ever will get to some other route handlers. If the examine does cross, then the request can proceed on to the subsequent route handler:
// in app.js import categorical from 'categorical'; import logger from 'morgan'; // ***ADD THE AUTH ROUTER IMPORT*** import authRouter from './routes/auth.js'; import indexRouter from './routes/index.js'; // skipping a few of the boilerplate… // ***ADD THE AUTH ROUTER TO THE APP*** app.use(authRouter); app.use('/', indexRouter); // the remainder of the file stays the identical…
Now in case you run your server once more, you possibly can take a look at out your authentication examine. You possibly can attempt with only a easy POST from a neighborhood cURL or Postman request. Right here’s a cURL command that I used to check it in opposition to my native service:
$ curl --location --request POST 'localhost:3000' --header 'x-spark-signature: incorrect-value' --header 'Content material-Sort: software/json' --data-raw '{ "key": "worth" }'
Operating that very same request in Postman produces the next output:
Now, in case you ship a message to your bot via Webex, you must see the Webhook occasion move via your authentication examine and into the route handler that we created within the first publish.
Tips on how to add non-obligatory authorization
At this level, we are able to relaxation assured that any request that comes via got here from Webex. However that doesn’t imply we’re executed with safety! We would need to prohibit which customers in Webex can name our bot by mentioning it in a Webex Room. If that’s the case, we have to add an authorization examine as effectively.
Tips on how to examine in opposition to a listing of licensed customers
Webex sends consumer info with every occasion notification, indicating the Webex consumer ID and the corresponding e mail handle of the one that triggered the occasion (an instance is displayed within the first publish on this collection). Within the case of a message creation occasion, that is the one that wrote the message about which our net service is notified. There are dozens of the way to examine for authorization – AD teams, AWS Cognito integrations, and so on.
For simplicity’s sake, on this demo service, I’m simply utilizing a hard-coded record of authorised e mail addresses that I’ve added to the Auth service constructor, and a easy public methodology to examine the e-mail handle that Webex supplied within the POST physique in opposition to that hard-coded record. Different, extra difficult modes of authz checks are past the scope of this publish.
// in providers/Auth.js export class Auth { constructor(appConfig) { this.encryptionSecret = appConfig.encryptionSecret; this.encryptionAlgorithm = appConfig.encryptionAlgorithm; // ADDING AUTHORIZED USERS this.authorizedUsers = [ "colacy@cisco.com" // hey, that’s me! ]; } // ADDING AUTHZ CHECK METHOD isUserAuthorized(messageBody) { return this.authorizedUsers.indexOf(messageBody.information.personEmail) !== -1 } // the remainder of the category is unchanged
Identical to with the authentication examine, we have to add this to our routes/auth.js handler. We’ll add this between the authentication examine and the subsequent() name that completes the route handler.
// in routes/auth.js // … const isProperlyEncrypted = auth.isProperlyEncrypted(signedValue, messageBody); if(!isProperlyEncrypted) { res.statusCode = 401; res.ship("Entry denied"); return; } // ADD THE AUTHORIZATION CHECK const isAuthorized = auth.isUserAuthorized(messageBody); if(!isAuthorized) { res.statusCode = 403; res.ship("Unauthorized"); return; } subsequent(); // …
If the sender’s e mail handle isn’t in that record, the bot will ship a 403 again to the API shopper with a message that the consumer was unauthorized. However that doesn’t actually let the consumer know what went flawed, does it?
Consumer Suggestions
If the consumer is unauthorized, we must always allow them to know in order that they aren’t beneath the inaccurate assumption that their request was profitable — or worse, questioning why nothing occurred. On this scenario, the one method to supply the consumer with that suggestions is to reply within the Webex Room the place they posted their message to the bot.
Creating messages on Webex is completed with POST requests to the Webex API. [The documentation and the data schema can be found here.] Keep in mind, the bot authenticates with the entry token that was supplied again once we created it within the first publish. We’ll have to cross that in as a brand new surroundings variable into our AppConfig class:
// in config/AppConfig.js export class AppConfig { constructor() { // ADD THE BOT'S TOKEN this.botToken = course of.env['WEBEX_BOT_TOKEN']; this.encryptionSecret = course of.env['WEBEX_ENCRYPTION_SECRET']; this.encryptionAlgorithm = course of.env['WEBEX_ENCRYPTION_ALGO']; this.encryptionHeader = course of.env['WEBEX_ENCRYPTION_HEADER']; } }
Now we are able to begin a brand new service class, WebexNotifications, in a brand new file known as providers/WebexNotifications.js, which is able to notify our customers of what’s taking place within the backend.
// in providers/WebexNotifications.js export class WebexNotifications { constructor(appConfig) { this.botToken = appConfig.botToken; } // new strategies to go right here }
This class is fairly sparse. For the needs of this demo, we’ll hold it that method. We simply want to provide our customers suggestions primarily based on whether or not or not their request was profitable. That may be executed with a single methodology, applied in our two routers; one to point authorization failures and the opposite to point profitable end-to-end messaging.
A observe on the code beneath: To remain future-proof, Iʼm utilizing the NodeJS model 17.7, which has fetch enabled utilizing the execution flag –experimental-fetch. You probably have an older model of NodeJS, you need to use a third-party HTTP request library, like axios, and use that rather than any strains the place you see fetch used.
Weʼll begin by implementing the sendNotification methodology, which is able to take the identical messageBody object that weʼre utilizing for our auth checks:
// in providers/WebexNotifications.js… // contained in the WebexNotifications.js class async sendNotification(messageBody, success=false) { // we'll begin a response by tagging the one that created the message let responseToUser = `<@personEmail:${messageBody.information.personEmail}>`; // decide if the notification is being despatched as a result of a profitable or failed authz examine if (success === false) { responseToUser += ` Uh oh! You are not licensed to make requests.`; } else { responseToUser += ` Thanks to your message!`; } // ship a message creation request on behalf of the bot const res = await fetch("https://webexapis.com/v1/messages", { headers: { "Content material-Sort": "software/json", "Authorization": `Bearer ${this.botToken}` }, methodology: "POST", physique: JSON.stringify({ roomId: messageBody.information.roomId, markdown: responseToUser }) }); return res.json(); }
Now it’s only a matter of calling this methodology from inside our route handlers. In routes/auth.js we’ll name it within the occasion of an authorization failure:
// in routes/auth.js import categorical from 'categorical' import {AppConfig} from '../config/AppConfig.js'; import {Auth} from "../providers/Auth.js"; // ADD THE WEBEXNOTIFICATIONS IMPORT import {WebexNotifications} from '../providers/WebexNotifications.js'; // … const auth = new Auth(config); // ADD CLASS INSTANTIATION const webex = new WebexNotifications(config); // ... if(!isAuthorized) { res.statusCode = 403; res.ship("Unauthorized"); // ADD THE FAILURE NOTIFICATION await webex.sendNotification(messageBody, false); return; } // ...
Equally, we’ll add the success model of this methodology name to routes/index.js. Right here’s the ultimate model of routes/index.js as soon as we’ve added just a few extra strains like we did within the auth route:
// in routes/index.js import categorical from 'categorical' // Add the AppConfig import import {AppConfig} from '../config/AppConfig.js'; // Add the WebexNotifications import import {WebexNotifications} from '../providers/WebexNotifications.js'; const router = categorical.Router(); // instantiate the AppConfig const config = new AppConfig(); // instantiate the WebexNotification class, passing in our app config const webex = new WebexNotifications(config); router.publish('/', async operate(req, res) { console.log(`Obtained a POST`, req.physique); res.statusCode = 201; res.finish(); await webex.sendNotification(req.physique, true); }); export default router;
To check this out, I’ll merely remark out my very own e mail handle from the authorised record after which ship a message to my bot. As you possibly can see from the screenshot beneath, Webex will show the notification from the code above, indicating that I’m not allowed to make the request.
If I un-comment my e mail handle, the request goes via efficiently.
Conclusion
Wow, that lined so much in a brief quantity of code. Now we now have an end-to-end working setup for ChatOps, with built-in safety. We are able to begin fascinated about all of the totally different workflows that we’d prefer to allow for issues like:
- DevOps automation
- Function-based workflows
- Agile practices for distant groups
- And the rest you possibly can consider!
That is solely the second in a collection of weblog posts about ChatOps. Now that we now have secured our Webex bot, we now have an awesome jump-off level for constructing advanced workflows that may automate loads of downstream processes. As we cowl extra subjects, we’ll publish these right here, so examine again usually!
Observe Cisco Studying & Certifications
Twitter, Fb, LinkedIn and Instagram.
Share: