react / wstein / node_modules / jest-cli / node_modules / jsdom / node_modules / request / node_modules / hawk / lib / server.js
81146 views// Load modules12var Boom = require('boom');3var Hoek = require('hoek');4var Cryptiles = require('cryptiles');5var Crypto = require('./crypto');6var Utils = require('./utils');789// Declare internals1011var internals = {};121314// Hawk authentication1516/*17req: node's HTTP request object or an object as follows:1819var request = {20method: 'GET',21url: '/resource/4?a=1&b=2',22host: 'example.com',23port: 8080,24authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="'25};2627credentialsFunc: required function to lookup the set of Hawk credentials based on the provided credentials id.28The credentials include the MAC key, MAC algorithm, and other attributes (such as username)29needed by the application. This function is the equivalent of verifying the username and30password in Basic authentication.3132var credentialsFunc = function (id, callback) {3334// Lookup credentials in database35db.lookup(id, function (err, item) {3637if (err || !item) {38return callback(err);39}4041var credentials = {42// Required43key: item.key,44algorithm: item.algorithm,45// Application specific46user: item.user47};4849return callback(null, credentials);50});51};5253options: {5455hostHeaderName: optional header field name, used to override the default 'Host' header when used56behind a cache of a proxy. Apache2 changes the value of the 'Host' header while preserving57the original (which is what the module must verify) in the 'x-forwarded-host' header field.58Only used when passed a node Http.ServerRequest object.5960nonceFunc: optional nonce validation function. The function signature is function(nonce, ts, callback)61where 'callback' must be called using the signature function(err).6263timestampSkewSec: optional number of seconds of permitted clock skew for incoming timestamps. Defaults to 60 seconds.64Provides a +/- skew which means actual allowed window is double the number of seconds.6566localtimeOffsetMsec: optional local clock time offset express in a number of milliseconds (positive or negative).67Defaults to 0.6869payload: optional payload for validation. The client calculates the hash value and includes it via the 'hash'70header attribute. The server always ensures the value provided has been included in the request71MAC. When this option is provided, it validates the hash value itself. Validation is done by calculating72a hash value over the entire payload (assuming it has already be normalized to the same format and73encoding used by the client to calculate the hash on request). If the payload is not available at the time74of authentication, the authenticatePayload() method can be used by passing it the credentials and75attributes.hash returned in the authenticate callback.7677host: optional host name override. Only used when passed a node request object.78port: optional port override. Only used when passed a node request object.79}8081callback: function (err, credentials, artifacts) { }82*/8384exports.authenticate = function (req, credentialsFunc, options, callback) {8586callback = Hoek.nextTick(callback);8788// Default options8990options.nonceFunc = options.nonceFunc || function (nonce, ts, nonceCallback) { return nonceCallback(); }; // No validation91options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds9293// Application time9495var now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing9697// Convert node Http request object to a request configuration object9899var request = Utils.parseRequest(req, options);100if (request instanceof Error) {101return callback(Boom.badRequest(request.message));102}103104// Parse HTTP Authorization header105106var attributes = Utils.parseAuthorizationHeader(request.authorization);107if (attributes instanceof Error) {108return callback(attributes);109}110111// Construct artifacts container112113var artifacts = {114method: request.method,115host: request.host,116port: request.port,117resource: request.url,118ts: attributes.ts,119nonce: attributes.nonce,120hash: attributes.hash,121ext: attributes.ext,122app: attributes.app,123dlg: attributes.dlg,124mac: attributes.mac,125id: attributes.id126};127128// Verify required header attributes129130if (!attributes.id ||131!attributes.ts ||132!attributes.nonce ||133!attributes.mac) {134135return callback(Boom.badRequest('Missing attributes'), null, artifacts);136}137138// Fetch Hawk credentials139140credentialsFunc(attributes.id, function (err, credentials) {141142if (err) {143return callback(err, credentials || null, artifacts);144}145146if (!credentials) {147return callback(Boom.unauthorized('Unknown credentials', 'Hawk'), null, artifacts);148}149150if (!credentials.key ||151!credentials.algorithm) {152153return callback(Boom.internal('Invalid credentials'), credentials, artifacts);154}155156if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {157return callback(Boom.internal('Unknown algorithm'), credentials, artifacts);158}159160// Calculate MAC161162var mac = Crypto.calculateMac('header', credentials, artifacts);163if (!Cryptiles.fixedTimeComparison(mac, attributes.mac)) {164return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials, artifacts);165}166167// Check payload hash168169if (options.payload ||170options.payload === '') {171172if (!attributes.hash) {173return callback(Boom.unauthorized('Missing required payload hash', 'Hawk'), credentials, artifacts);174}175176var hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.contentType);177if (!Cryptiles.fixedTimeComparison(hash, attributes.hash)) {178return callback(Boom.unauthorized('Bad payload hash', 'Hawk'), credentials, artifacts);179}180}181182// Check nonce183184options.nonceFunc(attributes.nonce, attributes.ts, function (err) {185186if (err) {187return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials, artifacts);188}189190// Check timestamp staleness191192if (Math.abs((attributes.ts * 1000) - now) > (options.timestampSkewSec * 1000)) {193var tsm = Crypto.timestampMessage(credentials, options.localtimeOffsetMsec);194return callback(Boom.unauthorized('Stale timestamp', 'Hawk', tsm), credentials, artifacts);195}196197// Successful authentication198199return callback(null, credentials, artifacts);200});201});202};203204205// Authenticate payload hash - used when payload cannot be provided during authenticate()206207/*208payload: raw request payload209credentials: from authenticate callback210artifacts: from authenticate callback211contentType: req.headers['content-type']212*/213214exports.authenticatePayload = function (payload, credentials, artifacts, contentType) {215216var calculatedHash = Crypto.calculatePayloadHash(payload, credentials.algorithm, contentType);217return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash);218};219220221// Authenticate payload hash - used when payload cannot be provided during authenticate()222223/*224calculatedHash: the payload hash calculated using Crypto.calculatePayloadHash()225artifacts: from authenticate callback226*/227228exports.authenticatePayloadHash = function (calculatedHash, artifacts) {229230return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash);231};232233234// Generate a Server-Authorization header for a given response235236/*237credentials: {}, // Object received from authenticate()238artifacts: {} // Object received from authenticate(); 'mac', 'hash', and 'ext' - ignored239options: {240ext: 'application-specific', // Application specific data sent via the ext attribute241payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided)242contentType: 'application/json', // Payload content-type (ignored if hash provided)243hash: 'U4MKKSmiVxk37JCCrAVIjV=' // Pre-calculated payload hash244}245*/246247exports.header = function (credentials, artifacts, options) {248249// Prepare inputs250251options = options || {};252253if (!artifacts ||254typeof artifacts !== 'object' ||255typeof options !== 'object') {256257return '';258}259260artifacts = Hoek.clone(artifacts);261delete artifacts.mac;262artifacts.hash = options.hash;263artifacts.ext = options.ext;264265// Validate credentials266267if (!credentials ||268!credentials.key ||269!credentials.algorithm) {270271// Invalid credential object272return '';273}274275if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {276return '';277}278279// Calculate payload hash280281if (!artifacts.hash &&282(options.payload || options.payload === '')) {283284artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);285}286287var mac = Crypto.calculateMac('response', credentials, artifacts);288289// Construct header290291var header = 'Hawk mac="' + mac + '"' +292(artifacts.hash ? ', hash="' + artifacts.hash + '"' : '');293294if (artifacts.ext !== null &&295artifacts.ext !== undefined &&296artifacts.ext !== '') { // Other falsey values allowed297298header += ', ext="' + Hoek.escapeHeaderAttribute(artifacts.ext) + '"';299}300301return header;302};303304305/*306* Arguments and options are the same as authenticate() with the exception that the only supported options are:307* 'hostHeaderName', 'localtimeOffsetMsec', 'host', 'port'308*/309310exports.authenticateBewit = function (req, credentialsFunc, options, callback) {311312callback = Hoek.nextTick(callback);313314// Application time315316var now = Utils.now(options.localtimeOffsetMsec);317318// Convert node Http request object to a request configuration object319320var request = Utils.parseRequest(req, options);321if (request instanceof Error) {322return callback(Boom.badRequest(request.message));323}324325// Extract bewit326327// 1 2 3 4328var resource = request.url.match(/^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/);329if (!resource) {330return callback(Boom.unauthorized(null, 'Hawk'));331}332333// Bewit not empty334335if (!resource[3]) {336return callback(Boom.unauthorized('Empty bewit', 'Hawk'));337}338339// Verify method is GET340341if (request.method !== 'GET' &&342request.method !== 'HEAD') {343344return callback(Boom.unauthorized('Invalid method', 'Hawk'));345}346347// No other authentication348349if (request.authorization) {350return callback(Boom.badRequest('Multiple authentications'));351}352353// Parse bewit354355var bewitString = Hoek.base64urlDecode(resource[3]);356if (bewitString instanceof Error) {357return callback(Boom.badRequest('Invalid bewit encoding'));358}359360// Bewit format: id\exp\mac\ext ('\' is used because it is a reserved header attribute character)361362var bewitParts = bewitString.split('\\');363if (bewitParts.length !== 4) {364return callback(Boom.badRequest('Invalid bewit structure'));365}366367var bewit = {368id: bewitParts[0],369exp: parseInt(bewitParts[1], 10),370mac: bewitParts[2],371ext: bewitParts[3] || ''372};373374if (!bewit.id ||375!bewit.exp ||376!bewit.mac) {377378return callback(Boom.badRequest('Missing bewit attributes'));379}380381// Construct URL without bewit382383var url = resource[1];384if (resource[4]) {385url += resource[2] + resource[4];386}387388// Check expiration389390if (bewit.exp * 1000 <= now) {391return callback(Boom.unauthorized('Access expired', 'Hawk'), null, bewit);392}393394// Fetch Hawk credentials395396credentialsFunc(bewit.id, function (err, credentials) {397398if (err) {399return callback(err, credentials || null, bewit.ext);400}401402if (!credentials) {403return callback(Boom.unauthorized('Unknown credentials', 'Hawk'), null, bewit);404}405406if (!credentials.key ||407!credentials.algorithm) {408409return callback(Boom.internal('Invalid credentials'), credentials, bewit);410}411412if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {413return callback(Boom.internal('Unknown algorithm'), credentials, bewit);414}415416// Calculate MAC417418var mac = Crypto.calculateMac('bewit', credentials, {419ts: bewit.exp,420nonce: '',421method: 'GET',422resource: url,423host: request.host,424port: request.port,425ext: bewit.ext426});427428if (!Cryptiles.fixedTimeComparison(mac, bewit.mac)) {429return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials, bewit);430}431432// Successful authentication433434return callback(null, credentials, bewit);435});436};437438439/*440* options are the same as authenticate() with the exception that the only supported options are:441* 'nonceFunc', 'timestampSkewSec', 'localtimeOffsetMsec'442*/443444exports.authenticateMessage = function (host, port, message, authorization, credentialsFunc, options, callback) {445446callback = Hoek.nextTick(callback);447448// Default options449450options.nonceFunc = options.nonceFunc || function (nonce, ts, nonceCallback) { return nonceCallback(); }; // No validation451options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds452453// Application time454455var now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing456457// Validate authorization458459if (!authorization.id ||460!authorization.ts ||461!authorization.nonce ||462!authorization.hash ||463!authorization.mac) {464465return callback(Boom.badRequest('Invalid authorization'))466}467468// Fetch Hawk credentials469470credentialsFunc(authorization.id, function (err, credentials) {471472if (err) {473return callback(err, credentials || null);474}475476if (!credentials) {477return callback(Boom.unauthorized('Unknown credentials', 'Hawk'));478}479480if (!credentials.key ||481!credentials.algorithm) {482483return callback(Boom.internal('Invalid credentials'), credentials);484}485486if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {487return callback(Boom.internal('Unknown algorithm'), credentials);488}489490// Construct artifacts container491492var artifacts = {493ts: authorization.ts,494nonce: authorization.nonce,495host: host,496port: port,497hash: authorization.hash498};499500// Calculate MAC501502var mac = Crypto.calculateMac('message', credentials, artifacts);503if (!Cryptiles.fixedTimeComparison(mac, authorization.mac)) {504return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials);505}506507// Check payload hash508509var hash = Crypto.calculatePayloadHash(message, credentials.algorithm);510if (!Cryptiles.fixedTimeComparison(hash, authorization.hash)) {511return callback(Boom.unauthorized('Bad message hash', 'Hawk'), credentials);512}513514// Check nonce515516options.nonceFunc(authorization.nonce, authorization.ts, function (err) {517518if (err) {519return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials);520}521522// Check timestamp staleness523524if (Math.abs((authorization.ts * 1000) - now) > (options.timestampSkewSec * 1000)) {525return callback(Boom.unauthorized('Stale timestamp'), credentials);526}527528// Successful authentication529530return callback(null, credentials);531});532});533};534535536