local.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import { importJWK } from '../key/import.js';
  2. import { JWKSInvalid, JOSENotSupported, JWKSNoMatchingKey, JWKSMultipleMatchingKeys, } from '../util/errors.js';
  3. import isObject from '../lib/is_object.js';
  4. function getKtyFromAlg(alg) {
  5. switch (typeof alg === 'string' && alg.slice(0, 2)) {
  6. case 'RS':
  7. case 'PS':
  8. return 'RSA';
  9. case 'ES':
  10. return 'EC';
  11. case 'Ed':
  12. return 'OKP';
  13. default:
  14. throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set');
  15. }
  16. }
  17. export function isJWKSLike(jwks) {
  18. return (jwks &&
  19. typeof jwks === 'object' &&
  20. Array.isArray(jwks.keys) &&
  21. jwks.keys.every(isJWKLike));
  22. }
  23. function isJWKLike(key) {
  24. return isObject(key);
  25. }
  26. function clone(obj) {
  27. if (typeof structuredClone === 'function') {
  28. return structuredClone(obj);
  29. }
  30. return JSON.parse(JSON.stringify(obj));
  31. }
  32. export class LocalJWKSet {
  33. constructor(jwks) {
  34. this._cached = new WeakMap();
  35. if (!isJWKSLike(jwks)) {
  36. throw new JWKSInvalid('JSON Web Key Set malformed');
  37. }
  38. this._jwks = clone(jwks);
  39. }
  40. async getKey(protectedHeader, token) {
  41. const { alg, kid } = { ...protectedHeader, ...token === null || token === void 0 ? void 0 : token.header };
  42. const kty = getKtyFromAlg(alg);
  43. const candidates = this._jwks.keys.filter((jwk) => {
  44. let candidate = kty === jwk.kty;
  45. if (candidate && typeof kid === 'string') {
  46. candidate = kid === jwk.kid;
  47. }
  48. if (candidate && typeof jwk.alg === 'string') {
  49. candidate = alg === jwk.alg;
  50. }
  51. if (candidate && typeof jwk.use === 'string') {
  52. candidate = jwk.use === 'sig';
  53. }
  54. if (candidate && Array.isArray(jwk.key_ops)) {
  55. candidate = jwk.key_ops.includes('verify');
  56. }
  57. if (candidate && alg === 'EdDSA') {
  58. candidate = jwk.crv === 'Ed25519' || jwk.crv === 'Ed448';
  59. }
  60. if (candidate) {
  61. switch (alg) {
  62. case 'ES256':
  63. candidate = jwk.crv === 'P-256';
  64. break;
  65. case 'ES256K':
  66. candidate = jwk.crv === 'secp256k1';
  67. break;
  68. case 'ES384':
  69. candidate = jwk.crv === 'P-384';
  70. break;
  71. case 'ES512':
  72. candidate = jwk.crv === 'P-521';
  73. break;
  74. }
  75. }
  76. return candidate;
  77. });
  78. const { 0: jwk, length } = candidates;
  79. if (length === 0) {
  80. throw new JWKSNoMatchingKey();
  81. }
  82. else if (length !== 1) {
  83. const error = new JWKSMultipleMatchingKeys();
  84. const { _cached } = this;
  85. error[Symbol.asyncIterator] = async function* () {
  86. for (const jwk of candidates) {
  87. try {
  88. yield await importWithAlgCache(_cached, jwk, alg);
  89. }
  90. catch {
  91. continue;
  92. }
  93. }
  94. };
  95. throw error;
  96. }
  97. return importWithAlgCache(this._cached, jwk, alg);
  98. }
  99. }
  100. async function importWithAlgCache(cache, jwk, alg) {
  101. const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk);
  102. if (cached[alg] === undefined) {
  103. const key = await importJWK({ ...jwk, ext: true }, alg);
  104. if (key instanceof Uint8Array || key.type !== 'public') {
  105. throw new JWKSInvalid('JSON Web Key Set members must be public keys');
  106. }
  107. cached[alg] = key;
  108. }
  109. return cached[alg];
  110. }
  111. export function createLocalJWKSet(jwks) {
  112. const set = new LocalJWKSet(jwks);
  113. return async function (protectedHeader, token) {
  114. return set.getKey(protectedHeader, token);
  115. };
  116. }