server.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  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(key, 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 = Hoek.nextTick(callback);
  64. // Default options
  65. options.nonceFunc = options.nonceFunc || internals.nonceFunc;
  66. options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds
  67. // Application time
  68. var now = Utils.now(options.localtimeOffsetMsec); // 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 ||
  123. options.payload === '') {
  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(credentials.key, 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. // Authenticate payload hash - used when payload cannot be provided during authenticate()
  159. /*
  160. calculatedHash: the payload hash calculated using Crypto.calculatePayloadHash()
  161. artifacts: from authenticate callback
  162. */
  163. exports.authenticatePayloadHash = function (calculatedHash, artifacts) {
  164. return Cryptiles.fixedTimeComparison(calculatedHash, artifacts.hash);
  165. };
  166. // Generate a Server-Authorization header for a given response
  167. /*
  168. credentials: {}, // Object received from authenticate()
  169. artifacts: {} // Object received from authenticate(); 'mac', 'hash', and 'ext' - ignored
  170. options: {
  171. ext: 'application-specific', // Application specific data sent via the ext attribute
  172. payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided)
  173. contentType: 'application/json', // Payload content-type (ignored if hash provided)
  174. hash: 'U4MKKSmiVxk37JCCrAVIjV=' // Pre-calculated payload hash
  175. }
  176. */
  177. exports.header = function (credentials, artifacts, options) {
  178. // Prepare inputs
  179. options = options || {};
  180. if (!artifacts ||
  181. typeof artifacts !== 'object' ||
  182. typeof options !== 'object') {
  183. return '';
  184. }
  185. artifacts = Hoek.clone(artifacts);
  186. delete artifacts.mac;
  187. artifacts.hash = options.hash;
  188. artifacts.ext = options.ext;
  189. // Validate credentials
  190. if (!credentials ||
  191. !credentials.key ||
  192. !credentials.algorithm) {
  193. // Invalid credential object
  194. return '';
  195. }
  196. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  197. return '';
  198. }
  199. // Calculate payload hash
  200. if (!artifacts.hash &&
  201. (options.payload || options.payload === '')) {
  202. artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
  203. }
  204. var mac = Crypto.calculateMac('response', credentials, artifacts);
  205. // Construct header
  206. var header = 'Hawk mac="' + mac + '"' +
  207. (artifacts.hash ? ', hash="' + artifacts.hash + '"' : '');
  208. if (artifacts.ext !== null &&
  209. artifacts.ext !== undefined &&
  210. artifacts.ext !== '') { // Other falsey values allowed
  211. header += ', ext="' + Hoek.escapeHeaderAttribute(artifacts.ext) + '"';
  212. }
  213. return header;
  214. };
  215. /*
  216. * Arguments and options are the same as authenticate() with the exception that the only supported options are:
  217. * 'hostHeaderName', 'localtimeOffsetMsec', 'host', 'port'
  218. */
  219. // 1 2 3 4
  220. internals.bewitRegex = /^(\/.*)([\?&])bewit\=([^&$]*)(?:&(.+))?$/;
  221. exports.authenticateBewit = function (req, credentialsFunc, options, callback) {
  222. callback = Hoek.nextTick(callback);
  223. // Application time
  224. var now = Utils.now(options.localtimeOffsetMsec);
  225. // Convert node Http request object to a request configuration object
  226. var request = Utils.parseRequest(req, options);
  227. if (request instanceof Error) {
  228. return callback(Boom.badRequest(request.message));
  229. }
  230. // Extract bewit
  231. if (request.url.length > Utils.limits.maxMatchLength) {
  232. return callback(Boom.badRequest('Resource path exceeds max length'));
  233. }
  234. var resource = request.url.match(internals.bewitRegex);
  235. if (!resource) {
  236. return callback(Boom.unauthorized(null, 'Hawk'));
  237. }
  238. // Bewit not empty
  239. if (!resource[3]) {
  240. return callback(Boom.unauthorized('Empty bewit', 'Hawk'));
  241. }
  242. // Verify method is GET
  243. if (request.method !== 'GET' &&
  244. request.method !== 'HEAD') {
  245. return callback(Boom.unauthorized('Invalid method', 'Hawk'));
  246. }
  247. // No other authentication
  248. if (request.authorization) {
  249. return callback(Boom.badRequest('Multiple authentications'));
  250. }
  251. // Parse bewit
  252. var bewitString = Hoek.base64urlDecode(resource[3]);
  253. if (bewitString instanceof Error) {
  254. return callback(Boom.badRequest('Invalid bewit encoding'));
  255. }
  256. // Bewit format: id\exp\mac\ext ('\' is used because it is a reserved header attribute character)
  257. var bewitParts = bewitString.split('\\');
  258. if (bewitParts.length !== 4) {
  259. return callback(Boom.badRequest('Invalid bewit structure'));
  260. }
  261. var bewit = {
  262. id: bewitParts[0],
  263. exp: parseInt(bewitParts[1], 10),
  264. mac: bewitParts[2],
  265. ext: bewitParts[3] || ''
  266. };
  267. if (!bewit.id ||
  268. !bewit.exp ||
  269. !bewit.mac) {
  270. return callback(Boom.badRequest('Missing bewit attributes'));
  271. }
  272. // Construct URL without bewit
  273. var url = resource[1];
  274. if (resource[4]) {
  275. url += resource[2] + resource[4];
  276. }
  277. // Check expiration
  278. if (bewit.exp * 1000 <= now) {
  279. return callback(Boom.unauthorized('Access expired', 'Hawk'), null, bewit);
  280. }
  281. // Fetch Hawk credentials
  282. credentialsFunc(bewit.id, function (err, credentials) {
  283. if (err) {
  284. return callback(err, credentials || null, bewit.ext);
  285. }
  286. if (!credentials) {
  287. return callback(Boom.unauthorized('Unknown credentials', 'Hawk'), null, bewit);
  288. }
  289. if (!credentials.key ||
  290. !credentials.algorithm) {
  291. return callback(Boom.internal('Invalid credentials'), credentials, bewit);
  292. }
  293. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  294. return callback(Boom.internal('Unknown algorithm'), credentials, bewit);
  295. }
  296. // Calculate MAC
  297. var mac = Crypto.calculateMac('bewit', credentials, {
  298. ts: bewit.exp,
  299. nonce: '',
  300. method: 'GET',
  301. resource: url,
  302. host: request.host,
  303. port: request.port,
  304. ext: bewit.ext
  305. });
  306. if (!Cryptiles.fixedTimeComparison(mac, bewit.mac)) {
  307. return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials, bewit);
  308. }
  309. // Successful authentication
  310. return callback(null, credentials, bewit);
  311. });
  312. };
  313. /*
  314. * options are the same as authenticate() with the exception that the only supported options are:
  315. * 'nonceFunc', 'timestampSkewSec', 'localtimeOffsetMsec'
  316. */
  317. exports.authenticateMessage = function (host, port, message, authorization, credentialsFunc, options, callback) {
  318. callback = Hoek.nextTick(callback);
  319. // Default options
  320. options.nonceFunc = options.nonceFunc || internals.nonceFunc;
  321. options.timestampSkewSec = options.timestampSkewSec || 60; // 60 seconds
  322. // Application time
  323. var now = Utils.now(options.localtimeOffsetMsec); // Measure now before any other processing
  324. // Validate authorization
  325. if (!authorization.id ||
  326. !authorization.ts ||
  327. !authorization.nonce ||
  328. !authorization.hash ||
  329. !authorization.mac) {
  330. return callback(Boom.badRequest('Invalid authorization'));
  331. }
  332. // Fetch Hawk credentials
  333. credentialsFunc(authorization.id, function (err, credentials) {
  334. if (err) {
  335. return callback(err, credentials || null);
  336. }
  337. if (!credentials) {
  338. return callback(Boom.unauthorized('Unknown credentials', 'Hawk'));
  339. }
  340. if (!credentials.key ||
  341. !credentials.algorithm) {
  342. return callback(Boom.internal('Invalid credentials'), credentials);
  343. }
  344. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  345. return callback(Boom.internal('Unknown algorithm'), credentials);
  346. }
  347. // Construct artifacts container
  348. var artifacts = {
  349. ts: authorization.ts,
  350. nonce: authorization.nonce,
  351. host: host,
  352. port: port,
  353. hash: authorization.hash
  354. };
  355. // Calculate MAC
  356. var mac = Crypto.calculateMac('message', credentials, artifacts);
  357. if (!Cryptiles.fixedTimeComparison(mac, authorization.mac)) {
  358. return callback(Boom.unauthorized('Bad mac', 'Hawk'), credentials);
  359. }
  360. // Check payload hash
  361. var hash = Crypto.calculatePayloadHash(message, credentials.algorithm);
  362. if (!Cryptiles.fixedTimeComparison(hash, authorization.hash)) {
  363. return callback(Boom.unauthorized('Bad message hash', 'Hawk'), credentials);
  364. }
  365. // Check nonce
  366. options.nonceFunc(credentials.key, authorization.nonce, authorization.ts, function (err) {
  367. if (err) {
  368. return callback(Boom.unauthorized('Invalid nonce', 'Hawk'), credentials);
  369. }
  370. // Check timestamp staleness
  371. if (Math.abs((authorization.ts * 1000) - now) > (options.timestampSkewSec * 1000)) {
  372. return callback(Boom.unauthorized('Stale timestamp'), credentials);
  373. }
  374. // Successful authentication
  375. return callback(null, credentials);
  376. });
  377. });
  378. };
  379. internals.nonceFunc = function (key, nonce, ts, nonceCallback) {
  380. return nonceCallback(); // No validation
  381. };