@auth0/auth0-spa-js 2.18.2 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,14 @@
1
1
  import { MissingRefreshTokenError } from '../errors';
2
2
  import { FetchResponse } from '../global';
3
3
  import { createQueryParams, fromEntries } from '../utils';
4
- import { WorkerRefreshTokenMessage } from './worker.types';
4
+ import {
5
+ WorkerMessage,
6
+ WorkerRefreshTokenMessage,
7
+ WorkerRevokeTokenMessage
8
+ } from './worker.types';
5
9
 
6
10
  let refreshTokens: Record<string, string> = {};
11
+ let allowedBaseUrl: string | null = null;
7
12
 
8
13
  const cacheKey = (audience: string, scope: string) => `${audience}|${scope}`;
9
14
 
@@ -21,6 +26,24 @@ const setRefreshToken = (
21
26
  const deleteRefreshToken = (audience: string, scope: string) =>
22
27
  delete refreshTokens[cacheKey(audience, scope)];
23
28
 
29
+ const getRefreshTokensByAudience = (audience: string): string[] => {
30
+ const seen = new Set<string>();
31
+ Object.entries(refreshTokens).forEach(([key, token]) => {
32
+ if (cacheKeyContainsAudience(audience, key)) {
33
+ seen.add(token);
34
+ }
35
+ });
36
+ return Array.from(seen);
37
+ };
38
+
39
+ const deleteRefreshTokensByValue = (refreshToken: string): void => {
40
+ Object.entries(refreshTokens).forEach(([key, token]) => {
41
+ if (token === refreshToken) {
42
+ delete refreshTokens[key];
43
+ }
44
+ });
45
+ };
46
+
24
47
  const wait = (time: number) =>
25
48
  new Promise<void>(resolve => setTimeout(resolve, time));
26
49
 
@@ -180,11 +203,159 @@ const messageHandler = async ({
180
203
  }
181
204
  };
182
205
 
206
+ const revokeMessageHandler = async ({
207
+ data: { timeout, auth, fetchUrl, fetchOptions, useFormData },
208
+ ports: [port]
209
+ }: MessageEvent<WorkerRevokeTokenMessage>) => {
210
+ const { audience } = auth || {};
211
+
212
+ try {
213
+ const tokensToRevoke = getRefreshTokensByAudience(audience);
214
+
215
+ if (tokensToRevoke.length === 0) {
216
+ port.postMessage({ ok: true });
217
+ return;
218
+ }
219
+
220
+ // Parse the base body once; rebuild per RT so each request is independent.
221
+ const baseBody = useFormData
222
+ ? formDataToObject(fetchOptions.body as string)
223
+ : JSON.parse(fetchOptions.body as string);
224
+
225
+ for (const refreshToken of tokensToRevoke) {
226
+ const body = useFormData
227
+ ? createQueryParams({ ...baseBody, token: refreshToken })
228
+ : JSON.stringify({ ...baseBody, token: refreshToken });
229
+
230
+ let abortController: AbortController | undefined;
231
+ let signal: AbortSignal | undefined;
232
+
233
+ if (typeof AbortController === 'function') {
234
+ abortController = new AbortController();
235
+ signal = abortController.signal;
236
+ }
237
+
238
+ let timeoutId: ReturnType<typeof setTimeout>;
239
+ let response: void | Response;
240
+
241
+ try {
242
+ response = await Promise.race([
243
+ new Promise<void>(resolve => { timeoutId = setTimeout(resolve, timeout); }),
244
+ fetch(fetchUrl, { ...fetchOptions, body, signal })
245
+ ]).finally(() => clearTimeout(timeoutId));
246
+ } catch (error) {
247
+ port.postMessage({ error: error.message });
248
+ return;
249
+ }
250
+
251
+ if (!response) {
252
+ if (abortController) abortController.abort();
253
+ port.postMessage({ error: "Timeout when executing 'fetch'" });
254
+ return;
255
+ }
256
+
257
+ if (!response.ok) {
258
+ let errorDescription: string | undefined;
259
+ try {
260
+ const { error_description } = JSON.parse(await response.text());
261
+ errorDescription = error_description;
262
+ } catch {
263
+ // body absent or not valid JSON
264
+ }
265
+
266
+ port.postMessage({ error: errorDescription || `HTTP error ${response.status}` });
267
+ return;
268
+ }
269
+
270
+ deleteRefreshTokensByValue(refreshToken);
271
+ }
272
+
273
+ port.postMessage({ ok: true });
274
+ } catch (error) {
275
+ port.postMessage({
276
+ error: error.message || 'Unknown error during token revocation'
277
+ });
278
+ }
279
+ };
280
+
281
+ const isAuthorizedWorkerRequest = (
282
+ workerRequest: WorkerRefreshTokenMessage | WorkerRevokeTokenMessage,
283
+ expectedPath: string
284
+ ) => {
285
+ if (!allowedBaseUrl) {
286
+ return false;
287
+ }
288
+
289
+ try {
290
+ const allowedBaseOrigin = new URL(allowedBaseUrl).origin;
291
+ const requestedUrl = new URL(workerRequest.fetchUrl);
292
+
293
+ return (
294
+ requestedUrl.origin === allowedBaseOrigin &&
295
+ requestedUrl.pathname === expectedPath
296
+ );
297
+ } catch {
298
+ return false;
299
+ }
300
+ };
301
+
302
+ const messageRouter = (event: MessageEvent<WorkerMessage>) => {
303
+ const { data, ports } = event;
304
+ const [port] = ports;
305
+
306
+ if ('type' in data && data.type === 'init') {
307
+ if (allowedBaseUrl === null) {
308
+ try {
309
+ new URL(data.allowedBaseUrl);
310
+ allowedBaseUrl = data.allowedBaseUrl;
311
+ } catch {
312
+ return;
313
+ }
314
+ }
315
+
316
+ return;
317
+ }
318
+
319
+ if ('type' in data && data.type === 'revoke') {
320
+ if (!isAuthorizedWorkerRequest(data as WorkerRevokeTokenMessage, '/oauth/revoke')) {
321
+ port?.postMessage({
322
+ ok: false,
323
+ json: {
324
+ error: 'invalid_fetch_url',
325
+ error_description: 'Unauthorized fetch URL'
326
+ },
327
+ headers: {}
328
+ });
329
+ return;
330
+ }
331
+
332
+ revokeMessageHandler(event as MessageEvent<WorkerRevokeTokenMessage>);
333
+ return;
334
+ }
335
+
336
+ if (
337
+ !('fetchUrl' in data) ||
338
+ !isAuthorizedWorkerRequest(data as WorkerRefreshTokenMessage, '/oauth/token')
339
+ ) {
340
+ port?.postMessage({
341
+ ok: false,
342
+ json: {
343
+ error: 'invalid_fetch_url',
344
+ error_description: 'Unauthorized fetch URL'
345
+ },
346
+ headers: {}
347
+ });
348
+ return;
349
+ }
350
+
351
+ messageHandler(event as MessageEvent<WorkerRefreshTokenMessage>);
352
+ };
353
+
183
354
  // Don't run `addEventListener` in our tests (this is replaced in rollup)
184
355
  if (process.env.NODE_ENV === 'test') {
185
- module.exports = { messageHandler };
356
+ module.exports = { messageHandler, revokeMessageHandler, messageRouter };
186
357
  /* c8 ignore next 4 */
187
358
  } else {
188
359
  // @ts-ignore
189
- addEventListener('message', messageHandler);
360
+ addEventListener('message', messageRouter);
190
361
  }
@@ -1,16 +1,34 @@
1
1
  import { FetchOptions } from '../global';
2
2
 
3
- /**
4
- * @ts-ignore
5
- */
6
- export type WorkerRefreshTokenMessage = {
3
+ export type WorkerInitMessage = {
4
+ type: 'init';
5
+ allowedBaseUrl: string;
6
+ };
7
+
8
+ type WorkerTokenMessage = {
7
9
  timeout: number;
8
10
  fetchUrl: string;
9
11
  fetchOptions: FetchOptions;
10
12
  useFormData?: boolean;
11
- useMrrt?: boolean;
12
13
  auth: {
13
14
  audience: string;
14
15
  scope: string;
15
16
  };
16
17
  };
18
+
19
+ export type WorkerRefreshTokenMessage = WorkerTokenMessage & {
20
+ type: 'refresh';
21
+ useMrrt?: boolean;
22
+ };
23
+
24
+ export type WorkerRevokeTokenMessage = Omit<WorkerTokenMessage, 'auth'> & {
25
+ type: 'revoke';
26
+ auth: {
27
+ audience: string;
28
+ };
29
+ };
30
+
31
+ export type WorkerMessage =
32
+ | WorkerInitMessage
33
+ | WorkerRefreshTokenMessage
34
+ | WorkerRevokeTokenMessage;
@@ -1,16 +1,27 @@
1
- import { WorkerRefreshTokenMessage } from './worker.types';
1
+ import {
2
+ WorkerRefreshTokenMessage,
3
+ WorkerRevokeTokenMessage
4
+ } from './worker.types';
2
5
 
3
6
  /**
4
- * Sends the specified message to the web worker
5
- * @param message The message to send
6
- * @param to The worker to send the message to
7
+ * Sends a message to a Web Worker and returns a Promise that resolves with
8
+ * the worker's response, or rejects if the worker replies with an error.
9
+ *
10
+ * Uses a {@link MessageChannel} so each call gets its own private reply port,
11
+ * making concurrent calls safe without shared state.
12
+ *
13
+ * @param message - The typed message to send (`refresh` or `revoke`).
14
+ * @param to - The target {@link Worker} instance.
15
+ * @returns A Promise that resolves with the worker's response payload.
7
16
  */
8
- export const sendMessage = (message: WorkerRefreshTokenMessage, to: Worker) =>
9
- new Promise(function (resolve, reject) {
17
+ export const sendMessage = <T = any>(
18
+ message: WorkerRefreshTokenMessage | WorkerRevokeTokenMessage,
19
+ to: Worker
20
+ ): Promise<T> =>
21
+ new Promise<T>(function (resolve, reject) {
10
22
  const messageChannel = new MessageChannel();
11
23
 
12
24
  messageChannel.port1.onmessage = function (event) {
13
- // Only for fetch errors, as these get retried
14
25
  if (event.data.error) {
15
26
  reject(new Error(event.data.error));
16
27
  } else {