ChatOps with Nexmo verify
I'm a huge fan of using Slack to empower my coworkers to perform actions they need but wouldn't normally do themselves. Developers don't need to know what package manager we use, or which supervisor they need to talk to for the app to be restarted. All they want to do is type robot, deploy my-service on dev-14
and have it take care of all the steps required for them.
A self-service system for developers is awesome, but wouldn't you want to use the same mechanisms to deploy to staging and production too? You've already automated the process and it'd be a shame not to use it yourself. However, once you start talking about production everything starts become a little more risky. If someone gains access to your Slack account they could start changing things in production and we don't want that!
We need a second layer of security - something to verify that we are who we say we are before running the command. Thankfully, Nexmo's Verify API makes this easy.
Get yourself a Hubot
If you want to work along with this tutorial, you'll need to generate a Hubot and connect it to your Slack team. There's a good getting started guide here, but here's the ultra-quick copy and paste version to create a Hubot and start it running:
bash
npm install -g yo generator-hubotmkdir my-awesome-hubot && cd my-awesome-hubotyo hubot --adapter=slack# Get your Slack token from https://my.slack.com/apps/A0F7YS25R-botsHUBOT_SLACK_TOKEN=xoxb-YOUR-TOKEN-HERE ./bin/hubot --adapter slack
You'll need to give your Hubot a name (I'm calling mine Artoo) and answer a few other questions. Once you've answered the questions, it'll run npm install
and then you're ready to start using your Hubot.
Add our command
Once you have a Hubot, it's time to add a custom command to it. I'll be pretending that mine is updating a package on a server.
Create a file at scripts/install-package.js
with the following contents:
javascript
module.exports = (robot) => {robot.respond(/deploy ([A-Za-z-]+) on ([A-Za-z\.]+)/i, (res) => {const package = res.match[1];const environment = res.match[2];installPackage(res, package, environment);});};function installPackage(res, package, environment) {res.send(`Installing ${package} on ${environment}`);}
This will tell Hubot to watch for the phrase "deploy MY-PACKAGE on ENVIRONMENT" and run the installPackage
function in response.
Once you've created this file, stop Hubot running by pressing ctrl+c
and run HUBOT_SLACK_TOKEN=xoxb-YOUR-TOKEN-HERE ./bin/hubot --adapter slack
again to reload your Hubot. As your install-package.js
file was in scripts
it will automatically be loaded.
You can test your bot at this point by sending it a private message saying "deploy nexmo-secret-service on production"
The command just responds to the user saying that it's installing nexmo-secret-service and doesn't actually install a package at the moment (I'll leave that as an exercise for you!), but it's enough for us to add in Nexmo's verify API before triggering the response.
Adding Nexmo verify
Installing software packages through Slack is fun, but what happens if someone else gets access to your Slack account? Wouldn't it be great if each time someone tried to install a package, you got a text message with a confirmation code to prove that you are who you say you are? This is exactly what Nexmo verify provides!
To get started, you'll need a Nexmo account (fun fact, Nexmo's signup process uses Nexmo verify!). Once you log in, you'll be presented with a key and a secret at the top of the screen. Keep these safe, as you'll need them in the next step.
Nexmo provide a node library for working with their API, so let's go ahead and install that into our project by running npm install nexmo --save
. After that completes, add the following to the top of install-package.js
, replacing the key and secret with the ones that you saved after you created an account:
javascript
var Nexmo = require("nexmo");var nexmo = new Nexmo({apiKey: API_KEY,apiSecret: API_SECRET,});
This will create a nexmo
instance that you can use to verify your account. Next, we need to update our code to only install the package once the verification code has been provided. To do this, we call nexmo.verify.request
, passing in a telephone number to use for the verify step and a brand (which is the identifier for who is sending the message). Replace your deploy handler with the following:
javascript
robot.respond(/deploy ([A-Za-z-]+) on ([A-Za-z\.]+)/i, (res) => {const package = res.match[1];const environment = res.match[2];nexmo.verify.request({number: "<your number>",brand: "Tutorial",},function (err, data) {if (err) {console.log(err);return;}return installPackage(res, package, environment);});});
At this point, our Hubot is configured to hear a deploy command, trigger a verify request via Nexmo and trigger a callback, passing data that contains reference for that verify request. The data returned will look like the following:
json
{ "request_id": "03baa29441cb4292a2e28318db15822a", "status": "0" }
We don't want to install the request package until we've verified their identity, so we'll need to store the package to install, which environment to install it in and the request_id
that Nexmo provided so that we can look it up later.
To do this, create a state
object just after your module.exports
line:
javascript
module.exports = (robot) => {let state = {};
This is where we'll store the user's name and request_id, as well as the package and environment to use. We'll also need to update the verify call to store this information in the state variable. Replace the installPackage
call in the nexmo.verify.request
callback with the following code to store this information:
javascript
let request_id = data.request_id;state[res.message.user.name] = { request_id, package, environment };return res.send("Sending text message to verify your identity. Check your phone");
We only want people to be able to run one command at a time, so let's add a check to our deploy command to see if there is an existing state entry for the current user before triggering a verify request. Change your code to add the check just after your robot.respond
handler definition.
javascript
robot.respond(/deploy ([A-Za-z-]+) on ([A-Za-z\.]+)/i, (res) => {if (state[res.message.user.name]) {return res.send("There is already a pending request. Please verify that one before running any more commands");}
We're almost there now, but let's take a look at what we've achieved so far. Our current code will:
- Listen for users asking to deploy a package to an environment
- Trigger a Nexmo verify request for that user
- Store the relevant information for future reference
- Prevent a user from deploying multiple packages at once
That's a fair amount of work, but we still have a little more work to do before our job here is done. We need to register a new handler that people can use to verify themselves. Nexmo supports either four or six digit codes - ours are four so we want a handler that matches the word "verify" then exactly four digits so let's add that now inside the module.exports
function, just underneath our deploy handler.
javascript
robot.respond(/verify ([0-9]{4})/i, (res) => {const code = res.match[1];const user = state[res.message.user.name];if (!user) {return res.send("There are no pending identify requests for the current user");}});
The code above captures the verify code, tells the user that we're verifying their identity and then checks that there is a pending request for the current user.
Telling the user that we're verifying their identity is a start, but we need to check the pin code provided with Nexmo too. Our nexmo
instance has a method for doing this, which we can use by calling nexmo.verify.check
. This method takes a request_id (which we saved earlier) and a code (which the user has just provided), and either returns an error or success using the standard node callback method.
We're about to cover a lot of actions here, so prepare yourself! In this handler, we need to do the following:
- Update your verify handler so that it uses the
nexmo.verify.check
function - If there is an error, log it and return the error to the user
- If there is no error, check the response from Nexmo. A non-zero status means that something went wrong, so we return the error message to the user.
- If it the return status is zero everything looks good, so continue running our function
- Run the
installPackage
command with the package name and environment that we had stored in our state variable. - Delete the entry in
state
for the current user as that command has been successfully run and any future commands need to be verified again.
That's a lot of things to take care of, so here's the code with comments that performs each step:
javascript
robot.respond(/verify ([0-9]{4})/i, (res) => {const code = res.match[1];let user = state[res.message.user.name];if (!user) {return res.send("There are no pending identify requests for the current user");}nexmo.verify.check({request_id: user.request_id,code: code,},function (err, data) {// Was there an error talking to Nexmo?if (err) {console.log(err);return res.send("Sorry, we couldn't verify that code");}// Did our verify call return an error?// e.g. The code was incorrectif (data.status !== "0") {return res.send(data.error_text);}res.send("Identity verified!");// Fetch the package/environment from `state` and installinstallPackage(res, user.package, user.environment);// Delete their existing request_id as we've// already verified itdelete state[res.message.user.name];});});
If you add that in to your Hubot instance, replacing your existing verify handler, you should be in a position to test out the verify logic. Run Hubot and send it a private message saying "deploy nexmo-secret-service on production". It should prompt you to check your phone. Look out for your verify text message from Nexmo, then message Hubot with "verify [code]". You should see nexmo-secret-service
being installed in production!
That's all there is to it! In just 60 lines we've implemented a Hubot listener that can help you deploy services on demand and secured it using Nexmo's verify API to provide two stage authentication.
If you want to take it a little further and make it actually install packages you'll need to write a full implementation for the installPackages
function. I'd recommend taking a look at sequest for all of your SSH implementation needs.
Thanks for reading this far, and if you have any questions feel free to ask in the comments or message me on Twitter (I'm @mheap on there).
You can find the code for this post on GitHub