Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds authentication of incoming Slack requests #167

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/CoreBot.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ function Botkit(configuration) {
try {
rendered = mustache.render(text, vars);
} catch (err) {
botkit.log('Error in message template. Mustache failed with error: ',err);
botkit.log('Error in message template. Mustache failed with error: ', err);
rendered = text;
};

Expand Down
25 changes: 23 additions & 2 deletions lib/SlackBot.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,28 @@ function Slackbot(configuration) {

};

// set up a web route for receiving outgoing webhooks and/or slash commands
slack_botkit.createWebhookEndpoints = function(webserver) {
// adds the webhook authentication middleware module to the webserver
function secureWebhookEndpoints() {
var authenticationMiddleware = require(__dirname + '/middleware/authentication.js');
// convert a variable argument list to an array, drop the webserver argument
var tokens = Array.prototype.slice.call(arguments);
var webserver = tokens.shift();

slack_botkit.log(
'** Requiring token authentication for webhook endpoints for Slash commands ' +
'and outgoing webhooks; configured ' + tokens.length + ' token(s)'
);

webserver.use(authenticationMiddleware(tokens));
}

// set up a web route for receiving outgoing webhooks and/or slash commands;
// accept an optional array of authentication tokens to validate the incoming request
slack_botkit.createWebhookEndpoints = function(webserver, authenticationTokens) {

if (authenticationTokens !== undefined && arguments.length > 1 && arguments[1].length) {
secureWebhookEndpoints.apply(null, arguments);
}

slack_botkit.log(
'** Serving webhook endpoints for Slash commands and outgoing ' +
Expand Down Expand Up @@ -168,6 +188,7 @@ function Slackbot(configuration) {

slack_botkit.config.port = port;


slack_botkit.webserver = express();
slack_botkit.webserver.use(bodyParser.json());
slack_botkit.webserver.use(bodyParser.urlencoded({ extended: true }));
Expand Down
63 changes: 63 additions & 0 deletions lib/middleware/authentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Authentication module composed of an Express middleware used to validate
* incoming requests from the Slack API for Slash commands and outgoing
* webhooks.
*/

// Comparison constant
var TOKEN_NOT_FOUND = -1;

function init(tokens) {
var authenticationTokens = flatten(tokens);

if (authenticationTokens.length === 0) {
console.warn('No auth tokens provided, webhook endpoints will always reply HTTP 401.')
}

/**
* Express middleware that verifies a Slack token is passed;
* if the expected token value is not passed, end with request test
* with a 401 HTTP status code.
*
* Note: Slack is totally wacky in that the auth token is sent in the body
* of the request instead of a header value.
*
* @param {object} req - Express request object
* @param {object} res - Express response object
* @param {function} next - Express callback
*/
function authenticate(req, res, next) {
if (!req.body || !req.body.token || authenticationTokens.indexOf(req.body.token) === TOKEN_NOT_FOUND) {
res.status(401).send({
'code': 401,
'message': 'Unauthorized'
});

return;
}

slack_botkit.log(
'** Requiring token authentication for webhook endpoints for Slash commands ' +
'and outgoing webhooks; configured ' + tokens.length + ' tokens'
);
next();
}

return authenticate;
}
/**
* Function that flattens a series of arguments into an array.
*
* @param {Array || String || null} args - No token, single token, or token array
* @returns {Array} - Every element of the array is an authentication token
*/
function flatten(args) {
var result = [];

// convert a variable argument list to an array
args.forEach(function(arg){
result = result.concat(arg);
});
return result;
}
module.exports = init;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"jscs": "^2.7.0",
"mocha": "^2.4.5",
"should": "^8.0.2",
"supertest": "^1.2.0",
"winston": "^2.1.1"
},
"scripts": {
Expand Down
22 changes: 22 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1013,7 +1013,27 @@ and properly configured within Slack.
controller.setupWebserver(port,function(err,express_webserver) {
controller.createWebhookEndpoints(express_webserver)
});
```

### Securing Outgoing Webhooks and Slash commands

You can optionally protect your application with authentication of the requests
from Slack. Slack will generate a unique request token for each Slash command and
outgoing webhook (see [Slack documentation](https://api.slack.com/slash-commands#validating_the_command)).
You can configure the web server to validate that incoming requests contain a valid api token
by adding an express middleware authentication module.

```javascript
controller.setupWebserver(port,function(err,express_webserver) {
controller.createWebhookEndpoints(express_webserver, ['AUTH_TOKEN', 'ANOTHER_AUTH_TOKEN']);
// you can pass the tokens as an array, or variable argument list
//controller.createWebhookEndpoints(express_webserver, 'AUTH_TOKEN_1', 'AUTH_TOKEN_2');
// or
//controller.createWebhookEndpoints(express_webserver, 'AUTH_TOKEN');
});
```

```
controller.on('slash_command',function(bot,message) {

// reply to slash command
Expand All @@ -1030,6 +1050,8 @@ controller.on('outgoing_webhook',function(bot,message) {
})
```



#### controller.setupWebserver()
| Argument | Description
|--- |---
Expand Down
92 changes: 92 additions & 0 deletions tests/middleware/authentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
var should = require('should');
var express = require('express');
var request = require('supertest');
var middleware = require('../../lib/middleware/authentication');

describe('Slack request token authentication', function () {
context('when authentication tokens are used', function () {
var app = express(),
authenticationTokens = ['SOME_VALID_TOKEN', 'ANOTHER_VALID_TOKEN'];

app.use(require('body-parser').json());
app.use(middleware(authenticationTokens));
app.post('/', function(req, res, next) {
res.status(200).send({ "message": "success" });
next();
});

it('should authorize the request if a valid token is passed in the body', function(done) {
request(app)
.post('/')
.send({"token": "SOME_VALID_TOKEN"})
.expect(200)
.end(done);
});

it('should authorize the request if any valid token is passed in the body', function(done) {
request(app)
.post('/')
.send({"token": "ANOTHER_VALID_TOKEN"})
.expect(200)
.end(done);
});

it('should send a HTTP 401 if the request token is missing', function(done) {
request(app)
.get('/')
.expect(401, done);
});

it('should send a HTTP 401 if the request token is not in the list of authorized tokens', function(done) {
request(app)
.post('/')
.send({"token": "INVALID_TOKEN_VALUE"})
.expect(401)
.expect(function(res) {
res.body.code.should.equal(401);
res.body.message.should.equal('Unauthorized');
})
.end(done);

});

it('should send a HTTP 401 if the request token is of the wrong type', function(done) {
request(app)
.post('/')
.send({"token": {"complex": "INVALID_TOKEN_VALUE"}})
.expect(401)
.expect(function(res) {
res.body.code.should.equal(401);
res.body.message.should.equal('Unauthorized');
})
.end(done);
});
});

context('when authentication tokens are not used', function () {
var app = express(),
authenticationTokens = [];

app.use(require('body-parser').json());
app.use(middleware(authenticationTokens));
app.post('/', function(req, res, next) {
res.status(200).send({ "message": "success" });
next();
});

it('should send a HTTP 401 for any request with tokens', function(done) {
request(app)
.post('/')
.send({"token": "SOME_VALID_TOKEN"})
.expect(401)
.end(done);
});

it('should send a HTTP 401 for any request without any tokens', function(done) {
request(app)
.get('/')
.expect(401)
.end(done);
});
});
});