alexa-ability-express-handler
Advanced tools
Comparing version 0.3.1 to 0.4.0
@@ -7,23 +7,95 @@ 'use strict'; | ||
exports.verifyRequest = verifyRequest; | ||
exports.validateUrl = validateUrl; | ||
exports.getCertificate = getCertificate; | ||
exports.validateCertificate = validateCertificate; | ||
exports.validateBody = validateBody; | ||
var _assert = require('assert'); | ||
var _assert2 = _interopRequireDefault(_assert); | ||
var _bluebird = require('bluebird'); | ||
var _bluebird2 = _interopRequireDefault(_bluebird); | ||
var _request = require('request'); | ||
var _request2 = _interopRequireDefault(_request); | ||
var _crypto = require('crypto'); | ||
var _crypto2 = _interopRequireDefault(_crypto); | ||
var _debug = require('debug'); | ||
var _debug2 = _interopRequireDefault(_debug); | ||
var _url = require('url'); | ||
var _path = require('path'); | ||
var _x2 = require('x509'); | ||
var _startsWith = require('lodash/startsWith'); | ||
var _startsWith2 = _interopRequireDefault(_startsWith); | ||
var _includes = require('lodash/includes'); | ||
var _includes2 = _interopRequireDefault(_includes); | ||
var _get = require('lodash/get'); | ||
var _get2 = _interopRequireDefault(_get); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
var log = (0, _debug2.default)('express-handler:verifyRequest'); | ||
var MAX_TOLERANCE = 60 * 2.5 * 1000; // 2.5 minutes | ||
var CERT_HEADER = 'SignatureCertChainUrl'; | ||
var SIG_HEADER = 'Signature'; | ||
var DEFAULT_TIME = new Date(0).toISOString(); | ||
var CERT_PROTO = 'https:'; | ||
var CERT_HOST = 's3.amazonaws.com'; | ||
var CERT_PORT = 443; | ||
var CERT_PATH = '/echo.api/'; | ||
var CERT_ALTNAME = 'echo-api.amazon.com'; | ||
function verifyRequest() { | ||
var _ref = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; | ||
var _ref$tolerance = _ref.tolerance; | ||
var tolerance = _ref$tolerance === undefined ? MAX_TOLERANCE : _ref$tolerance; | ||
return function verifyRequestMiddleware(req, res, next) { | ||
// get data | ||
var chainUrl = req.headers[CERT_HEADER]; | ||
var signature = req.headers[SIG_HEADER]; | ||
var sig = req.headers[SIG_HEADER]; | ||
var body = req.body; | ||
var timestamp = (0, _get2.default)(body, 'request.timestamp', DEFAULT_TIME); | ||
log('cert-url: %s', chainUrl); | ||
log('signature: %s', sig); | ||
log('timestamp: %s', timestamp); | ||
log('checking body: %o', body); | ||
if (!isValidSignatureUrl(chainUrl)) { | ||
return next(new Error()); | ||
// basic checks | ||
if (!chainUrl) return next(new Error('No SignatureCertChainUrl header provided.')); | ||
if (!sig) return next(new Error('No Signature header provided.')); | ||
if (!body) return next(new Error('No body provided.')); | ||
// check timestamp | ||
var diff = Math.abs(new Date(timestamp).valueOf() - new Date().valueOf()); | ||
if (diff > tolerance) { | ||
return next(new Error('Request timestamp is outside of allowed tolerance.')); | ||
} | ||
getCertificate(chainUrl).tap(validateCertificate).then(function (cert) { | ||
return validateBody(cert, signature, req.body); | ||
// validation | ||
_bluebird2.default.resolve(chainUrl).then(validateUrl).then(getCertificate).then(validateCertificate).then(function (cert) { | ||
return validateBody(cert, sig, body); | ||
}).then(function () { | ||
return next(); | ||
log('verified request'); | ||
next(); | ||
}, function (err) { | ||
return next(err); | ||
log('error verifiying request: %s', err); | ||
next(err); | ||
}); | ||
@@ -33,3 +105,16 @@ }; | ||
function isValidSignatureUrl(url) { | ||
/** | ||
* Validates that the certificate url is valid, as defined by | ||
* https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/developing-an-alexa-skill-as-a-web-service#verifying-the-signature-certificate-url | ||
* | ||
* Basically the rules are: | ||
* 1. The protocol is equal to https (case insensitive). | ||
* 2. The hostname is equal to s3.amazonaws.com (case insensitive). | ||
* 3. The normalized path starts with /echo.api/ (case sensitive). | ||
* 4. If a port is defined in the URL, the port is equal to 443. | ||
* | ||
* @param {String} url | ||
* @return {String} url | ||
*/ | ||
function validateUrl(url) { | ||
var _parse = (0, _url.parse)(url); | ||
@@ -42,14 +127,87 @@ | ||
if (protocol !== 'http') return false; | ||
if (hostname !== 's3.amazonaws.com') return false; | ||
if (!pathname.indexOf('/echo.api/')) return false; | ||
if (port !== undefined && port !== '443') return false; | ||
var path = (0, _path.normalize)(pathname); | ||
return true; | ||
_assert2.default.equal(protocol, CERT_PROTO); | ||
_assert2.default.equal(hostname, CERT_HOST); | ||
(0, _assert2.default)((0, _startsWith2.default)(path, CERT_PATH), path + ' does not start with ' + CERT_PATH); | ||
_assert2.default.equal(port || CERT_PORT, CERT_PORT); | ||
log('valid url: %s', url); | ||
// return the url to make promise chaining easier | ||
return url; | ||
} | ||
function getCertificate() {} | ||
/** | ||
* Get the x509 certificate from amazon | ||
* TODO cache some number of certs | ||
* | ||
* @param {String} url | ||
* @return {String} certificate | ||
*/ | ||
function getCertificate(url) { | ||
log('getting certificate'); | ||
function validateCertificate() {} | ||
return new _bluebird2.default(function (res, rej) { | ||
(0, _request2.default)(url, function (err, resp, body) { | ||
if (err) { | ||
log('error getting certificate'); | ||
return rej(err); | ||
} | ||
function validateBody() {} | ||
if (resp.statusCode !== 200) { | ||
log('invalid status code: %s', resp.statusCode); | ||
return rej(new Error('Invalid certificate response.')); | ||
} | ||
log('got certificate'); | ||
res(body); | ||
}); | ||
}); | ||
} | ||
/** | ||
* Validate a certificate, as defined by: | ||
* https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/developing-an-alexa-skill-as-a-web-service#Checking the Signature of the Request | ||
* @param {String} cert | ||
* @return {String} cert | ||
*/ | ||
function validateCertificate(cert) { | ||
log('validating certificate'); | ||
var _parseCert = (0, _x2.parseCert)(cert); | ||
var altNames = _parseCert.altNames; | ||
var notBefore = _parseCert.notBefore; | ||
var notAfter = _parseCert.notAfter; | ||
var now = new Date(); | ||
(0, _assert2.default)((0, _includes2.default)(altNames, CERT_ALTNAME), 'Invalid alt names.'); | ||
(0, _assert2.default)(now > new Date(notBefore), 'Certificate expired.'); | ||
(0, _assert2.default)(now < new Date(notAfter), 'Certificate expired.'); | ||
log('valid certificate'); | ||
// return cert to make promise chaining easier | ||
return cert; | ||
} | ||
/** | ||
* Check the body against the signature and validated cert | ||
* TODO do we need to check against raw body? | ||
* @param {String} cert | ||
* @param {String} sig | ||
* @param {Object} body | ||
*/ | ||
function validateBody(cert, sig, body) { | ||
log('checking body against signature'); | ||
var verifier = _crypto2.default.createVerify('SHA1'); | ||
verifier.update(JSON.stringify(body)); | ||
if (!verifier.verify(cert, sig, 'base64')) { | ||
throw new Error('Could not verify request body.'); | ||
} | ||
log('signature matches body'); | ||
} |
{ | ||
"name": "alexa-ability-express-handler", | ||
"version": "0.3.1", | ||
"description": "Publish an alexa-ability as an express route", | ||
"version": "0.4.0", | ||
"description": "Expose an alexa-ability as an express route", | ||
"main": "lib/index.js", | ||
@@ -35,6 +35,9 @@ "scripts": { | ||
"dependencies": { | ||
"lodash": "^4.0.1" | ||
"bluebird": "^3.3.1", | ||
"debug": "^2.2.0", | ||
"lodash": "^4.3.0", | ||
"request": "^2.69.0", | ||
"x509": "^0.2.3" | ||
}, | ||
"devDependencies": { | ||
"babel": "^6.3.26", | ||
"babel-cli": "^6.4.5", | ||
@@ -52,6 +55,3 @@ "babel-eslint": "^4.1.8", | ||
"sinon-chai": "^2.8.0" | ||
}, | ||
"peerDependencies": { | ||
"alexa-ability": "^0.6.0" | ||
} | ||
} |
@@ -9,6 +9,6 @@ # alexa-ability-express-handler [![Build Status](https://travis-ci.org/nickclaw/alexa-ability-express-handler.svg?branch=master)](https://travis-ci.org/nickclaw/alexa-ability-express-handler) | ||
import { Ability, events } from 'alexa-ability'; | ||
import { handleAbility } from 'alexa-ability-express-handler'; | ||
import { verifyRequest, handleAbility } from 'alexa-ability-express-handler'; | ||
// build skill | ||
const ability = new Ability(); | ||
ability.on(events.launch, function(req) { | ||
@@ -20,3 +20,4 @@ req.say("Testing testing one two three.").end(); | ||
app.use(bodyParser.json()); | ||
app.use('/my-intent', handleAbility(ability)); | ||
app.use(verifyRequest()); | ||
app.post('/my-intent', handleAbility(ability)); | ||
@@ -27,1 +28,13 @@ app.listen(8000, function() { | ||
``` | ||
### API | ||
##### `verifyRequest(options) -> express middleware` | ||
Creates a middleware function for express that implements the body verification process | ||
required by Amazon for certification. Takes an optional options object with the | ||
following values: | ||
- `tolerance`: only accept requests with a timestamp within the given tolerance. | ||
Value defaults to 2.5 minutes in milliseconds. | ||
##### `handleAbility(ability) -> express handler` | ||
Creates an express handler for the given ability. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
22273
12
16
342
38
5
1
1
+ Addedbluebird@^3.3.1
+ Addeddebug@^2.2.0
+ Addedrequest@^2.69.0
+ Addedx509@^0.2.3
+ Addedajv@6.12.6(transitive)
+ Addedasn1@0.2.6(transitive)
+ Addedassert-plus@1.0.0(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedaws-sign2@0.7.0(transitive)
+ Addedaws4@1.13.2(transitive)
+ Addedbcrypt-pbkdf@1.0.2(transitive)
+ Addedbluebird@3.7.2(transitive)
+ Addedcaseless@0.12.0(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedcore-util-is@1.0.2(transitive)
+ Addeddashdash@1.14.1(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addedecc-jsbn@0.1.2(transitive)
+ Addedextend@3.0.2(transitive)
+ Addedextsprintf@1.3.0(transitive)
+ Addedfast-deep-equal@3.1.3(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addedforever-agent@0.6.1(transitive)
+ Addedform-data@2.3.3(transitive)
+ Addedgetpass@0.1.7(transitive)
+ Addedhar-schema@2.0.0(transitive)
+ Addedhar-validator@5.1.5(transitive)
+ Addedhttp-signature@1.2.0(transitive)
+ Addedis-typedarray@1.0.0(transitive)
+ Addedisstream@0.1.2(transitive)
+ Addedjsbn@0.1.1(transitive)
+ Addedjson-schema@0.4.0(transitive)
+ Addedjson-schema-traverse@0.4.1(transitive)
+ Addedjson-stringify-safe@5.0.1(transitive)
+ Addedjsprim@1.4.2(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addednan@2.2.0(transitive)
+ Addedoauth-sign@0.9.0(transitive)
+ Addedperformance-now@2.1.0(transitive)
+ Addedpsl@1.13.0(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedqs@6.5.3(transitive)
+ Addedrequest@2.88.2(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsshpk@1.18.0(transitive)
+ Addedtough-cookie@2.5.0(transitive)
+ Addedtunnel-agent@0.6.0(transitive)
+ Addedtweetnacl@0.14.5(transitive)
+ Addeduri-js@4.4.1(transitive)
+ Addeduuid@3.4.0(transitive)
+ Addedverror@1.10.0(transitive)
+ Addedx509@0.2.6(transitive)
- Removedalexa-ability@0.6.1(transitive)
- Removedalexa-ssml@0.3.2(transitive)
- Removedtv4@1.3.0(transitive)
- Removedxmlbuilder@4.2.1(transitive)
Updatedlodash@^4.3.0