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