sign.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. var timespan = require('./lib/timespan');
  2. var PS_SUPPORTED = require('./lib/psSupported');
  3. var jws = require('jws');
  4. var includes = require('lodash.includes');
  5. var isBoolean = require('lodash.isboolean');
  6. var isInteger = require('lodash.isinteger');
  7. var isNumber = require('lodash.isnumber');
  8. var isPlainObject = require('lodash.isplainobject');
  9. var isString = require('lodash.isstring');
  10. var once = require('lodash.once');
  11. var SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none']
  12. if (PS_SUPPORTED) {
  13. SUPPORTED_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512');
  14. }
  15. var sign_options_schema = {
  16. expiresIn: { isValid: function(value) { return isInteger(value) || (isString(value) && value); }, message: '"expiresIn" should be a number of seconds or string representing a timespan' },
  17. notBefore: { isValid: function(value) { return isInteger(value) || (isString(value) && value); }, message: '"notBefore" should be a number of seconds or string representing a timespan' },
  18. audience: { isValid: function(value) { return isString(value) || Array.isArray(value); }, message: '"audience" must be a string or array' },
  19. algorithm: { isValid: includes.bind(null, SUPPORTED_ALGS), message: '"algorithm" must be a valid string enum value' },
  20. header: { isValid: isPlainObject, message: '"header" must be an object' },
  21. encoding: { isValid: isString, message: '"encoding" must be a string' },
  22. issuer: { isValid: isString, message: '"issuer" must be a string' },
  23. subject: { isValid: isString, message: '"subject" must be a string' },
  24. jwtid: { isValid: isString, message: '"jwtid" must be a string' },
  25. noTimestamp: { isValid: isBoolean, message: '"noTimestamp" must be a boolean' },
  26. keyid: { isValid: isString, message: '"keyid" must be a string' },
  27. mutatePayload: { isValid: isBoolean, message: '"mutatePayload" must be a boolean' }
  28. };
  29. var registered_claims_schema = {
  30. iat: { isValid: isNumber, message: '"iat" should be a number of seconds' },
  31. exp: { isValid: isNumber, message: '"exp" should be a number of seconds' },
  32. nbf: { isValid: isNumber, message: '"nbf" should be a number of seconds' }
  33. };
  34. function validate(schema, allowUnknown, object, parameterName) {
  35. if (!isPlainObject(object)) {
  36. throw new Error('Expected "' + parameterName + '" to be a plain object.');
  37. }
  38. Object.keys(object)
  39. .forEach(function(key) {
  40. var validator = schema[key];
  41. if (!validator) {
  42. if (!allowUnknown) {
  43. throw new Error('"' + key + '" is not allowed in "' + parameterName + '"');
  44. }
  45. return;
  46. }
  47. if (!validator.isValid(object[key])) {
  48. throw new Error(validator.message);
  49. }
  50. });
  51. }
  52. function validateOptions(options) {
  53. return validate(sign_options_schema, false, options, 'options');
  54. }
  55. function validatePayload(payload) {
  56. return validate(registered_claims_schema, true, payload, 'payload');
  57. }
  58. var options_to_payload = {
  59. 'audience': 'aud',
  60. 'issuer': 'iss',
  61. 'subject': 'sub',
  62. 'jwtid': 'jti'
  63. };
  64. var options_for_objects = [
  65. 'expiresIn',
  66. 'notBefore',
  67. 'noTimestamp',
  68. 'audience',
  69. 'issuer',
  70. 'subject',
  71. 'jwtid',
  72. ];
  73. module.exports = function (payload, secretOrPrivateKey, options, callback) {
  74. if (typeof options === 'function') {
  75. callback = options;
  76. options = {};
  77. } else {
  78. options = options || {};
  79. }
  80. var isObjectPayload = typeof payload === 'object' &&
  81. !Buffer.isBuffer(payload);
  82. var header = Object.assign({
  83. alg: options.algorithm || 'HS256',
  84. typ: isObjectPayload ? 'JWT' : undefined,
  85. kid: options.keyid
  86. }, options.header);
  87. function failure(err) {
  88. if (callback) {
  89. return callback(err);
  90. }
  91. throw err;
  92. }
  93. if (!secretOrPrivateKey && options.algorithm !== 'none') {
  94. return failure(new Error('secretOrPrivateKey must have a value'));
  95. }
  96. if (typeof payload === 'undefined') {
  97. return failure(new Error('payload is required'));
  98. } else if (isObjectPayload) {
  99. try {
  100. validatePayload(payload);
  101. }
  102. catch (error) {
  103. return failure(error);
  104. }
  105. if (!options.mutatePayload) {
  106. payload = Object.assign({},payload);
  107. }
  108. } else {
  109. var invalid_options = options_for_objects.filter(function (opt) {
  110. return typeof options[opt] !== 'undefined';
  111. });
  112. if (invalid_options.length > 0) {
  113. return failure(new Error('invalid ' + invalid_options.join(',') + ' option for ' + (typeof payload ) + ' payload'));
  114. }
  115. }
  116. if (typeof payload.exp !== 'undefined' && typeof options.expiresIn !== 'undefined') {
  117. return failure(new Error('Bad "options.expiresIn" option the payload already has an "exp" property.'));
  118. }
  119. if (typeof payload.nbf !== 'undefined' && typeof options.notBefore !== 'undefined') {
  120. return failure(new Error('Bad "options.notBefore" option the payload already has an "nbf" property.'));
  121. }
  122. try {
  123. validateOptions(options);
  124. }
  125. catch (error) {
  126. return failure(error);
  127. }
  128. var timestamp = payload.iat || Math.floor(Date.now() / 1000);
  129. if (options.noTimestamp) {
  130. delete payload.iat;
  131. } else if (isObjectPayload) {
  132. payload.iat = timestamp;
  133. }
  134. if (typeof options.notBefore !== 'undefined') {
  135. try {
  136. payload.nbf = timespan(options.notBefore, timestamp);
  137. }
  138. catch (err) {
  139. return failure(err);
  140. }
  141. if (typeof payload.nbf === 'undefined') {
  142. return failure(new Error('"notBefore" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
  143. }
  144. }
  145. if (typeof options.expiresIn !== 'undefined' && typeof payload === 'object') {
  146. try {
  147. payload.exp = timespan(options.expiresIn, timestamp);
  148. }
  149. catch (err) {
  150. return failure(err);
  151. }
  152. if (typeof payload.exp === 'undefined') {
  153. return failure(new Error('"expiresIn" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
  154. }
  155. }
  156. Object.keys(options_to_payload).forEach(function (key) {
  157. var claim = options_to_payload[key];
  158. if (typeof options[key] !== 'undefined') {
  159. if (typeof payload[claim] !== 'undefined') {
  160. return failure(new Error('Bad "options.' + key + '" option. The payload already has an "' + claim + '" property.'));
  161. }
  162. payload[claim] = options[key];
  163. }
  164. });
  165. var encoding = options.encoding || 'utf8';
  166. if (typeof callback === 'function') {
  167. callback = callback && once(callback);
  168. jws.createSign({
  169. header: header,
  170. privateKey: secretOrPrivateKey,
  171. payload: payload,
  172. encoding: encoding
  173. }).once('error', callback)
  174. .once('done', function (signature) {
  175. callback(null, signature);
  176. });
  177. } else {
  178. return jws.sign({header: header, payload: payload, secret: secretOrPrivateKey, encoding: encoding});
  179. }
  180. };