UNPKG

alexa-verifier-middleware

Version:

An expressjs middleware that verifies HTTP requests sent to an Alexa skill are sent from Amazon.

341 lines (297 loc) 8.93 kB
import url from 'url' import crypto from 'crypto' import { EventEmitter } from 'events'; import { test } from 'tap' import sinon from 'sinon' import nock from 'nock' import httpMocks from 'node-mocks-http' import forge from 'node-forge' import verifier from '../index.js' const MOCKED_TIMESTAMP = '2017-02-10T07:27:59Z' const VALID_CERT_SAN = 'echo-api.amazon.com' const VALID_CERT_URL = 'https://s3.amazonaws.com/echo.api/echo-api-cert-12.pem' // latest cert url const VALID_CERT_URL_PATH = url.parse(VALID_CERT_URL).path // Generate a certificate and a public/private key pair function generateCertKeyPair() { var prng = forge.random.createInstance(); prng.seedFileSync = function(needed) { return forge.util.fillString('a', needed); }; const keys = forge.pki.rsa.generateKeyPair({ bits: 512, workers: 1, prng: prng, algorithm: 'PRIMEINC', }) const cert = forge.pki.createCertificate() cert.publicKey = keys.publicKey cert.serialNumber = '01' cert.validity.notBefore = '0000-01-01T01:01:01Z' cert.validity.notAfter = '9999-12-31T23:59:59Z' const attrs = [ { name: 'commonName', value: 'example.org' }, { name: 'countryName', value: 'US' }, { shortName: 'ST', value: 'Virginia' }, { name: 'localityName', value: 'Blacksburg' }, { name: 'organizationName', value: 'Test' }, { shortName: 'OU', value: 'Test' } ] cert.setSubject(attrs) cert.setIssuer(attrs) cert.setExtensions([ { name: 'basicConstraints', cA: true }, { name: 'keyUsage', keyCertSign: true, digitalSignature: true, nonRepudiation: true, keyEncipherment: true, dataEncipherment: true }, { name: 'extKeyUsage', serverAuth: true, clientAuth: true, codeSigning: true, emailProtection: true, timeStamping: true }, { name: 'nsCertType', client: true, server: true, email: true, objsign: true, sslCA: true, emailCA: true, objCA: true }, { name: 'subjectAltName', altNames: [{ type: 6, // URI value: VALID_CERT_SAN }, { type: 7, // IP ip: '127.0.0.1' }] }, { name: 'subjectKeyIdentifier' } ]) // self-sign certificate cert.sign(keys.privateKey) return { certificate: forge.pki.certificateToPem(cert), publicKey: forge.pki.publicKeyToPem(keys.publicKey), privateKey: forge.pki.privateKeyToPem(keys.privateKey), } } // invoke the middleware by creating a mock request function invokeMiddleware (data, next, after) { const callbacks = { } data['method'] = data['method'] || 'POST' data['on'] = function (eventName, callback) { callbacks[eventName] = callback } const mockReq = httpMocks.createRequest(data) const mockRes = httpMocks.createResponse() next = next || function () {} // verifier is an express middleware (i.e., function (req, res, next) { ... ) verifier(mockReq, mockRes, next) if (callbacks['data']) { callbacks['data'](data['body']) } if (callbacks['end']) { callbacks['end']() } process.nextTick(after, mockRes) } test('enforce strict headerCheck always', function (t) { let calledNext = false const mockNext = function () { calledNext = true } const mockRes = invokeMiddleware({}, mockNext, function (mockRes) { t.equal(calledNext, false) t.equal(mockRes.statusCode, 400) t.same(mockRes._getJSONData(), { reason: 'missing certificate url', status: 'failure' }) t.end() }) }) test('fail if request body is already parsed', function (t) { let calledNext = false const mockNext = function () { calledNext = true } const mockRes = invokeMiddleware({ headers: {}, _body: true, rawBody: {} }, mockNext, function (mockRes) { t.equal(calledNext, false) t.equal(mockRes.statusCode, 400) t.same(mockRes._getJSONData(), { reason: 'The raw request body has already been parsed.', status: 'failure' }) t.end() }) }) test('fail invalid signaturecertchainurl header', function (t) { let calledNext = false const mockNext = function () { calledNext = true } const mockRes = invokeMiddleware({ headers: { 'signature-256': 'aGVsbG8NCg==', 'signaturecertchainurl': 'https://invalid' }, body: JSON.stringify({ hello: 'world', request: { timestamp: new Date().getTime() } }), }, mockNext, function (mockRes) { t.equal(calledNext, false) t.equal(mockRes.statusCode, 400) t.same(mockRes._getJSONData(), { reason: 'Certificate URI hostname must be s3.amazonaws.com: invalid', status: 'failure' }) t.end() }) }) test('fail invalid JSON body', function (t) { let calledNext = false const mockNext = function () { calledNext = true } const mockRes = invokeMiddleware({ headers: { 'signature-256': 'aGVsbG8NCg==', 'signaturecertchainurl': 'https://invalid' }, body: 'invalid' }, mockNext, function (mockRes) { t.equal(calledNext, false) t.equal(mockRes.statusCode, 400) t.same(mockRes._getJSONData(), { reason: 'request body invalid json', status: 'failure' }) t.end() }) }) test('fail invalid signature', function (t) { // Mount fake timers const timeout = global.setTimeout // see https://github.com/sinonjs/sinon/issues/269 const now = new Date(MOCKED_TIMESTAMP) const clock = sinon.useFakeTimers(now.getTime()) // Generate a certificate but will not validate anything against it const { certificate } = generateCertKeyPair() // Mock fetching of certificate for this session only nock('https://s3.amazonaws.com').get(VALID_CERT_URL_PATH).reply(200, certificate) let calledNext = false const mockNext = function () { calledNext = true } const mockRes = invokeMiddleware({ headers: { 'signature-256': 'aGVsbG8NCg==', 'signaturecertchainurl': VALID_CERT_URL }, body: JSON.stringify({ request: { timestamp: MOCKED_TIMESTAMP } }) }, mockNext, function (mockRes) { // we need a timeout function here since the request might not be fully resolved until some point later // this should be revisited at some point... timeout(function () { t.equal(calledNext, false) t.equal(mockRes.statusCode, 400) t.same(mockRes._getJSONData(), { reason: 'invalid signature', status: 'failure' }) clock.restore() t.end() }, 1000) }) }) test('pass with correct signature', function (t) { // Mount fake timers const timeout = global.setTimeout // see https://github.com/sinonjs/sinon/issues/269 const now = new Date(MOCKED_TIMESTAMP) const clock = sinon.useFakeTimers(now.getTime()) // Generate a certificate and private/public key pair const { certificate, privateKey } = generateCertKeyPair() // Mock fetching of certificate for this session only nock('https://s3.amazonaws.com').get(VALID_CERT_URL_PATH).reply(200, certificate) let calledNext = false const mockNext = function () { calledNext = true } const reqBody = JSON.stringify({ "version": "1.0", "session": { "new": true, "sessionId": "SessionId.7745e45d-3042-45eb-8e86-cab2cf285daf", "application": { "applicationId": "amzn1.ask.skill.75c997b8-610f-4eb4-bf2e-95810e15fba2" }, "attributes": {}, "user": { "userId": "amzn1.ask.account.AF6Z7574YHBQCNNTJK45QROUSCUJEHIYAHZRP35FVU673VDGDKV4PH2M52PX4XWGCSYDM66B6SKEEFJN6RYWN7EME3FKASDIG7DPNGFFFNTN4ZT6B64IIZKSNTXQXEMVBXMA7J3FN3ERT2A4EDYFUYMGM4NSQU4RTAQOZWDD2J7JH6P2ROP2A6QEGLNLZDXNZU2DL7BKGCVLMNA" } }, "request": { "type": "IntentRequest", "requestId": "EdwRequestId.fa7428b7-75d0-44c8-aebb-4c222ed48ebe", "timestamp": MOCKED_TIMESTAMP, "locale": "en-US", "intent": { "name": "HelloWorld" }, "inDialog": false } }) // Create a base64-encoded signature to validate the request against via the public certificate const signer = crypto.createSign('RSA-SHA256') signer.update(reqBody) const signature = signer.sign(privateKey, 'base64') const mockRes = invokeMiddleware({ headers: { 'signature-256': signature, 'signaturecertchainurl': VALID_CERT_URL }, body: reqBody }, mockNext, function (mockRes) { // we need a timeout function here since the request might not be fully resolved until some point later // this should be revisited at some point... timeout(function () { t.equal(calledNext, true) t.equal(mockRes.statusCode, 200) clock.restore() t.end() }, 1000) }) })