remote.js 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import fetchJwks from '../runtime/fetch_jwks.js';
  2. import { JWKSInvalid, JWKSNoMatchingKey } from '../util/errors.js';
  3. import { isJWKSLike, LocalJWKSet } from './local.js';
  4. function isCloudflareWorkers() {
  5. return (typeof WebSocketPair !== 'undefined' ||
  6. (typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers') ||
  7. (typeof EdgeRuntime !== 'undefined' && EdgeRuntime === 'vercel'));
  8. }
  9. class RemoteJWKSet extends LocalJWKSet {
  10. constructor(url, options) {
  11. super({ keys: [] });
  12. this._jwks = undefined;
  13. if (!(url instanceof URL)) {
  14. throw new TypeError('url must be an instance of URL');
  15. }
  16. this._url = new URL(url.href);
  17. this._options = { agent: options === null || options === void 0 ? void 0 : options.agent, headers: options === null || options === void 0 ? void 0 : options.headers };
  18. this._timeoutDuration =
  19. typeof (options === null || options === void 0 ? void 0 : options.timeoutDuration) === 'number' ? options === null || options === void 0 ? void 0 : options.timeoutDuration : 5000;
  20. this._cooldownDuration =
  21. typeof (options === null || options === void 0 ? void 0 : options.cooldownDuration) === 'number' ? options === null || options === void 0 ? void 0 : options.cooldownDuration : 30000;
  22. this._cacheMaxAge = typeof (options === null || options === void 0 ? void 0 : options.cacheMaxAge) === 'number' ? options === null || options === void 0 ? void 0 : options.cacheMaxAge : 600000;
  23. }
  24. coolingDown() {
  25. return typeof this._jwksTimestamp === 'number'
  26. ? Date.now() < this._jwksTimestamp + this._cooldownDuration
  27. : false;
  28. }
  29. fresh() {
  30. return typeof this._jwksTimestamp === 'number'
  31. ? Date.now() < this._jwksTimestamp + this._cacheMaxAge
  32. : false;
  33. }
  34. async getKey(protectedHeader, token) {
  35. if (!this._jwks || !this.fresh()) {
  36. await this.reload();
  37. }
  38. try {
  39. return await super.getKey(protectedHeader, token);
  40. }
  41. catch (err) {
  42. if (err instanceof JWKSNoMatchingKey) {
  43. if (this.coolingDown() === false) {
  44. await this.reload();
  45. return super.getKey(protectedHeader, token);
  46. }
  47. }
  48. throw err;
  49. }
  50. }
  51. async reload() {
  52. if (this._pendingFetch && isCloudflareWorkers()) {
  53. this._pendingFetch = undefined;
  54. }
  55. this._pendingFetch || (this._pendingFetch = fetchJwks(this._url, this._timeoutDuration, this._options)
  56. .then((json) => {
  57. if (!isJWKSLike(json)) {
  58. throw new JWKSInvalid('JSON Web Key Set malformed');
  59. }
  60. this._jwks = { keys: json.keys };
  61. this._jwksTimestamp = Date.now();
  62. this._pendingFetch = undefined;
  63. })
  64. .catch((err) => {
  65. this._pendingFetch = undefined;
  66. throw err;
  67. }));
  68. await this._pendingFetch;
  69. }
  70. }
  71. export function createRemoteJWKSet(url, options) {
  72. const set = new RemoteJWKSet(url, options);
  73. return async function (protectedHeader, token) {
  74. return set.getKey(protectedHeader, token);
  75. };
  76. }