3 var Boom = require('boom');
4 var Hoek = require('hoek');
5 var Cryptiles = require('cryptiles');
6 var Crypto = require('./crypto');
7 var Utils = require('./utils');
15 // Hawk authentication
18 req: node's HTTP request object or an object as follows:
22 url: '/resource/4?a=1&b=2',
25 authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="'
28 credentialsFunc: required function to lookup the set of Hawk credentials based on the provided credentials id.
29 The credentials include the MAC key, MAC algorithm, and other attributes (such as username)
30 needed by the application. This function is the equivalent of verifying the username and
31 password in Basic authentication.
33 var credentialsFunc = function (id, callback) {
35 // Lookup credentials in database
36 db.lookup(id, function (err, item) {
45 algorithm: item.algorithm,
46 // Application specific
50 return callback(null, credentials);
56 hostHeaderName: optional header field name, used to override the default 'Host' header when used
57 behind a cache of a proxy. Apache2 changes the value of the 'Host' header while preserving
58 the original (which is what the module must verify) in the 'x-forwarded-host' header field.
59 Only used when passed a node Http.ServerRequest object.
61 nonceFunc: optional nonce validation function. The function signature is function(key, nonce, ts, callback)
62 where 'callback' must be called using the signature function(err).
64 timestampSkewSec: optional number of seconds of permitted clock skew for incoming timestamps. Defaults to 60 seconds.
65 Provides a +/- skew which means actual allowed window is double the number of seconds.
67 localtimeOffsetMsec: optional local clock time offset express in a number of milliseconds (positive or negative).
70 payload: optional payload for validation. The client calculates the hash value and includes it via the 'hash'
71 header attribute. The server always ensures the value provided has been included in the request
72 MAC. When this option is provided, it validates the hash value itself. Validation is done by calculating
73 a hash value over the entire payload (assuming it has already be normalized to the same format and
74 encoding used by the client to calculate the hash on request). If the payload is not available at the time
75 of authentication, the authenticatePayload() method can be used by passing it the credentials and
76 attributes.hash returned in the authenticate callback.
78 host: optional host name override. Only used when passed a node request object.
79 port: optional port override. Only used when passed a node request object.
82 callback: function (err, credentials, artifacts) { }
85 exports.authenticate = function (req, credentialsFunc, options, callback) {
87 callback = Hoek.nextTick(callback);
91 options.nonceFunc = options.nonceFunc || internals.nonceFunc;
92 options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds
96 var now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing
98 // Convert node Http request object to a request configuration object
100 var request = Utils.parseRequest(req, options);
101 if (request instanceof Error) {
102 return callback(Boom.badRequest(request.message));
105 // Parse HTTP Authorization header
107 var attributes = Utils.parseAuthorizationHeader(request.authorization);
108 if (attributes instanceof Error) {
109 return callback(attributes);
112 // Construct artifacts container
115 method: request.method,
118 resource: request.url,
120 nonce: attributes.nonce,
121 hash: attributes.hash,
129 // Verify required header attributes
131 if (!attributes.id ||
136 return callback(Boom.badRequest('Missing attributes'), null, artifacts);
139 // Fetch Hawk credentials
141 credentialsFunc(attributes.id, function (err, credentials) {
144 return callback(err, credentials || null, artifacts);
148 return callback(Boom.unauthorized('Unknown credentials', 'Hawk'), null, artifacts);
151 if (!credentials.key ||
152 !credentials.algorithm) {
154 return callback(Boom.internal('Invalid credentials'), credentials, artifacts);
157 if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
158 return callback(Boom.internal('Unknown algorithm'), credentials, artifacts);
163 var mac = Crypto.calculateMac('header', credentials, artifacts);
164 if (!Cryptiles.fixedTimeComparison(mac, attributes.mac)) {
165 return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials, artifacts);
168 // Check payload hash
170 if (options.payload ||
171 options.payload === '') {
173 if (!attributes.hash) {
174 return callback(Boom.unauthorized('Missing required payload hash', 'Hawk'), credentials, artifacts);
177 var hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.contentType);
178 if (!Cryptiles.fixedTimeComparison(hash, attributes.hash)) {
179 return callback(Boom.unauthorized('Bad payload hash', 'Hawk'), credentials, artifacts);
185 options.nonceFunc(credentials.key, attributes.nonce, attributes.ts, function (err) {
188 return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials, artifacts);
191 // Check timestamp staleness
193 if (Math.abs((attributes.ts * 1000) - now) > (options.timestampSkewSec * 1000)) {
194 var tsm = Crypto.timestampMessage(credentials, options.localtimeOffsetMsec);
195 return callback(Boom.unauthorized('Stale timestamp', 'Hawk', tsm), credentials, artifacts);
198 // Successful authentication
200 return callback(null, credentials, artifacts);
206 // Authenticate payload hash - used when payload cannot be provided during authenticate()
209 payload: raw request payload
210 credentials: from authenticate callback
211 artifacts: from authenticate callback
212 contentType: req.headers['content-type']
215 exports.authenticatePayload = function (payload, credentials, artifacts, contentType) {
217 var calculatedHash = Crypto.calculatePayloadHash(payload, credentials.algorithm, contentType);
218 return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash);
222 // Authenticate payload hash - used when payload cannot be provided during authenticate()
225 calculatedHash: the payload hash calculated using Crypto.calculatePayloadHash()
226 artifacts: from authenticate callback
229 exports.authenticatePayloadHash = function (calculatedHash, artifacts) {
231 return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash);
235 // Generate a Server-Authorization header for a given response
238 credentials: {}, // Object received from authenticate()
239 artifacts: {} // Object received from authenticate(); 'mac', 'hash', and 'ext' - ignored
241 ext: 'application-specific', // Application specific data sent via the ext attribute
242 payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided)
243 contentType: 'application/json', // Payload content-type (ignored if hash provided)
244 hash: 'U4MKKSmiVxk37JCCrAVIjV=' // Pre-calculated payload hash
248 exports.header = function (credentials, artifacts, options) {
252 options = options || {};
255 typeof artifacts !== 'object' ||
256 typeof options !== 'object') {
261 artifacts = Hoek.clone(artifacts);
262 delete artifacts.mac;
263 artifacts.hash = options.hash;
264 artifacts.ext = options.ext;
266 // Validate credentials
270 !credentials.algorithm) {
272 // Invalid credential object
276 if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
280 // Calculate payload hash
282 if (!artifacts.hash &&
283 (options.payload || options.payload === '')) {
285 artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
288 var mac = Crypto.calculateMac('response', credentials, artifacts);
292 var header = 'Hawk mac="' + mac + '"' +
293 (artifacts.hash ? ', hash="' + artifacts.hash + '"' : '');
295 if (artifacts.ext !== null &&
296 artifacts.ext !== undefined &&
297 artifacts.ext !== '') { // Other falsey values allowed
299 header += ', ext="' + Hoek.escapeHeaderAttribute(artifacts.ext) + '"';
307 * Arguments and options are the same as authenticate() with the exception that the only supported options are:
308 * 'hostHeaderName', 'localtimeOffsetMsec', 'host', 'port'
313 internals.bewitRegex = /^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/;
316 exports.authenticateBewit = function (req, credentialsFunc, options, callback) {
318 callback = Hoek.nextTick(callback);
322 var now = Utils.now(options.localtimeOffsetMsec);
324 // Convert node Http request object to a request configuration object
326 var request = Utils.parseRequest(req, options);
327 if (request instanceof Error) {
328 return callback(Boom.badRequest(request.message));
333 if (request.url.length > Utils.limits.maxMatchLength) {
334 return callback(Boom.badRequest('Resource path exceeds max length'));
337 var resource = request.url.match(internals.bewitRegex);
339 return callback(Boom.unauthorized(null, 'Hawk'));
345 return callback(Boom.unauthorized('Empty bewit', 'Hawk'));
348 // Verify method is GET
350 if (request.method !== 'GET' &&
351 request.method !== 'HEAD') {
353 return callback(Boom.unauthorized('Invalid method', 'Hawk'));
356 // No other authentication
358 if (request.authorization) {
359 return callback(Boom.badRequest('Multiple authentications'));
364 var bewitString = Hoek.base64urlDecode(resource[3]);
365 if (bewitString instanceof Error) {
366 return callback(Boom.badRequest('Invalid bewit encoding'));
369 // Bewit format: id\exp\mac\ext ('\' is used because it is a reserved header attribute character)
371 var bewitParts = bewitString.split('\\');
372 if (bewitParts.length !== 4) {
373 return callback(Boom.badRequest('Invalid bewit structure'));
378 exp: parseInt(bewitParts[1], 10),
380 ext: bewitParts[3] || ''
387 return callback(Boom.badRequest('Missing bewit attributes'));
390 // Construct URL without bewit
392 var url = resource[1];
394 url += resource[2] + resource[4];
399 if (bewit.exp * 1000 <= now) {
400 return callback(Boom.unauthorized('Access expired', 'Hawk'), null, bewit);
403 // Fetch Hawk credentials
405 credentialsFunc(bewit.id, function (err, credentials) {
408 return callback(err, credentials || null, bewit.ext);
412 return callback(Boom.unauthorized('Unknown credentials', 'Hawk'), null, bewit);
415 if (!credentials.key ||
416 !credentials.algorithm) {
418 return callback(Boom.internal('Invalid credentials'), credentials, bewit);
421 if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
422 return callback(Boom.internal('Unknown algorithm'), credentials, bewit);
427 var mac = Crypto.calculateMac('bewit', credentials, {
437 if (!Cryptiles.fixedTimeComparison(mac, bewit.mac)) {
438 return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials, bewit);
441 // Successful authentication
443 return callback(null, credentials, bewit);
449 * options are the same as authenticate() with the exception that the only supported options are:
450 * 'nonceFunc', 'timestampSkewSec', 'localtimeOffsetMsec'
453 exports.authenticateMessage = function (host, port, message, authorization, credentialsFunc, options, callback) {
455 callback = Hoek.nextTick(callback);
459 options.nonceFunc = options.nonceFunc || internals.nonceFunc;
460 options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds
464 var now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing
466 // Validate authorization
468 if (!authorization.id ||
470 !authorization.nonce ||
471 !authorization.hash ||
472 !authorization.mac) {
474 return callback(Boom.badRequest('Invalid authorization'));
477 // Fetch Hawk credentials
479 credentialsFunc(authorization.id, function (err, credentials) {
482 return callback(err, credentials || null);
486 return callback(Boom.unauthorized('Unknown credentials', 'Hawk'));
489 if (!credentials.key ||
490 !credentials.algorithm) {
492 return callback(Boom.internal('Invalid credentials'), credentials);
495 if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
496 return callback(Boom.internal('Unknown algorithm'), credentials);
499 // Construct artifacts container
502 ts: authorization.ts,
503 nonce: authorization.nonce,
506 hash: authorization.hash
511 var mac = Crypto.calculateMac('message', credentials, artifacts);
512 if (!Cryptiles.fixedTimeComparison(mac, authorization.mac)) {
513 return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials);
516 // Check payload hash
518 var hash = Crypto.calculatePayloadHash(message, credentials.algorithm);
519 if (!Cryptiles.fixedTimeComparison(hash, authorization.hash)) {
520 return callback(Boom.unauthorized('Bad message hash', 'Hawk'), credentials);
525 options.nonceFunc(credentials.key, authorization.nonce, authorization.ts, function (err) {
528 return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials);
531 // Check timestamp staleness
533 if (Math.abs((authorization.ts * 1000) - now) > (options.timestampSkewSec * 1000)) {
534 return callback(Boom.unauthorized('Stale timestamp'), credentials);
537 // Successful authentication
539 return callback(null, credentials);
545 internals.nonceFunc = function (key, nonce, ts, nonceCallback) {
547 return nonceCallback(); // No validation