server.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. // Load modules
  2. var Boom = require('boom');
  3. var Hoek = require('hoek');
  4. var Cryptiles = require('cryptiles');
  5. var Crypto = require('./crypto');
  6. var Utils = require('./utils');
  7. // Declare internals
  8. var internals = {};
  9. // Hawk authentication
  10. /*
  11. req: node's HTTP request object or an object as follows:
  12. var request = {
  13. method: 'GET',
  14. url: '/resource/4?a=1&b=2',
  15. host: 'example.com',
  16. port: 8080,
  17. authorization: 'Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="'
  18. };
  19. credentialsFunc: required function to lookup the set of Hawk credentials based on the provided credentials id.
  20. The credentials include the MAC key, MAC algorithm, and other attributes (such as username)
  21. needed by the application. This function is the equivalent of verifying the username and
  22. password in Basic authentication.
  23. var credentialsFunc = function (id, callback) {
  24. // Lookup credentials in database
  25. db.lookup(id, function (err, item) {
  26. if (err || !item) {
  27. return callback(err);
  28. }
  29. var credentials = {
  30. // Required
  31. key: item.key,
  32. algorithm: item.algorithm,
  33. // Application specific
  34. user: item.user
  35. };
  36. return callback(null, credentials);
  37. });
  38. };
  39. options: {
  40. hostHeaderName: optional header field name, used to override the default 'Host' header when used
  41. behind a cache of a proxy. Apache2 changes the value of the 'Host' header while preserving
  42. the original (which is what the module must verify) in the 'x-forwarded-host' header field.
  43. Only used when passed a node Http.ServerRequest object.
  44. nonceFunc: optional nonce validation function. The function signature is function(nonce, ts, callback)
  45. where 'callback' must be called using the signature function(err).
  46. timestampSkewSec: optional number of seconds of permitted clock skew for incoming timestamps. Defaults to 60 seconds.
  47. Provides a +/- skew which means actual allowed window is double the number of seconds.
  48. localtimeOffsetMsec: optional local clock time offset express in a number of milliseconds (positive or negative).
  49. Defaults to 0.
  50. payload: optional payload for validation. The client calculates the hash value and includes it via the 'hash'
  51. header attribute. The server always ensures the value provided has been included in the request
  52. MAC. When this option is provided, it validates the hash value itself. Validation is done by calculating
  53. a hash value over the entire payload (assuming it has already be normalized to the same format and
  54. encoding used by the client to calculate the hash on request). If the payload is not available at the time
  55. of authentication, the authenticatePayload() method can be used by passing it the credentials and
  56. attributes.hash returned in the authenticate callback.
  57. host: optional host name override. Only used when passed a node request object.
  58. port: optional port override. Only used when passed a node request object.
  59. }
  60. callback: function (err, credentials, artifacts) { }
  61. */
  62. exports.authenticate = function (req, credentialsFunc, options, callback) {
  63. callback = Utils.nextTick(callback);
  64. // Default options
  65. options.nonceFunc = options.nonceFunc || function (nonce, ts, nonceCallback) { return nonceCallback(); }; // No validation
  66. options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds
  67. // Application time
  68. var now = Utils.now() + (options.localtimeOffsetMsec || 0); // Measure now before any other processing
  69. // Convert node Http request object to a request configuration object
  70. var request = Utils.parseRequest(req, options);
  71. if (request instanceof Error) {
  72. return callback(Boom.badRequest(request.message));
  73. }
  74. // Parse HTTP Authorization header
  75. var attributes = Utils.parseAuthorizationHeader(request.authorization);
  76. if (attributes instanceof Error) {
  77. return callback(attributes);
  78. }
  79. // Construct artifacts container
  80. var artifacts = {
  81. method: request.method,
  82. host: request.host,
  83. port: request.port,
  84. resource: request.url,
  85. ts: attributes.ts,
  86. nonce: attributes.nonce,
  87. hash: attributes.hash,
  88. ext: attributes.ext,
  89. app: attributes.app,
  90. dlg: attributes.dlg,
  91. mac: attributes.mac,
  92. id: attributes.id
  93. };
  94. // Verify required header attributes
  95. if (!attributes.id ||
  96. !attributes.ts ||
  97. !attributes.nonce ||
  98. !attributes.mac) {
  99. return callback(Boom.badRequest('Missing attributes'), null, artifacts);
  100. }
  101. // Fetch Hawk credentials
  102. credentialsFunc(attributes.id, function (err, credentials) {
  103. if (err) {
  104. return callback(err, credentials || null, artifacts);
  105. }
  106. if (!credentials) {
  107. return callback(Boom.unauthorized('Unknown credentials', 'Hawk'), null, artifacts);
  108. }
  109. if (!credentials.key ||
  110. !credentials.algorithm) {
  111. return callback(Boom.internal('Invalid credentials'), credentials, artifacts);
  112. }
  113. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  114. return callback(Boom.internal('Unknown algorithm'), credentials, artifacts);
  115. }
  116. // Calculate MAC
  117. var mac = Crypto.calculateMac('header', credentials, artifacts);
  118. if (!Cryptiles.fixedTimeComparison(mac, attributes.mac)) {
  119. return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials, artifacts);
  120. }
  121. // Check payload hash
  122. if (options.payload !== null &&
  123. options.payload !== undefined) { // '' is valid
  124. if (!attributes.hash) {
  125. return callback(Boom.unauthorized('Missing required payload hash', 'Hawk'), credentials, artifacts);
  126. }
  127. var hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.contentType);
  128. if (!Cryptiles.fixedTimeComparison(hash, attributes.hash)) {
  129. return callback(Boom.unauthorized('Bad payload hash', 'Hawk'), credentials, artifacts);
  130. }
  131. }
  132. // Check nonce
  133. options.nonceFunc(attributes.nonce, attributes.ts, function (err) {
  134. if (err) {
  135. return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials, artifacts);
  136. }
  137. // Check timestamp staleness
  138. if (Math.abs((attributes.ts * 1000) - now) > (options.timestampSkewSec * 1000)) {
  139. var tsm = Crypto.timestampMessage(credentials, options.localtimeOffsetMsec);
  140. return callback(Boom.unauthorized('Stale timestamp', 'Hawk', tsm), credentials, artifacts);
  141. }
  142. // Successful authentication
  143. return callback(null, credentials, artifacts);
  144. });
  145. });
  146. };
  147. // Authenticate payload hash - used when payload cannot be provided during authenticate()
  148. /*
  149. payload: raw request payload
  150. credentials: from authenticate callback
  151. artifacts: from authenticate callback
  152. contentType: req.headers['content-type']
  153. */
  154. exports.authenticatePayload = function (payload, credentials, artifacts, contentType) {
  155. var calculatedHash = Crypto.calculatePayloadHash(payload, credentials.algorithm, contentType);
  156. return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash);
  157. };
  158. // Generate a Server-Authorization header for a given response
  159. /*
  160. credentials: {}, // Object received from authenticate()
  161. artifacts: {} // Object received from authenticate(); 'mac', 'hash', and 'ext' - ignored
  162. options: {
  163. ext: 'application-specific', // Application specific data sent via the ext attribute
  164. payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided)
  165. contentType: 'application/json', // Payload content-type (ignored if hash provided)
  166. hash: 'U4MKKSmiVxk37JCCrAVIjV=' // Pre-calculated payload hash
  167. }
  168. */
  169. exports.header = function (credentials, artifacts, options) {
  170. // Prepare inputs
  171. options = options || {};
  172. if (!artifacts ||
  173. typeof artifacts !== 'object' ||
  174. typeof options !== 'object') {
  175. return '';
  176. }
  177. artifacts = Hoek.clone(artifacts);
  178. delete artifacts.mac;
  179. artifacts.hash = options.hash;
  180. artifacts.ext = options.ext;
  181. // Validate credentials
  182. if (!credentials ||
  183. !credentials.key ||
  184. !credentials.algorithm) {
  185. // Invalid credential object
  186. return '';
  187. }
  188. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  189. return '';
  190. }
  191. // Calculate payload hash
  192. if (!artifacts.hash &&
  193. options.hasOwnProperty('payload')) {
  194. artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
  195. }
  196. var mac = Crypto.calculateMac('response', credentials, artifacts);
  197. // Construct header
  198. var header = 'Hawk mac="' + mac + '"' +
  199. (artifacts.hash ? ', hash="' + artifacts.hash + '"' : '');
  200. if (artifacts.ext !== null &&
  201. artifacts.ext !== undefined &&
  202. artifacts.ext !== '') { // Other falsey values allowed
  203. header += ', ext="' + Utils.escapeHeaderAttribute(artifacts.ext) + '"';
  204. }
  205. return header;
  206. };
  207. /*
  208. * Arguments and options are the same as authenticate() with the exception that the only supported options are:
  209. * 'hostHeaderName', 'localtimeOffsetMsec', 'host', 'port'
  210. */
  211. exports.authenticateBewit = function (req, credentialsFunc, options, callback) {
  212. callback = Utils.nextTick(callback);
  213. // Application time
  214. var now = Utils.now() + (options.localtimeOffsetMsec || 0);
  215. // Convert node Http request object to a request configuration object
  216. var request = Utils.parseRequest(req, options);
  217. if (request instanceof Error) {
  218. return callback(Boom.badRequest(request.message));
  219. }
  220. // Extract bewit
  221. // 1 2 3 4
  222. var resource = request.url.match(/^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/);
  223. if (!resource) {
  224. return callback(Boom.unauthorized(null, 'Hawk'));
  225. }
  226. // Bewit not empty
  227. if (!resource[3]) {
  228. return callback(Boom.unauthorized('Empty bewit', 'Hawk'));
  229. }
  230. // Verify method is GET
  231. if (request.method !== 'GET' &&
  232. request.method !== 'HEAD') {
  233. return callback(Boom.unauthorized('Invalid method', 'Hawk'));
  234. }
  235. // No other authentication
  236. if (request.authorization) {
  237. return callback(Boom.badRequest('Multiple authentications', 'Hawk'));
  238. }
  239. // Parse bewit
  240. var bewitString = Utils.base64urlDecode(resource[3]);
  241. if (bewitString instanceof Error) {
  242. return callback(Boom.badRequest('Invalid bewit encoding'));
  243. }
  244. // Bewit format: id\exp\mac\ext ('\' is used because it is a reserved header attribute character)
  245. var bewitParts = bewitString.split('\\');
  246. if (!bewitParts ||
  247. bewitParts.length !== 4) {
  248. return callback(Boom.badRequest('Invalid bewit structure'));
  249. }
  250. var bewit = {
  251. id: bewitParts[0],
  252. exp: parseInt(bewitParts[1], 10),
  253. mac: bewitParts[2],
  254. ext: bewitParts[3] || ''
  255. };
  256. if (!bewit.id ||
  257. !bewit.exp ||
  258. !bewit.mac) {
  259. return callback(Boom.badRequest('Missing bewit attributes'));
  260. }
  261. // Construct URL without bewit
  262. var url = resource[1];
  263. if (resource[4]) {
  264. url += resource[2] + resource[4];
  265. }
  266. // Check expiration
  267. if (bewit.exp * 1000 <= now) {
  268. return callback(Boom.unauthorized('Access expired', 'Hawk'), null, bewit);
  269. }
  270. // Fetch Hawk credentials
  271. credentialsFunc(bewit.id, function (err, credentials) {
  272. if (err) {
  273. return callback(err, credentials || null, bewit.ext);
  274. }
  275. if (!credentials) {
  276. return callback(Boom.unauthorized('Unknown credentials', 'Hawk'), null, bewit);
  277. }
  278. if (!credentials.key ||
  279. !credentials.algorithm) {
  280. return callback(Boom.internal('Invalid credentials'), credentials, bewit);
  281. }
  282. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  283. return callback(Boom.internal('Unknown algorithm'), credentials, bewit);
  284. }
  285. // Calculate MAC
  286. var mac = Crypto.calculateMac('bewit', credentials, {
  287. ts: bewit.exp,
  288. nonce: '',
  289. method: 'GET',
  290. resource: url,
  291. host: request.host,
  292. port: request.port,
  293. ext: bewit.ext
  294. });
  295. if (!Cryptiles.fixedTimeComparison(mac, bewit.mac)) {
  296. return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials, bewit);
  297. }
  298. // Successful authentication
  299. return callback(null, credentials, bewit);
  300. });
  301. };
  302. /*
  303. * options are the same as authenticate() with the exception that the only supported options are:
  304. * 'nonceFunc', 'timestampSkewSec', 'localtimeOffsetMsec'
  305. */
  306. exports.authenticateMessage = function (host, port, message, authorization, credentialsFunc, options, callback) {
  307. callback = Utils.nextTick(callback);
  308. // Default options
  309. options.nonceFunc = options.nonceFunc || function (nonce, ts, nonceCallback) { return nonceCallback(); }; // No validation
  310. options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds
  311. // Application time
  312. var now = Utils.now() + (options.localtimeOffsetMsec || 0); // Measure now before any other processing
  313. // Validate authorization
  314. if (!authorization.id ||
  315. !authorization.ts ||
  316. !authorization.nonce ||
  317. !authorization.hash ||
  318. !authorization.mac) {
  319. return callback(Boom.badRequest('Invalid authorization'))
  320. }
  321. // Fetch Hawk credentials
  322. credentialsFunc(authorization.id, function (err, credentials) {
  323. if (err) {
  324. return callback(err, credentials || null);
  325. }
  326. if (!credentials) {
  327. return callback(Boom.unauthorized('Unknown credentials', 'Hawk'));
  328. }
  329. if (!credentials.key ||
  330. !credentials.algorithm) {
  331. return callback(Boom.internal('Invalid credentials'), credentials);
  332. }
  333. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  334. return callback(Boom.internal('Unknown algorithm'), credentials);
  335. }
  336. // Construct artifacts container
  337. var artifacts = {
  338. ts: authorization.ts,
  339. nonce: authorization.nonce,
  340. host: host,
  341. port: port,
  342. hash: authorization.hash
  343. };
  344. // Calculate MAC
  345. var mac = Crypto.calculateMac('message', credentials, artifacts);
  346. if (!Cryptiles.fixedTimeComparison(mac, authorization.mac)) {
  347. return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials);
  348. }
  349. // Check payload hash
  350. var hash = Crypto.calculatePayloadHash(message, credentials.algorithm);
  351. if (!Cryptiles.fixedTimeComparison(hash, authorization.hash)) {
  352. return callback(Boom.unauthorized('Bad message hash', 'Hawk'), credentials);
  353. }
  354. // Check nonce
  355. options.nonceFunc(authorization.nonce, authorization.ts, function (err) {
  356. if (err) {
  357. return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials);
  358. }
  359. // Check timestamp staleness
  360. if (Math.abs((authorization.ts * 1000) - now) > (options.timestampSkewSec * 1000)) {
  361. return callback(Boom.unauthorized('Stale timestamp'), credentials);
  362. }
  363. // Successful authentication
  364. return callback(null, credentials);
  365. });
  366. });
  367. };