sign.js 9.1 KB

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