sign.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. const timespan = require('./lib/timespan');
  2. const PS_SUPPORTED = require('./lib/psSupported');
  3. const validateAsymmetricKey = require('./lib/validateAsymmetricKey');
  4. const jws = require('jws');
  5. const {includes, isBoolean, isInteger, isNumber, isPlainObject, isString, once} = require('lodash')
  6. const { KeyObject, createSecretKey, createPrivateKey } = require('crypto')
  7. const SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none'];
  8. if (PS_SUPPORTED) {
  9. SUPPORTED_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512');
  10. }
  11. const sign_options_schema = {
  12. expiresIn: { isValid: function(value) { return isInteger(value) || (isString(value) && value); }, message: '"expiresIn" should be a number of seconds or string representing a timespan' },
  13. notBefore: { isValid: function(value) { return isInteger(value) || (isString(value) && value); }, message: '"notBefore" should be a number of seconds or string representing a timespan' },
  14. audience: { isValid: function(value) { return isString(value) || Array.isArray(value); }, message: '"audience" must be a string or array' },
  15. algorithm: { isValid: includes.bind(null, SUPPORTED_ALGS), message: '"algorithm" must be a valid string enum value' },
  16. header: { isValid: isPlainObject, message: '"header" must be an object' },
  17. encoding: { isValid: isString, message: '"encoding" must be a string' },
  18. issuer: { isValid: isString, message: '"issuer" must be a string' },
  19. subject: { isValid: isString, message: '"subject" must be a string' },
  20. jwtid: { isValid: isString, message: '"jwtid" must be a string' },
  21. noTimestamp: { isValid: isBoolean, message: '"noTimestamp" must be a boolean' },
  22. keyid: { isValid: isString, message: '"keyid" must be a string' },
  23. mutatePayload: { isValid: isBoolean, message: '"mutatePayload" must be a boolean' },
  24. allowInsecureKeySizes: { isValid: isBoolean, message: '"allowInsecureKeySizes" must be a boolean'},
  25. allowInvalidAsymmetricKeyTypes: { isValid: isBoolean, message: '"allowInvalidAsymmetricKeyTypes" must be a boolean'}
  26. };
  27. const registered_claims_schema = {
  28. iat: { isValid: isNumber, message: '"iat" should be a number of seconds' },
  29. exp: { isValid: isNumber, message: '"exp" should be a number of seconds' },
  30. nbf: { isValid: isNumber, message: '"nbf" should be a number of seconds' }
  31. };
  32. function validate(schema, allowUnknown, object, parameterName) {
  33. if (!isPlainObject(object)) {
  34. throw new Error('Expected "' + parameterName + '" to be a plain object.');
  35. }
  36. Object.keys(object)
  37. .forEach(function(key) {
  38. const validator = schema[key];
  39. if (!validator) {
  40. if (!allowUnknown) {
  41. throw new Error('"' + key + '" is not allowed in "' + parameterName + '"');
  42. }
  43. return;
  44. }
  45. if (!validator.isValid(object[key])) {
  46. throw new Error(validator.message);
  47. }
  48. });
  49. }
  50. function validateOptions(options) {
  51. return validate(sign_options_schema, false, options, 'options');
  52. }
  53. function validatePayload(payload) {
  54. return validate(registered_claims_schema, true, payload, 'payload');
  55. }
  56. const options_to_payload = {
  57. 'audience': 'aud',
  58. 'issuer': 'iss',
  59. 'subject': 'sub',
  60. 'jwtid': 'jti'
  61. };
  62. const options_for_objects = [
  63. 'expiresIn',
  64. 'notBefore',
  65. 'noTimestamp',
  66. 'audience',
  67. 'issuer',
  68. 'subject',
  69. 'jwtid',
  70. ];
  71. module.exports = function (payload, secretOrPrivateKey, options, callback) {
  72. if (typeof options === 'function') {
  73. callback = options;
  74. options = {};
  75. } else {
  76. options = options || {};
  77. }
  78. const isObjectPayload = typeof payload === 'object' &&
  79. !Buffer.isBuffer(payload);
  80. const header = Object.assign({
  81. alg: options.algorithm || 'HS256',
  82. typ: isObjectPayload ? 'JWT' : undefined,
  83. kid: options.keyid
  84. }, options.header);
  85. function failure(err) {
  86. if (callback) {
  87. return callback(err);
  88. }
  89. throw err;
  90. }
  91. if (!secretOrPrivateKey && options.algorithm !== 'none') {
  92. return failure(new Error('secretOrPrivateKey must have a value'));
  93. }
  94. if (secretOrPrivateKey != null && !(secretOrPrivateKey instanceof KeyObject)) {
  95. try {
  96. secretOrPrivateKey = createPrivateKey(secretOrPrivateKey)
  97. } catch (_) {
  98. try {
  99. secretOrPrivateKey = createSecretKey(typeof secretOrPrivateKey === 'string' ? Buffer.from(secretOrPrivateKey) : secretOrPrivateKey)
  100. } catch (_) {
  101. return failure(new Error('secretOrPrivateKey is not valid key material'));
  102. }
  103. }
  104. }
  105. if (header.alg.startsWith('HS') && secretOrPrivateKey.type !== 'secret') {
  106. return failure(new Error((`secretOrPrivateKey must be a symmetric key when using ${header.alg}`)))
  107. } else if (/^(?:RS|PS|ES)/.test(header.alg)) {
  108. if (secretOrPrivateKey.type !== 'private') {
  109. return failure(new Error((`secretOrPrivateKey must be an asymmetric key when using ${header.alg}`)))
  110. }
  111. if (!options.allowInsecureKeySizes &&
  112. !header.alg.startsWith('ES') &&
  113. secretOrPrivateKey.asymmetricKeyDetails !== undefined && //KeyObject.asymmetricKeyDetails is supported in Node 15+
  114. secretOrPrivateKey.asymmetricKeyDetails.modulusLength < 2048) {
  115. return failure(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`));
  116. }
  117. }
  118. if (typeof payload === 'undefined') {
  119. return failure(new Error('payload is required'));
  120. } else if (isObjectPayload) {
  121. try {
  122. validatePayload(payload);
  123. }
  124. catch (error) {
  125. return failure(error);
  126. }
  127. if (!options.mutatePayload) {
  128. payload = Object.assign({},payload);
  129. }
  130. } else {
  131. const invalid_options = options_for_objects.filter(function (opt) {
  132. return typeof options[opt] !== 'undefined';
  133. });
  134. if (invalid_options.length > 0) {
  135. return failure(new Error('invalid ' + invalid_options.join(',') + ' option for ' + (typeof payload ) + ' payload'));
  136. }
  137. }
  138. if (typeof payload.exp !== 'undefined' && typeof options.expiresIn !== 'undefined') {
  139. return failure(new Error('Bad "options.expiresIn" option the payload already has an "exp" property.'));
  140. }
  141. if (typeof payload.nbf !== 'undefined' && typeof options.notBefore !== 'undefined') {
  142. return failure(new Error('Bad "options.notBefore" option the payload already has an "nbf" property.'));
  143. }
  144. try {
  145. validateOptions(options);
  146. }
  147. catch (error) {
  148. return failure(error);
  149. }
  150. if (!options.allowInvalidAsymmetricKeyTypes) {
  151. try {
  152. validateAsymmetricKey(header.alg, secretOrPrivateKey);
  153. } catch (error) {
  154. return failure(error);
  155. }
  156. }
  157. const timestamp = payload.iat || Math.floor(Date.now() / 1000);
  158. if (options.noTimestamp) {
  159. delete payload.iat;
  160. } else if (isObjectPayload) {
  161. payload.iat = timestamp;
  162. }
  163. if (typeof options.notBefore !== 'undefined') {
  164. try {
  165. payload.nbf = timespan(options.notBefore, timestamp);
  166. }
  167. catch (err) {
  168. return failure(err);
  169. }
  170. if (typeof payload.nbf === 'undefined') {
  171. return failure(new Error('"notBefore" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
  172. }
  173. }
  174. if (typeof options.expiresIn !== 'undefined' && typeof payload === 'object') {
  175. try {
  176. payload.exp = timespan(options.expiresIn, timestamp);
  177. }
  178. catch (err) {
  179. return failure(err);
  180. }
  181. if (typeof payload.exp === 'undefined') {
  182. return failure(new Error('"expiresIn" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
  183. }
  184. }
  185. Object.keys(options_to_payload).forEach(function (key) {
  186. const claim = options_to_payload[key];
  187. if (typeof options[key] !== 'undefined') {
  188. if (typeof payload[claim] !== 'undefined') {
  189. return failure(new Error('Bad "options.' + key + '" option. The payload already has an "' + claim + '" property.'));
  190. }
  191. payload[claim] = options[key];
  192. }
  193. });
  194. const encoding = options.encoding || 'utf8';
  195. if (typeof callback === 'function') {
  196. callback = callback && once(callback);
  197. jws.createSign({
  198. header: header,
  199. privateKey: secretOrPrivateKey,
  200. payload: payload,
  201. encoding: encoding
  202. }).once('error', callback)
  203. .once('done', function (signature) {
  204. // TODO: Remove in favor of the modulus length check before signing once node 15+ is the minimum supported version
  205. if(!options.allowInsecureKeySizes && /^(?:RS|PS)/.test(header.alg) && signature.length < 256) {
  206. return callback(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`))
  207. }
  208. callback(null, signature);
  209. });
  210. } else {
  211. let signature = jws.sign({header: header, payload: payload, secret: secretOrPrivateKey, encoding: encoding});
  212. // TODO: Remove in favor of the modulus length check before signing once node 15+ is the minimum supported version
  213. if(!options.allowInsecureKeySizes && /^(?:RS|PS)/.test(header.alg) && signature.length < 256) {
  214. throw new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`)
  215. }
  216. return signature
  217. }
  218. };