jwt_claims_set.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from '../util/errors.js';
  2. import { decoder } from './buffer_utils.js';
  3. import epoch from './epoch.js';
  4. import secs from './secs.js';
  5. import isObject from './is_object.js';
  6. const normalizeTyp = (value) => value.toLowerCase().replace(/^application\//, '');
  7. const checkAudiencePresence = (audPayload, audOption) => {
  8. if (typeof audPayload === 'string') {
  9. return audOption.includes(audPayload);
  10. }
  11. if (Array.isArray(audPayload)) {
  12. return audOption.some(Set.prototype.has.bind(new Set(audPayload)));
  13. }
  14. return false;
  15. };
  16. export default (protectedHeader, encodedPayload, options = {}) => {
  17. const { typ } = options;
  18. if (typ &&
  19. (typeof protectedHeader.typ !== 'string' ||
  20. normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) {
  21. throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', 'typ', 'check_failed');
  22. }
  23. let payload;
  24. try {
  25. payload = JSON.parse(decoder.decode(encodedPayload));
  26. }
  27. catch (_a) {
  28. }
  29. if (!isObject(payload)) {
  30. throw new JWTInvalid('JWT Claims Set must be a top-level JSON object');
  31. }
  32. const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options;
  33. if (maxTokenAge !== undefined)
  34. requiredClaims.push('iat');
  35. if (audience !== undefined)
  36. requiredClaims.push('aud');
  37. if (subject !== undefined)
  38. requiredClaims.push('sub');
  39. if (issuer !== undefined)
  40. requiredClaims.push('iss');
  41. for (const claim of new Set(requiredClaims.reverse())) {
  42. if (!(claim in payload)) {
  43. throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, claim, 'missing');
  44. }
  45. }
  46. if (issuer && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) {
  47. throw new JWTClaimValidationFailed('unexpected "iss" claim value', 'iss', 'check_failed');
  48. }
  49. if (subject && payload.sub !== subject) {
  50. throw new JWTClaimValidationFailed('unexpected "sub" claim value', 'sub', 'check_failed');
  51. }
  52. if (audience &&
  53. !checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)) {
  54. throw new JWTClaimValidationFailed('unexpected "aud" claim value', 'aud', 'check_failed');
  55. }
  56. let tolerance;
  57. switch (typeof options.clockTolerance) {
  58. case 'string':
  59. tolerance = secs(options.clockTolerance);
  60. break;
  61. case 'number':
  62. tolerance = options.clockTolerance;
  63. break;
  64. case 'undefined':
  65. tolerance = 0;
  66. break;
  67. default:
  68. throw new TypeError('Invalid clockTolerance option type');
  69. }
  70. const { currentDate } = options;
  71. const now = epoch(currentDate || new Date());
  72. if ((payload.iat !== undefined || maxTokenAge) && typeof payload.iat !== 'number') {
  73. throw new JWTClaimValidationFailed('"iat" claim must be a number', 'iat', 'invalid');
  74. }
  75. if (payload.nbf !== undefined) {
  76. if (typeof payload.nbf !== 'number') {
  77. throw new JWTClaimValidationFailed('"nbf" claim must be a number', 'nbf', 'invalid');
  78. }
  79. if (payload.nbf > now + tolerance) {
  80. throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', 'nbf', 'check_failed');
  81. }
  82. }
  83. if (payload.exp !== undefined) {
  84. if (typeof payload.exp !== 'number') {
  85. throw new JWTClaimValidationFailed('"exp" claim must be a number', 'exp', 'invalid');
  86. }
  87. if (payload.exp <= now - tolerance) {
  88. throw new JWTExpired('"exp" claim timestamp check failed', 'exp', 'check_failed');
  89. }
  90. }
  91. if (maxTokenAge) {
  92. const age = now - payload.iat;
  93. const max = typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge);
  94. if (age - tolerance > max) {
  95. throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', 'iat', 'check_failed');
  96. }
  97. if (age < 0 - tolerance) {
  98. throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', 'iat', 'check_failed');
  99. }
  100. }
  101. return payload;
  102. };