You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

146 lines
4.2 KiB

  1. var Joi = require('joi');
  2. var timespan = require('./lib/timespan');
  3. var xtend = require('xtend');
  4. var jws = require('jws');
  5. var cb = require('cb');
  6. var sign_options_schema = Joi.object().keys({
  7. expiresIn: [Joi.number().integer(), Joi.string()],
  8. notBefore: [Joi.number().integer(), Joi.string()],
  9. audience: [Joi.string(), Joi.array()],
  10. algorithm: Joi.string().valid('RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none'),
  11. header: Joi.object(),
  12. encoding: Joi.string(),
  13. issuer: Joi.string(),
  14. subject: Joi.string(),
  15. jwtid: Joi.string(),
  16. noTimestamp: Joi.boolean()
  17. });
  18. var registered_claims_schema = Joi.object().keys({
  19. iat: Joi.number(),
  20. exp: Joi.number(),
  21. nbf: Joi.number()
  22. }).unknown();
  23. var options_to_payload = {
  24. 'audience': 'aud',
  25. 'issuer': 'iss',
  26. 'subject': 'sub',
  27. 'jwtid': 'jti'
  28. };
  29. var options_for_objects = [
  30. 'expiresIn',
  31. 'notBefore',
  32. 'noTimestamp',
  33. 'audience',
  34. 'issuer',
  35. 'subject',
  36. 'jwtid',
  37. ];
  38. module.exports = function (payload, secretOrPrivateKey, options, callback) {
  39. options = options || {};
  40. var isObjectPayload = typeof payload === 'object' &&
  41. !Buffer.isBuffer(payload);
  42. var header = xtend({
  43. alg: options.algorithm || 'HS256',
  44. typ: isObjectPayload ? 'JWT' : undefined
  45. }, options.header);
  46. function failure(err) {
  47. if (callback) {
  48. return callback(err);
  49. }
  50. throw err;
  51. }
  52. if (typeof payload === 'undefined') {
  53. return failure(new Error('payload is required'));
  54. } else if (isObjectPayload) {
  55. var payload_validation_result = registered_claims_schema.validate(payload);
  56. if (payload_validation_result.error) {
  57. return failure(payload_validation_result.error);
  58. }
  59. payload = xtend(payload);
  60. } else {
  61. var invalid_options = options_for_objects.filter(function (opt) {
  62. return typeof options[opt] !== 'undefined';
  63. });
  64. if (invalid_options.length > 0) {
  65. return failure(new Error('invalid ' + invalid_options.join(',') + ' option for ' + (typeof payload ) + ' payload'));
  66. }
  67. }
  68. if (typeof payload.exp !== 'undefined' && typeof options.expiresIn !== 'undefined') {
  69. return failure(new Error('Bad "options.expiresIn" option the payload already has an "exp" property.'));
  70. }
  71. if (typeof payload.nbf !== 'undefined' && typeof options.notBefore !== 'undefined') {
  72. return failure(new Error('Bad "options.notBefore" option the payload already has an "nbf" property.'));
  73. }
  74. var validation_result = sign_options_schema.validate(options);
  75. if (validation_result.error) {
  76. return failure(validation_result.error);
  77. }
  78. var timestamp = payload.iat || Math.floor(Date.now() / 1000);
  79. if (!options.noTimestamp) {
  80. payload.iat = timestamp;
  81. } else {
  82. delete payload.iat;
  83. }
  84. if (typeof options.notBefore !== 'undefined') {
  85. payload.nbf = timespan(options.notBefore);
  86. if (typeof payload.nbf === 'undefined') {
  87. return failure(new Error('"notBefore" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
  88. }
  89. }
  90. if (typeof options.expiresIn !== 'undefined' && typeof payload === 'object') {
  91. payload.exp = timespan(options.expiresIn, timestamp);
  92. if (typeof payload.exp === 'undefined') {
  93. return failure(new Error('"expiresIn" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
  94. }
  95. }
  96. Object.keys(options_to_payload).forEach(function (key) {
  97. var claim = options_to_payload[key];
  98. if (typeof options[key] !== 'undefined') {
  99. if (typeof payload[claim] !== 'undefined') {
  100. return failure(new Error('Bad "options.' + key + '" option. The payload already has an "' + claim + '" property.'));
  101. }
  102. payload[claim] = options[key];
  103. }
  104. });
  105. var encoding = options.encoding || 'utf8';
  106. if (typeof callback === 'function') {
  107. callback = callback && cb(callback).once();
  108. jws.createSign({
  109. header: header,
  110. privateKey: secretOrPrivateKey,
  111. payload: payload,
  112. encoding: encoding
  113. }).once('error', callback)
  114. .once('done', function (signature) {
  115. callback(null, signature);
  116. });
  117. } else {
  118. return jws.sign({header: header, payload: payload, secret: secretOrPrivateKey, encoding: encoding});
  119. }
  120. };