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