Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81146 views
1
// Load modules
2
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');
8
9
10
// Declare internals
11
12
var internals = {};
13
14
15
// Hawk authentication
16
17
/*
18
req: node's HTTP request object or an object as follows:
19
20
var request = {
21
method: 'GET',
22
url: '/resource/4?a=1&b=2',
23
host: 'example.com',
24
port: 8080,
25
authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="'
26
};
27
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.
32
33
var credentialsFunc = function (id, callback) {
34
35
// Lookup credentials in database
36
db.lookup(id, function (err, item) {
37
38
if (err || !item) {
39
return callback(err);
40
}
41
42
var credentials = {
43
// Required
44
key: item.key,
45
algorithm: item.algorithm,
46
// Application specific
47
user: item.user
48
};
49
50
return callback(null, credentials);
51
});
52
};
53
54
options: {
55
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.
60
61
nonceFunc: optional nonce validation function. The function signature is function(nonce, ts, callback)
62
where 'callback' must be called using the signature function(err).
63
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.
66
67
localtimeOffsetMsec: optional local clock time offset express in a number of milliseconds (positive or negative).
68
Defaults to 0.
69
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.
77
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.
80
}
81
82
callback: function (err, credentials, artifacts) { }
83
*/
84
85
exports.authenticate = function (req, credentialsFunc, options, callback) {
86
87
callback = Hoek.nextTick(callback);
88
89
// Default options
90
91
options.nonceFunc = options.nonceFunc || function (nonce, ts, nonceCallback) { return nonceCallback(); }; // No validation
92
options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds
93
94
// Application time
95
96
var now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing
97
98
// Convert node Http request object to a request configuration object
99
100
var request = Utils.parseRequest(req, options);
101
if (request instanceof Error) {
102
return callback(Boom.badRequest(request.message));
103
}
104
105
// Parse HTTP Authorization header
106
107
var attributes = Utils.parseAuthorizationHeader(request.authorization);
108
if (attributes instanceof Error) {
109
return callback(attributes);
110
}
111
112
// Construct artifacts container
113
114
var artifacts = {
115
method: request.method,
116
host: request.host,
117
port: request.port,
118
resource: request.url,
119
ts: attributes.ts,
120
nonce: attributes.nonce,
121
hash: attributes.hash,
122
ext: attributes.ext,
123
app: attributes.app,
124
dlg: attributes.dlg,
125
mac: attributes.mac,
126
id: attributes.id
127
};
128
129
// Verify required header attributes
130
131
if (!attributes.id ||
132
!attributes.ts ||
133
!attributes.nonce ||
134
!attributes.mac) {
135
136
return callback(Boom.badRequest('Missing attributes'), null, artifacts);
137
}
138
139
// Fetch Hawk credentials
140
141
credentialsFunc(attributes.id, function (err, credentials) {
142
143
if (err) {
144
return callback(err, credentials || null, artifacts);
145
}
146
147
if (!credentials) {
148
return callback(Boom.unauthorized('Unknown credentials', 'Hawk'), null, artifacts);
149
}
150
151
if (!credentials.key ||
152
!credentials.algorithm) {
153
154
return callback(Boom.internal('Invalid credentials'), credentials, artifacts);
155
}
156
157
if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
158
return callback(Boom.internal('Unknown algorithm'), credentials, artifacts);
159
}
160
161
// Calculate MAC
162
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);
166
}
167
168
// Check payload hash
169
170
if (options.payload ||
171
options.payload === '') {
172
173
if (!attributes.hash) {
174
return callback(Boom.unauthorized('Missing required payload hash', 'Hawk'), credentials, artifacts);
175
}
176
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);
180
}
181
}
182
183
// Check nonce
184
185
options.nonceFunc(attributes.nonce, attributes.ts, function (err) {
186
187
if (err) {
188
return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials, artifacts);
189
}
190
191
// Check timestamp staleness
192
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);
196
}
197
198
// Successful authentication
199
200
return callback(null, credentials, artifacts);
201
});
202
});
203
};
204
205
206
// Authenticate payload hash - used when payload cannot be provided during authenticate()
207
208
/*
209
payload: raw request payload
210
credentials: from authenticate callback
211
artifacts: from authenticate callback
212
contentType: req.headers['content-type']
213
*/
214
215
exports.authenticatePayload = function (payload, credentials, artifacts, contentType) {
216
217
var calculatedHash = Crypto.calculatePayloadHash(payload, credentials.algorithm, contentType);
218
return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash);
219
};
220
221
222
// Authenticate payload hash - used when payload cannot be provided during authenticate()
223
224
/*
225
calculatedHash: the payload hash calculated using Crypto.calculatePayloadHash()
226
artifacts: from authenticate callback
227
*/
228
229
exports.authenticatePayloadHash = function (calculatedHash, artifacts) {
230
231
return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash);
232
};
233
234
235
// Generate a Server-Authorization header for a given response
236
237
/*
238
credentials: {}, // Object received from authenticate()
239
artifacts: {} // Object received from authenticate(); 'mac', 'hash', and 'ext' - ignored
240
options: {
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
245
}
246
*/
247
248
exports.header = function (credentials, artifacts, options) {
249
250
// Prepare inputs
251
252
options = options || {};
253
254
if (!artifacts ||
255
typeof artifacts !== 'object' ||
256
typeof options !== 'object') {
257
258
return '';
259
}
260
261
artifacts = Hoek.clone(artifacts);
262
delete artifacts.mac;
263
artifacts.hash = options.hash;
264
artifacts.ext = options.ext;
265
266
// Validate credentials
267
268
if (!credentials ||
269
!credentials.key ||
270
!credentials.algorithm) {
271
272
// Invalid credential object
273
return '';
274
}
275
276
if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
277
return '';
278
}
279
280
// Calculate payload hash
281
282
if (!artifacts.hash &&
283
(options.payload || options.payload === '')) {
284
285
artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
286
}
287
288
var mac = Crypto.calculateMac('response', credentials, artifacts);
289
290
// Construct header
291
292
var header = 'Hawk mac="' + mac + '"' +
293
(artifacts.hash ? ', hash="' + artifacts.hash + '"' : '');
294
295
if (artifacts.ext !== null &&
296
artifacts.ext !== undefined &&
297
artifacts.ext !== '') { // Other falsey values allowed
298
299
header += ', ext="' + Hoek.escapeHeaderAttribute(artifacts.ext) + '"';
300
}
301
302
return header;
303
};
304
305
306
/*
307
* Arguments and options are the same as authenticate() with the exception that the only supported options are:
308
* 'hostHeaderName', 'localtimeOffsetMsec', 'host', 'port'
309
*/
310
311
exports.authenticateBewit = function (req, credentialsFunc, options, callback) {
312
313
callback = Hoek.nextTick(callback);
314
315
// Application time
316
317
var now = Utils.now(options.localtimeOffsetMsec);
318
319
// Convert node Http request object to a request configuration object
320
321
var request = Utils.parseRequest(req, options);
322
if (request instanceof Error) {
323
return callback(Boom.badRequest(request.message));
324
}
325
326
// Extract bewit
327
328
// 1 2 3 4
329
var resource = request.url.match(/^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/);
330
if (!resource) {
331
return callback(Boom.unauthorized(null, 'Hawk'));
332
}
333
334
// Bewit not empty
335
336
if (!resource[3]) {
337
return callback(Boom.unauthorized('Empty bewit', 'Hawk'));
338
}
339
340
// Verify method is GET
341
342
if (request.method !== 'GET' &&
343
request.method !== 'HEAD') {
344
345
return callback(Boom.unauthorized('Invalid method', 'Hawk'));
346
}
347
348
// No other authentication
349
350
if (request.authorization) {
351
return callback(Boom.badRequest('Multiple authentications'));
352
}
353
354
// Parse bewit
355
356
var bewitString = Hoek.base64urlDecode(resource[3]);
357
if (bewitString instanceof Error) {
358
return callback(Boom.badRequest('Invalid bewit encoding'));
359
}
360
361
// Bewit format: id\exp\mac\ext ('\' is used because it is a reserved header attribute character)
362
363
var bewitParts = bewitString.split('\\');
364
if (bewitParts.length !== 4) {
365
return callback(Boom.badRequest('Invalid bewit structure'));
366
}
367
368
var bewit = {
369
id: bewitParts[0],
370
exp: parseInt(bewitParts[1], 10),
371
mac: bewitParts[2],
372
ext: bewitParts[3] || ''
373
};
374
375
if (!bewit.id ||
376
!bewit.exp ||
377
!bewit.mac) {
378
379
return callback(Boom.badRequest('Missing bewit attributes'));
380
}
381
382
// Construct URL without bewit
383
384
var url = resource[1];
385
if (resource[4]) {
386
url += resource[2] + resource[4];
387
}
388
389
// Check expiration
390
391
if (bewit.exp * 1000 <= now) {
392
return callback(Boom.unauthorized('Access expired', 'Hawk'), null, bewit);
393
}
394
395
// Fetch Hawk credentials
396
397
credentialsFunc(bewit.id, function (err, credentials) {
398
399
if (err) {
400
return callback(err, credentials || null, bewit.ext);
401
}
402
403
if (!credentials) {
404
return callback(Boom.unauthorized('Unknown credentials', 'Hawk'), null, bewit);
405
}
406
407
if (!credentials.key ||
408
!credentials.algorithm) {
409
410
return callback(Boom.internal('Invalid credentials'), credentials, bewit);
411
}
412
413
if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
414
return callback(Boom.internal('Unknown algorithm'), credentials, bewit);
415
}
416
417
// Calculate MAC
418
419
var mac = Crypto.calculateMac('bewit', credentials, {
420
ts: bewit.exp,
421
nonce: '',
422
method: 'GET',
423
resource: url,
424
host: request.host,
425
port: request.port,
426
ext: bewit.ext
427
});
428
429
if (!Cryptiles.fixedTimeComparison(mac, bewit.mac)) {
430
return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials, bewit);
431
}
432
433
// Successful authentication
434
435
return callback(null, credentials, bewit);
436
});
437
};
438
439
440
/*
441
* options are the same as authenticate() with the exception that the only supported options are:
442
* 'nonceFunc', 'timestampSkewSec', 'localtimeOffsetMsec'
443
*/
444
445
exports.authenticateMessage = function (host, port, message, authorization, credentialsFunc, options, callback) {
446
447
callback = Hoek.nextTick(callback);
448
449
// Default options
450
451
options.nonceFunc = options.nonceFunc || function (nonce, ts, nonceCallback) { return nonceCallback(); }; // No validation
452
options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds
453
454
// Application time
455
456
var now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing
457
458
// Validate authorization
459
460
if (!authorization.id ||
461
!authorization.ts ||
462
!authorization.nonce ||
463
!authorization.hash ||
464
!authorization.mac) {
465
466
return callback(Boom.badRequest('Invalid authorization'))
467
}
468
469
// Fetch Hawk credentials
470
471
credentialsFunc(authorization.id, function (err, credentials) {
472
473
if (err) {
474
return callback(err, credentials || null);
475
}
476
477
if (!credentials) {
478
return callback(Boom.unauthorized('Unknown credentials', 'Hawk'));
479
}
480
481
if (!credentials.key ||
482
!credentials.algorithm) {
483
484
return callback(Boom.internal('Invalid credentials'), credentials);
485
}
486
487
if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
488
return callback(Boom.internal('Unknown algorithm'), credentials);
489
}
490
491
// Construct artifacts container
492
493
var artifacts = {
494
ts: authorization.ts,
495
nonce: authorization.nonce,
496
host: host,
497
port: port,
498
hash: authorization.hash
499
};
500
501
// Calculate MAC
502
503
var mac = Crypto.calculateMac('message', credentials, artifacts);
504
if (!Cryptiles.fixedTimeComparison(mac, authorization.mac)) {
505
return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials);
506
}
507
508
// Check payload hash
509
510
var hash = Crypto.calculatePayloadHash(message, credentials.algorithm);
511
if (!Cryptiles.fixedTimeComparison(hash, authorization.hash)) {
512
return callback(Boom.unauthorized('Bad message hash', 'Hawk'), credentials);
513
}
514
515
// Check nonce
516
517
options.nonceFunc(authorization.nonce, authorization.ts, function (err) {
518
519
if (err) {
520
return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials);
521
}
522
523
// Check timestamp staleness
524
525
if (Math.abs((authorization.ts * 1000) - now) > (options.timestampSkewSec * 1000)) {
526
return callback(Boom.unauthorized('Stale timestamp'), credentials);
527
}
528
529
// Successful authentication
530
531
return callback(null, credentials);
532
});
533
});
534
};
535
536