├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── algorithm.js ├── errors.js ├── index.js ├── parser.js └── verify.js ├── package.json ├── signature.md └── test ├── algorithm.test.js ├── apiKeyAuth.test.js ├── parser.test.js └── verify.test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb-base', 3 | env: { 4 | mocha: true, 5 | node: true 6 | }, 7 | rules: { 8 | 'comma-dangle': 'off', 9 | 'consistent-return': 'off', 10 | 'max-len': ['error', 120], 11 | 'newline-per-chained-call': 'off', 12 | 'no-param-reassign': 'off', 13 | 'no-restricted-syntax': 'off', 14 | 'no-unused-expressions': 0 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .history/* 2 | node_modules/* 3 | .nyc_output/* 4 | package-lock.json 5 | *.lcov -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | - '8' 5 | sudo: false 6 | deploy: 7 | provider: npm 8 | email: $NPM_EMAIL 9 | api_key: $NPM_TOKEN 10 | on: 11 | branch: master 12 | after_success: npm run coverage 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Axel SHAÏTA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # api-key-auth 2 | 3 | Express/Restify middleware to authenticate HTTP requests based on api key and signature. 4 | 5 | [![npm version](https://badge.fury.io/js/api-key-auth.svg)](https://badge.fury.io/js/api-key-auth) 6 | [![codebeat badge](https://codebeat.co/badges/8b9de4e3-0841-4a91-85fd-5a26f58901c3)](https://codebeat.co/projects/github-com-arkerone-api-key-auth-master) 7 | [![Build Status](https://travis-ci.org/arkerone/api-key-auth.svg?branch=master)](https://travis-ci.org/arkerone/api-key-auth) 8 | [![codecov](https://codecov.io/gh/arkerone/api-key-auth/branch/master/graph/badge.svg)](https://codecov.io/gh/arkerone/api-key-auth) 9 | [![Greenkeeper badge](https://badges.greenkeeper.io/arkerone/api-key-auth.svg)](https://greenkeeper.io/) 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ npm install --save api-key-auth 15 | ``` 16 | 17 | ## Usage 18 | 19 | This middleware authenticates callers using an api key and the signature of the request. If the api key and the signature are valid, `req.credentials` will be set with the calling application information. 20 | 21 | ### Example 22 | 23 | This basic usage example should help you get started : 24 | 25 | ```javascript 26 | const express = require('express'); 27 | const apiKeyAuth = require('api-key-auth'); 28 | 29 | const app = express(); 30 | 31 | // Create the collection of api keys 32 | const apiKeys = new Map(); 33 | apiKeys.set('123456789', { 34 | id: 1, 35 | name: 'app1', 36 | secret: 'secret1' 37 | }); 38 | apiKeys.set('987654321', { 39 | id: 2, 40 | name: 'app2', 41 | secret: 'secret2' 42 | }); 43 | 44 | // Your function to get the secret associated to the key id 45 | function getSecret(keyId, done) { 46 | if (!apiKeys.has(keyId)) { 47 | return done(new Error('Unknown api key')); 48 | } 49 | const clientApp = apiKeys.get(keyId); 50 | done(null, clientApp.secret, { 51 | id: clientApp.id, 52 | name: clientApp.name 53 | }); 54 | } 55 | 56 | app.use(apiKeyAuth({ getSecret })); 57 | 58 | app.get('/protected', (req, res) => { 59 | res.send(`Hello ${req.credentials.name}`); 60 | }); 61 | 62 | app.listen(8080); 63 | ``` 64 | 65 | ## API 66 | 67 | ### apiKeyAuth(options) 68 | 69 | Create an api key based authentication middleware function using the given `options` : 70 | 71 | | Name | Type | Default | Description | 72 | | :---------------: | :-------------: | :-------------: | :---------------------------------------------- | 73 | | `getSecret` | `Function` | `-` | Invoked to retrieve the secret from the `keyId` | 74 | | `requestProperty` | `String` | `'credentials'` | The request property to attach the information | 75 | | `requestLifetime` | `Number | null` | `300` | The lifetime of a request in seconds | 76 | 77 | #### options.getSecret (REQUIRED) 78 | 79 | A function with signature `function(keyId, done)` to be invoked to retrieve the secret from the `keyId`. 80 | 81 | - `keyId` (`String`) - The api key used to retrieve the secret. 82 | - `done` (`Function`) - A function with signature `function(err, secret, credentials)` to be invoked when the secret is retrieved. 83 | 84 | - `err` (`Error`) - The error that occurred. 85 | - `secret` (`String`) - The secret to use to verify the signature. 86 | - `credentials` (`Object`) - `req.credentials` will be set with this object. 87 | 88 | #### options.requestProperty (OPTIONAL) 89 | 90 | By default, you can attach information about the client application on `req.credentials` but can be configured with the `requestProperty` option. 91 | 92 | #### options.requestLifetime (OPTIONAL) 93 | 94 | The lifetime of a request in second, by default is set to 300 seconds, set it to null to disable it. This options is used if HTTP header "date" is used to create the signature. 95 | 96 | ## HTTP signature scheme 97 | 98 | Look ["HTTP signature scheme"](signature.md) to sign a HTTP request. 99 | 100 | ## License 101 | 102 | The MIT License (MIT) 103 | 104 | Copyright (c) 2018 Axel SHAÏTA 105 | 106 | Permission is hereby granted, free of charge, to any person obtaining a copy 107 | of this software and associated documentation files (the "Software"), to deal 108 | in the Software without restriction, including without limitation the rights 109 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 110 | copies of the Software, and to permit persons to whom the Software is 111 | furnished to do so, subject to the following conditions: 112 | 113 | The above copyright notice and this permission notice shall be included in 114 | all copies or substantial portions of the Software. 115 | 116 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 117 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 118 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 119 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 120 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 121 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 122 | THE SOFTWARE. 123 | -------------------------------------------------------------------------------- /lib/algorithm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Axel SHAÏTA 3 | * MIT Licensed 4 | */ 5 | 6 | const crypto = require('crypto'); 7 | const { UnsupportedAlgorithmError } = require('./errors'); 8 | 9 | /** 10 | * @module algorithm 11 | */ 12 | module.exports = { 13 | /** 14 | * @description A list of available algorithms 15 | * @public 16 | * @type Array 17 | */ 18 | availableAlgorithms: ['hmac-sha256', 'hmac-sha1', 'hmac-sha512'], 19 | 20 | /** 21 | * @function 22 | * @public 23 | * @description Create the HMAC algorithm 24 | * @param {Object} name - The algorithm's name 25 | * @param {string|Buffer|TypedArray|DataView} secret - The secret key 26 | * @return {Hmac} The Hmac object 27 | * @throws {UnsupportedAlgorithmError} 28 | */ 29 | create(name, secret) { 30 | let algorithm = null; 31 | switch (name) { 32 | case 'hmac-sha1': 33 | algorithm = crypto.createHmac('sha1', secret); 34 | break; 35 | case 'hmac-sha256': 36 | algorithm = crypto.createHmac('sha256', secret); 37 | break; 38 | case 'hmac-sha512': 39 | algorithm = crypto.createHmac('sha512', secret); 40 | break; 41 | default: 42 | throw new UnsupportedAlgorithmError(); 43 | } 44 | 45 | return algorithm; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class BaseError 3 | * @private 4 | * @extends Error 5 | */ 6 | class BaseError extends Error { 7 | /** 8 | * @param {Number} status The HTTP status code 9 | * @param {number} message The error message 10 | */ 11 | constructor(status, message) { 12 | super(message); 13 | Error.captureStackTrace(this, this.constructor); 14 | this.name = this.constructor.name; 15 | this.status = status; 16 | } 17 | } 18 | 19 | /** 20 | * @module errors 21 | */ 22 | module.exports = { 23 | /** 24 | * @class BadAuthenticationSchemeError 25 | * @public 26 | * @extends BaseError 27 | */ 28 | BadAuthenticationSchemeError: class BadAuthenticationSchemeError extends BaseError { 29 | constructor() { 30 | const message = 'Bad authentication scheme.'; 31 | super(401, message); 32 | } 33 | }, 34 | 35 | /** 36 | * @class BadHeaderFormatError 37 | * @public 38 | * @extends BaseError 39 | */ 40 | BadHeaderFormatError: class BadHeaderFormatError extends BaseError { 41 | /** 42 | * @param {string} header The header's name 43 | * @param {string} expectedFormat the expected format 44 | */ 45 | constructor(header, expectedFormat) { 46 | const message = `Bad value format for the HTTP header ${header}. Expected format : ${expectedFormat}.`; 47 | super(400, message); 48 | } 49 | }, 50 | 51 | /** 52 | * @class BadSignatureError 53 | * @public 54 | * @extends BaseError 55 | */ 56 | BadSignatureError: class BadSignatureError extends BaseError { 57 | constructor() { 58 | const message = 'Bad signature.'; 59 | super(401, message); 60 | } 61 | }, 62 | 63 | /** 64 | * @class ExpiredRequestError 65 | * @public 66 | * @extends BaseError 67 | */ 68 | ExpiredRequestError: class ExpiredRequestError extends BaseError { 69 | constructor() { 70 | const message = 'Request is expired'; 71 | super(401, message); 72 | } 73 | }, 74 | /** 75 | * @class MissingRequiredHeadersError 76 | * @public 77 | * @extends BaseError 78 | */ 79 | MissingRequiredHeadersError: class MissingRequiredHeadersError extends BaseError { 80 | /** 81 | * @param {Array} headers The header's names 82 | */ 83 | constructor(...headers) { 84 | const message = `Missing required HTTP headers : ${headers.join(', ')}.`; 85 | super(400, message); 86 | } 87 | }, 88 | 89 | /** 90 | * @class MissingRequiredSignatureParamsError 91 | * @public 92 | * @extends BaseError 93 | */ 94 | MissingRequiredSignatureParamsError: class MissingRequiredSignatureParamsError extends BaseError { 95 | /** 96 | * @param {Array} params The signature parameter's names 97 | */ 98 | constructor(...params) { 99 | const message = `Missing required signature parameters : ${params.join(', ')}.`; 100 | super(400, message); 101 | } 102 | }, 103 | 104 | /** 105 | * @class UnauthorizedError 106 | * @public 107 | * @extends BaseError 108 | */ 109 | UnauthorizedError: class UnauthorizedError extends BaseError { 110 | /** 111 | * @param {number} message The error message 112 | */ 113 | constructor(message) { 114 | super(401, message); 115 | } 116 | }, 117 | /** 118 | * @class UnsupportedAlgorithmError 119 | * @public 120 | * @extends BaseError 121 | */ 122 | UnsupportedAlgorithmError: class UnsupportedAlgorithmError extends BaseError { 123 | constructor(...algorithms) { 124 | let message = 'Unsupported algorithm.'; 125 | if (algorithms.length > 0) { 126 | message += ` You can use these algorithms : ${algorithms.join(', ')}.`; 127 | } 128 | super(401, message); 129 | } 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Axel SHAÏTA 3 | * MIT Licensed 4 | */ 5 | 6 | const { availableAlgorithms } = require('./algorithm'); 7 | const parser = require('./parser'); 8 | const verify = require('./verify'); 9 | const { BadSignatureError, UnauthorizedError } = require('./errors'); 10 | 11 | /** 12 | * @function 13 | * @public 14 | * @description Create the middleware for api key based authentication 15 | * @param {Object} options An object with options. 16 | * @param {Function} options.getSecret The function to get the secret 17 | * @param {String} [options.requestProperty='credentials'] The request property's name used to attach credentials 18 | * @param {Number|null} [options.requestLifetime=300] The lifetime of a request in second (set to null to disable it) 19 | * @return {Function} The middleware function 20 | * @throws {Error} The method "getSecret" must be defined 21 | */ 22 | function apiKeyAuth(options) { 23 | if (!options || !options.getSecret) { 24 | throw new Error('The method "getSecret" must be defined'); 25 | } 26 | const { getSecret, requestLifetime = 300, requestProperty = 'credentials' } = options; 27 | 28 | const middleware = function middleware(req, res, next) { 29 | /* Don't check the signature for preflight request */ 30 | if (req.method === 'OPTIONS' && req.headers['access-control-request-headers']) { 31 | const hasAuthInAccessControl = 32 | req.headers['access-control-request-headers'] 33 | .split(',') 34 | .map(header => header.trim().toLowerCase()) 35 | .indexOf('authorization') !== -1; 36 | if (hasAuthInAccessControl) { 37 | return next(); 38 | } 39 | } 40 | let signatureParams = null; 41 | try { 42 | signatureParams = parser.parseRequest(req, { 43 | algorithms: availableAlgorithms, 44 | requestLifetime 45 | }); 46 | } catch (err) { 47 | return next(err); 48 | } 49 | getSecret(signatureParams.keyid, (err, secret, credentials) => { 50 | if (err) { 51 | return next(new UnauthorizedError(err.message)); 52 | } 53 | if (!secret) { 54 | throw new Error('The method "getSecret" must return the secret key through the callback function'); 55 | } 56 | if (!verify.verifySignature(signatureParams, secret)) { 57 | return next(new BadSignatureError()); 58 | } 59 | 60 | req[requestProperty] = credentials; 61 | return next(); 62 | }); 63 | }; 64 | 65 | return middleware; 66 | } 67 | 68 | /** 69 | * @module apiKeyAuth 70 | * @description The middleware for api key based authentication 71 | */ 72 | module.exports = apiKeyAuth; 73 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Axel SHAÏTA 3 | * MIT Licensed 4 | */ 5 | 6 | const { 7 | BadAuthenticationSchemeError, 8 | BadHeaderFormatError, 9 | ExpiredRequestError, 10 | MissingRequiredHeadersError, 11 | MissingRequiredSignatureParamsError, 12 | UnsupportedAlgorithmError 13 | } = require('./errors'); 14 | 15 | module.exports = { 16 | /** 17 | * @function 18 | * @public 19 | * @description Parse the request and extract the signature parameters 20 | * @param {Object} req The request 21 | * @param {Object} options An object with options. 22 | * @param {Array} options.algorithms A list of available algorithms 23 | * @param {Number|null} [options.requestLifetime=300] The lifetime of a request in second (set to null to disable it) 24 | * @return {Object} Signature parameters 25 | * @throws {MissingRequiredHeadersError} 26 | * @throws {MissingRequiredSignatureParamsError} 27 | * @throws {BadAuthenticationSchemeError} 28 | */ 29 | parseRequest(req, options) { 30 | if (!req.headers || !req.headers.authorization) { 31 | throw new MissingRequiredHeadersError('authorization'); 32 | } 33 | 34 | const { algorithms, requestLifetime = 300 } = options; 35 | 36 | /* Check the authorization scheme */ 37 | let { authorization } = req.headers; 38 | const scheme = 'signature'; 39 | const prefix = authorization.substring(0, scheme.length).toLowerCase(); 40 | if (prefix !== scheme) { 41 | throw new BadAuthenticationSchemeError(); 42 | } 43 | 44 | /* Get the signature parameters */ 45 | authorization = authorization.substring(scheme.length).trim(); 46 | const parts = authorization.split(','); 47 | const signatureParams = {}; 48 | for (const part of parts) { 49 | const index = part.indexOf('="'); 50 | const key = part.substring(0, index).toLowerCase(); 51 | const value = part.substring(index + 2, part.length - 1); 52 | signatureParams[key] = value; 53 | } 54 | 55 | /* Check if the signature param exists */ 56 | const requiredParams = ['keyid', 'algorithm', 'signature']; 57 | const missingSignatureParams = []; 58 | for (const param of requiredParams) { 59 | if (!signatureParams[param.toLowerCase()]) { 60 | missingSignatureParams.push(param); 61 | } 62 | } 63 | if (missingSignatureParams.length > 0) { 64 | throw new MissingRequiredSignatureParamsError(...missingSignatureParams); 65 | } 66 | 67 | /* If "headers" param not exists use the date HTTP header by default */ 68 | signatureParams.headers = signatureParams.headers ? signatureParams.headers.toLowerCase().split(' ') : ['date']; 69 | 70 | /* Check algoritm */ 71 | if (algorithms.indexOf(signatureParams.algorithm) === -1) { 72 | throw new UnsupportedAlgorithmError(...algorithms); 73 | } 74 | 75 | /* Check if the request if expired */ 76 | if (signatureParams.headers.indexOf('date') !== -1 && req.headers.date && requestLifetime) { 77 | /* Check if the request is not expired */ 78 | const currentDate = new Date().getTime(); 79 | const requestDate = Date.parse(req.headers.date); 80 | if (Number.isNaN(requestDate)) { 81 | throw new BadHeaderFormatError('date', ', :: GMT'); 82 | } 83 | 84 | if (Math.abs(currentDate - requestDate) >= requestLifetime * 1000) { 85 | throw new ExpiredRequestError(); 86 | } 87 | } 88 | 89 | /* Create the signature string */ 90 | const missingRequiredHeaders = []; 91 | signatureParams.signingString = ''; 92 | signatureParams.headers.forEach((header, index, arr) => { 93 | if (header === '(request-target)') { 94 | signatureParams.signingString += `(request-target): ${req.method.toLowerCase()} ${req.path}`; 95 | } else if (req.headers[header]) { 96 | signatureParams.signingString += `${header}: ${req.headers[header]}`; 97 | } else { 98 | missingRequiredHeaders.push(header); 99 | } 100 | if (index < arr.length - 1) { 101 | signatureParams.signingString += '\n'; 102 | } 103 | }); 104 | 105 | if (missingRequiredHeaders.length > 0) { 106 | throw new MissingRequiredHeadersError(...missingRequiredHeaders); 107 | } 108 | 109 | return signatureParams; 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /lib/verify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Axel SHAÏTA 3 | * MIT Licensed 4 | */ 5 | 6 | const algorithmFactory = require('./algorithm'); 7 | 8 | /** 9 | * @module verify 10 | */ 11 | module.exports = { 12 | /** 13 | * @function 14 | * @public 15 | * @description Verify the signature 16 | * @param {Object} signatureParams The signature's parameters 17 | * @param {string|Buffer|TypedArray|DataView} secret The secret key 18 | * @return {Boolean} True if the signature is ok, false otherwise 19 | */ 20 | verifySignature(signatureParams, secret) { 21 | const hmac = algorithmFactory.create(signatureParams.algorithm, secret); 22 | hmac.update(signatureParams.signingString); 23 | 24 | /* Use double hmac to protect against timing attacks */ 25 | let h1 = algorithmFactory.create(signatureParams.algorithm, secret); 26 | h1 = h1.update(hmac.digest()).digest(); 27 | let h2 = algorithmFactory.create(signatureParams.algorithm, secret); 28 | h2 = h2.update(Buffer.from(signatureParams.signature, 'base64')).digest(); 29 | 30 | return h1.equals(h2); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-key-auth", 3 | "version": "0.2.2", 4 | "description": "Express/Restify middleware to authenticate HTTP requests based on api key and signature", 5 | "author": { 6 | "name": "Axel SHAÏTA", 7 | "email": "shaita.axel@gmail.com", 8 | "url": "https://www.codeheroes.fr" 9 | }, 10 | "license": "MIT", 11 | "keywords": [ 12 | "authentication", 13 | "auth", 14 | "http", 15 | "api", 16 | "rest", 17 | "apiKey", 18 | "key", 19 | "signature", 20 | "express", 21 | "expressjs", 22 | "restify", 23 | "middleware", 24 | "plugin", 25 | "nodejs" 26 | ], 27 | "main": "./lib", 28 | "scripts": { 29 | "test": "nyc mocha", 30 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/arkerone/api-key-auth" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/arkerone/api-key-auth/issues" 38 | }, 39 | "homepage": "https://github.com/arkerone/api-key-auth#readme", 40 | "devDependencies": { 41 | "chai": "^4.2.0", 42 | "codecov": "^3.6.1", 43 | "eslint": "^6.8.0", 44 | "eslint-config-airbnb": "^18.0.1", 45 | "eslint-config-airbnb-base": "^14.0.0", 46 | "eslint-plugin-import": "^2.19.1", 47 | "eslint-plugin-jsx-a11y": "^6.2.3", 48 | "eslint-plugin-react": "^7.17.0", 49 | "mocha": "^7.0.1", 50 | "nyc": "^15.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /signature.md: -------------------------------------------------------------------------------- 1 | # HTTP signature scheme 2 | 3 | The signature is based on this draft ["Signing HTTP Messages"](https://tools.ietf.org/html/draft-cavage-http-signatures-09). 4 | Your application must provide to the client application both unique identifier : 5 | 6 | * **key** : A key used to identify the client application; 7 | * **shared secret**: A secret key shared between your application and the client application used to sign the requests and authenticate the client application. 8 | 9 | ## HTTP header 10 | 11 | The signature must be sent in the HTTP header "Authorization" with the authentication scheme "Signature" : 12 | 13 | ``` 14 | Authorization: Signature keyId="API_KEY",algorithm="hmac-sha256",headers="(request-target) host date digest content-length",signature="Base64(HMAC-SHA256(signing string))" 15 | ``` 16 | 17 | Let's see the different components of the signature : 18 | 19 | * **keyId (REQUIRED)** : The client application's key; 20 | * **algorithm (REQUIRED)** : The algorithm used to create the signature; 21 | * **header (OPTIONAL)** : The list of HTTP headers used to create the signature of the request. If specified, it should be a lowercased, quoted list of HTTP header fields, separated by a single space character. If not specified, the `Date` header is used by default therefore the client must send this `Date` header. Note : The list order is important, and must be specified in the order the HTTP header field-value pairs are concatenated together during signing. 22 | * **signature (REQUIRED)** : A base 64 encoded digital signature. The client uses the `algorithm` and `headers` signature parameters to form a canonicalized `signing string`. 23 | 24 | ## Signature String Construction [](signature-string-construction) 25 | 26 | To generate the string that is signed with the shared secret and the `algorithm`, the client must use the values of each HTTP header field in the `headers` Signature parameter in the order they appear. 27 | 28 | To include the HTTP request target in the signature calculation, use the special `(request-target)` header field name. 29 | 30 | 1. If the header field name is `(request-target)` then generate the header field value by concatenating the lowercased HTTP method, an ASCII space, and the path pseudo-headers (example : get /protected); 31 | 2. Create the header field string by concatenating the lowercased header field name followed with an ASCII colon `:`, an ASCII space `` and the header field value. If there are multiple instances of the same header field, all header field values associated with the header field must be concatenated, separated by a ASCII comma and an ASCII space `,`, and used in the order in which they will appear in the HTTP request; 32 | 3. If value is not the last value then append an ASCII newline `\n`. 33 | 34 | To illustrate the rules specified above, assume a `headers` parameter list with the value of `(request-target) host date cache-control x-test` with the following HTTP request headers: 35 | 36 | ``` 37 | GET /protected HTTP/1.1 38 | Host: example.org 39 | Date: Tue, 10 Apr 2018 10:30:32 GMT 40 | x-test: Hello world 41 | Cache-Control: max-age=60 42 | Cache-Control: must-revalidate 43 | ``` 44 | 45 | For the HTTP request headers above, the corresponding signature string is: 46 | 47 | ``` 48 | (request-target): get /protected 49 | host: example.org 50 | date: Tue, 10 Apr 2018 10:30:32 GMT 51 | cache-control: max-age=60, must-revalidate 52 | x-test: Hello world 53 | ``` 54 | 55 | ## Signature creation 56 | 57 | In order to create a signature, a client must : 58 | 59 | 1. Create the signature string as described in [Signature String Construction](#signature-string-construction); 60 | 61 | 2. The `algorithm` and shared secret associated with `keyId` must then be used 62 | to generate a digital signature on the signature string; 63 | 64 | 3. The `signature` is then generated by base 64 encoding the output 65 | of the digital signature algorithm. 66 | 67 | ## Supported algorithms 68 | 69 | Currently supported algorithm names are: 70 | 71 | * hmac-sha1 72 | * hmac-sha256 73 | * hmac-sha512 74 | -------------------------------------------------------------------------------- /test/algorithm.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const crypto = require('crypto'); 3 | const algorithm = require('../lib/algorithm'); 4 | const errors = require('../lib/errors'); 5 | 6 | describe('Algorithm', () => { 7 | describe('Failure tests', () => { 8 | it('should throw if the algorithms type is unsupported', () => { 9 | chai.expect(() => algorithm.create()).to.throw(errors.UnsupportedAlgorithmError); 10 | }); 11 | }); 12 | 13 | describe('work tests', () => { 14 | before(() => { 15 | this.checkHmac = function checkHmac(type, data, secret) { 16 | const hmac = algorithm.create(`hmac-${type}`, secret); 17 | chai.expect(hmac).to.be.an.instanceof(crypto.Hmac); 18 | const h1 = crypto 19 | .createHmac(type, secret) 20 | .update(data) 21 | .digest('hex'); 22 | const h2 = hmac.update(data).digest('hex'); 23 | chai.expect(h1).to.equal(h2); 24 | }; 25 | }); 26 | it('should return a HMAC-SHA1 algorithm', () => { 27 | this.checkHmac('sha1', 'test', 'secret'); 28 | }); 29 | 30 | it('should return a HMAC-SHA256 algorithm', () => { 31 | this.checkHmac('sha256', 'test', 'secret'); 32 | }); 33 | 34 | it('should return a HMAC-SHA512 algorithm', () => { 35 | this.checkHmac('sha512', 'test', 'secret'); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/apiKeyAuth.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | 3 | const chai = require('chai'); 4 | const errors = require('../lib/errors'); 5 | const apiKeyAuth = require('../lib'); 6 | 7 | describe('ApiKeyAuth', () => { 8 | describe('Failure tests', () => { 9 | it('should throw if the "getSecret" method is missing', () => { 10 | chai.expect(() => apiKeyAuth()).to.throw(); 11 | }); 12 | 13 | it('should throw if the "getSecret" method does not return the secret key', () => { 14 | const req = { 15 | headers: { 16 | authorization: 'Signature keyid="123456789",algorithm="hmac-sha1",signature="Slpm4XpaxXaYPx75x5mnDUxmIEA="', 17 | date: 'Tue, 10 Apr 2018 10:30:32 GMT' 18 | } 19 | }; 20 | const middleware = apiKeyAuth({ 21 | getSecret: (keyId, done) => { 22 | done(null, null, null); 23 | }, 24 | requestLifetime: null 25 | }); 26 | chai 27 | .expect(() => middleware(req)) 28 | .to.throw('The method "getSecret" must return the secret key through the callback function'); 29 | }); 30 | }); 31 | 32 | describe('work tests', () => { 33 | it('should skip on CORS preflight if authorization header is present ', () => { 34 | const res = {}; 35 | const req = { 36 | method: 'OPTIONS', 37 | headers: { 38 | 'access-control-request-headers': 'test1, test2, authorization' 39 | } 40 | }; 41 | const middleware = apiKeyAuth({ 42 | getSecret: () => {} 43 | }); 44 | middleware(req, res, (err) => { 45 | chai.expect(!err).to.be.true; 46 | }); 47 | }); 48 | 49 | it('should failed the CORS preflight if authorization header is missing', () => { 50 | const res = {}; 51 | const req = { 52 | method: 'OPTIONS', 53 | headers: { 54 | 'access-control-request-headers': 'test1, test2' 55 | } 56 | }; 57 | const middleware = apiKeyAuth({ 58 | getSecret: () => {} 59 | }); 60 | middleware(req, res, (err) => { 61 | chai.expect(err).to.be.an.instanceof(errors.MissingRequiredHeadersError); 62 | }); 63 | }); 64 | 65 | it('should return a parsing error', () => { 66 | const res = {}; 67 | const req = {}; 68 | const middleware = apiKeyAuth({ 69 | getSecret: () => {} 70 | }); 71 | middleware(req, res, (err) => { 72 | chai.expect(!!err).to.be.true; 73 | }); 74 | }); 75 | 76 | it('should return a UnauthorizedError', () => { 77 | const res = {}; 78 | const req = { 79 | headers: { 80 | authorization: 'Signature keyid="123456789",algorithm="hmac-sha1",signature="Slpm4XpaxXaYPx75x5mnDUxmIEA="', 81 | date: 'Wed, 21 Oct 2015 07:28:00 GMT' 82 | } 83 | }; 84 | const middleware = apiKeyAuth({ 85 | getSecret: (keyId, done) => { 86 | done(new Error('Unauthorized'), null, null); 87 | }, 88 | requestLifetime: null 89 | }); 90 | middleware(req, res, (err) => { 91 | chai.expect(err).to.be.an.instanceof(errors.UnauthorizedError); 92 | chai.expect(err.message).to.equal('Unauthorized'); 93 | }); 94 | }); 95 | 96 | it('should return a BadSignatureError', () => { 97 | const res = {}; 98 | const req = { 99 | headers: { 100 | authorization: 101 | 'Signature keyid="123456789",algorithm="hmac-sha1",headers="(request-target) host date",signature="d3Jvbmdfc2lnbmF0dXJl"', 102 | date: 'Tue, 10 Apr 2018 10:30:32 GMT', 103 | host: 'http://localhost' 104 | }, 105 | method: 'GET', 106 | path: '/protected' 107 | }; 108 | const middleware = apiKeyAuth({ 109 | getSecret: (keyId, done) => { 110 | done(null, 'secret', null); 111 | }, 112 | requestLifetime: null 113 | }); 114 | middleware(req, res, (err) => { 115 | chai.expect(err).to.be.an.instanceof(errors.BadSignatureError); 116 | }); 117 | }); 118 | 119 | it('should authorize the request', () => { 120 | const res = {}; 121 | const req = { 122 | headers: { 123 | authorization: 'Signature keyid="123456789",algorithm="hmac-sha1",signature="Slpm4XpaxXaYPx75x5mnDUxmIEA="', 124 | date: 'Tue, 10 Apr 2018 10:30:32 GMT', 125 | host: 'http://localhost' 126 | }, 127 | method: 'GET', 128 | path: '/protected' 129 | }; 130 | const middleware = apiKeyAuth({ 131 | getSecret: (keyId, done) => { 132 | done(null, 'secret', { 133 | name: 'App1' 134 | }); 135 | }, 136 | requestLifetime: null 137 | }); 138 | middleware(req, res, (err) => { 139 | chai.expect(!err).to.be.true; 140 | chai.expect(req.credentials.name).to.equal('App1'); 141 | }); 142 | }); 143 | 144 | it('should change the request property name', () => { 145 | const res = {}; 146 | const req = { 147 | headers: { 148 | authorization: 'Signature keyid="123456789",algorithm="hmac-sha1",signature="Slpm4XpaxXaYPx75x5mnDUxmIEA="', 149 | date: 'Tue, 10 Apr 2018 10:30:32 GMT', 150 | host: 'http://localhost' 151 | }, 152 | method: 'GET', 153 | path: '/protected' 154 | }; 155 | const middleware = apiKeyAuth({ 156 | getSecret: (keyId, done) => { 157 | done(null, 'secret', { 158 | name: 'App1' 159 | }); 160 | }, 161 | requestLifetime: null, 162 | requestProperty: 'client' 163 | }); 164 | middleware(req, res, (err) => { 165 | chai.expect(!err).to.be.true; 166 | chai.expect(req.client.name).to.equal('App1'); 167 | }); 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /test/parser.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | const chai = require('chai'); 3 | const crypto = require('crypto'); 4 | const { availableAlgorithms } = require('../lib/algorithm'); 5 | const errors = require('../lib/errors'); 6 | const parser = require('../lib/parser'); 7 | 8 | describe('Parser', () => { 9 | describe('Failure tests', () => { 10 | it('should throw if HTTP header "Authorization" is not present', () => { 11 | const req = {}; 12 | chai.expect(() => parser.parseRequest(req, {})).to.throw(errors.MissingRequiredHeadersError); 13 | }); 14 | 15 | it('should throw if the auth scheme of the HTTP header "Authorization" is not valid', () => { 16 | const req = { 17 | headers: { 18 | authorization: 'bad_scheme params' 19 | } 20 | }; 21 | chai.expect(() => parser.parseRequest(req, {})).to.throw(errors.BadAuthenticationSchemeError); 22 | }); 23 | 24 | it('should throw if the signature parameters are not valid', () => { 25 | const req = { 26 | headers: { 27 | authorization: 'Signature bad_params' 28 | } 29 | }; 30 | chai.expect(() => parser.parseRequest(req, {})).to.throw(errors.MissingRequiredSignatureParamsError); 31 | }); 32 | 33 | it('should throw if the algorithm is not supported', () => { 34 | const req = { 35 | headers: { 36 | authorization: 37 | 'Signature keyid="123456789",algorithm="unknown_algorithm",headers="host date",signature="Slpm4XpaxXaYPx75x5mnDUxmIEA="' 38 | } 39 | }; 40 | chai 41 | .expect(() => 42 | parser.parseRequest(req, { 43 | algorithms: availableAlgorithms 44 | })) 45 | .to.throw(errors.UnsupportedAlgorithmError); 46 | }); 47 | 48 | it('should throw if the HTTP header date is malformed', () => { 49 | const req = { 50 | headers: { 51 | authorization: 'Signature keyid="123456789",algorithm="hmac-sha1",signature="Slpm4XpaxXaYPx75x5mnDUxmIEA="', 52 | date: 'malformed_date' 53 | } 54 | }; 55 | chai 56 | .expect(() => 57 | parser.parseRequest(req, { 58 | algorithms: availableAlgorithms 59 | })) 60 | .to.throw(errors.BadHeaderFormatError); 61 | }); 62 | 63 | it('should throw if the request is expired', () => { 64 | const req = { 65 | headers: { 66 | authorization: 'Signature keyid="123456789",algorithm="hmac-sha1",signature="Slpm4XpaxXaYPx75x5mnDUxmIEA="', 67 | date: 'Tue, 10 Apr 2018 10:30:32 GMT' 68 | } 69 | }; 70 | chai 71 | .expect(() => 72 | parser.parseRequest(req, { 73 | algorithms: availableAlgorithms 74 | })) 75 | .to.throw(errors.ExpiredRequestError); 76 | }); 77 | 78 | it('should throw if required headers for signature are missing', () => { 79 | const req = { 80 | headers: { 81 | authorization: 82 | 'Signature keyid="123456789",algorithm="hmac-sha1",headers="host date",signature="Slpm4XpaxXaYPx75x5mnDUxmIEA="' 83 | } 84 | }; 85 | chai 86 | .expect(() => 87 | parser.parseRequest(req, { 88 | algorithms: availableAlgorithms 89 | })) 90 | .to.throw(errors.MissingRequiredHeadersError); 91 | }); 92 | }); 93 | 94 | describe('work tests', () => { 95 | it('should return the parsing request', () => { 96 | const req = { 97 | headers: { 98 | authorization: 99 | 'Signature keyId="123456789",algorithm="hmac-sha1",headers="(request-target) host date",signature="ay+nsBHNuPjNcCSPYDkJRD3Lm1g="', 100 | date: 'Tue, 10 Apr 2018 10:30:32 GMT', 101 | host: 'http://localhost' 102 | }, 103 | method: 'GET', 104 | path: '/protected' 105 | }; 106 | const expectedResult = { 107 | keyid: '123456789', 108 | algorithm: 'hmac-sha1', 109 | headers: ['(request-target)', 'host', 'date'], 110 | signature: 'ay+nsBHNuPjNcCSPYDkJRD3Lm1g=', 111 | signingString: '(request-target): get /protected\nhost: http://localhost\ndate: Tue, 10 Apr 2018 10:30:32 GMT' 112 | }; 113 | const parsedRequest = parser.parseRequest(req, { 114 | algorithms: availableAlgorithms, 115 | requestLifetime: null 116 | }); 117 | chai.expect(parsedRequest).to.deep.equal(expectedResult); 118 | }); 119 | 120 | it('should return the parsing request (with request expiration checking)', () => { 121 | const currentDate = new Date().toString(); 122 | const signingString = `(request-target): get /protected\nhost: http://localhost\ndate: ${currentDate}`; 123 | const signature = crypto 124 | .createHmac('sha1', 'secret') 125 | .update(signingString) 126 | .digest('base64'); 127 | const req = { 128 | headers: { 129 | authorization: `Signature keyId="123456789",algorithm="hmac-sha1",headers="(request-target) host date",signature="${signature}"`, 130 | date: currentDate, 131 | host: 'http://localhost' 132 | }, 133 | method: 'GET', 134 | path: '/protected' 135 | }; 136 | const expectedResult = { 137 | keyid: '123456789', 138 | algorithm: 'hmac-sha1', 139 | headers: ['(request-target)', 'host', 'date'], 140 | signature, 141 | signingString 142 | }; 143 | const parsedRequest = parser.parseRequest(req, { 144 | algorithms: availableAlgorithms, 145 | requestLifetime: 300 146 | }); 147 | chai.expect(parsedRequest).to.deep.equal(expectedResult); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/verify.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const verify = require('../lib/verify'); 3 | 4 | describe('Verify', () => { 5 | describe('Verify the signature', () => { 6 | it('should return true if the signature is valid', () => { 7 | const signatureParams = { 8 | keyid: '123456789', 9 | algorithm: 'hmac-sha1', 10 | signature: 'Slpm4XpaxXaYPx75x5mnDUxmIEA=', 11 | headers: ['date'], 12 | signingString: 'date: Tue, 10 Apr 2018 10:30:32 GMT' 13 | }; 14 | chai.expect(verify.verifySignature(signatureParams, 'secret')).to.be.true; 15 | }); 16 | 17 | it('should return false if the signature is not valid', () => { 18 | const signatureParams = { 19 | keyid: '123456789', 20 | algorithm: 'hmac-sha1', 21 | signature: 'd3Jvbmdfc2lnbmF0dXJl', 22 | headers: ['date'], 23 | signingString: 'date: Tue, 10 Apr 2018 10:30:32 GMT' 24 | }; 25 | chai.expect(verify.verifySignature(signatureParams, 'secret')).to.be.false; 26 | }); 27 | }); 28 | }); 29 | --------------------------------------------------------------------------------