@axa-fr/oidc-client 7.26.8 → 7.27.1

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,7 +1,19 @@
1
- export const initSession = (configurationName, storage = sessionStorage) => {
1
+ export const initSession = (
2
+ configurationName,
3
+ storage = sessionStorage,
4
+ loginStateStorage?: Storage,
5
+ ) => {
6
+ const loginStorage = loginStateStorage ?? storage;
7
+
2
8
  const clearAsync = status => {
3
9
  storage[`oidc.${configurationName}`] = JSON.stringify({ tokens: null, status });
4
10
  delete storage[`oidc.${configurationName}.userInfo`];
11
+ if (loginStateStorage && loginStateStorage !== storage) {
12
+ delete loginStorage[`oidc.login.${configurationName}`];
13
+ delete loginStorage[`oidc.state.${configurationName}`];
14
+ delete loginStorage[`oidc.code_verifier.${configurationName}`];
15
+ delete loginStorage[`oidc.nonce.${configurationName}`];
16
+ }
5
17
  return Promise.resolve();
6
18
  };
7
19
 
@@ -27,7 +39,7 @@ export const initSession = (configurationName, storage = sessionStorage) => {
27
39
  };
28
40
 
29
41
  const setNonceAsync = nonce => {
30
- storage[`oidc.nonce.${configurationName}`] = nonce.nonce;
42
+ loginStorage[`oidc.nonce.${configurationName}`] = nonce.nonce;
31
43
  };
32
44
 
33
45
  const setDemonstratingProofOfPossessionJwkAsync = (jwk: JsonWebKey) => {
@@ -40,7 +52,7 @@ export const initSession = (configurationName, storage = sessionStorage) => {
40
52
 
41
53
  const getNonceAsync = async () => {
42
54
  // @ts-ignore
43
- return { nonce: storage[`oidc.nonce.${configurationName}`] };
55
+ return { nonce: loginStorage[`oidc.nonce.${configurationName}`] };
44
56
  };
45
57
 
46
58
  const setDemonstratingProofOfPossessionNonce = async (dpopNonce: string) => {
@@ -61,10 +73,10 @@ export const initSession = (configurationName, storage = sessionStorage) => {
61
73
  const getLoginParamsCache = {};
62
74
  const setLoginParams = data => {
63
75
  getLoginParamsCache[configurationName] = data;
64
- storage[`oidc.login.${configurationName}`] = JSON.stringify(data);
76
+ loginStorage[`oidc.login.${configurationName}`] = JSON.stringify(data);
65
77
  };
66
78
  const getLoginParams = () => {
67
- const dataString = storage[`oidc.login.${configurationName}`];
79
+ const dataString = loginStorage[`oidc.login.${configurationName}`];
68
80
 
69
81
  if (!dataString) {
70
82
  console.warn(
@@ -80,19 +92,19 @@ export const initSession = (configurationName, storage = sessionStorage) => {
80
92
  };
81
93
 
82
94
  const getStateAsync = async () => {
83
- return storage[`oidc.state.${configurationName}`];
95
+ return loginStorage[`oidc.state.${configurationName}`];
84
96
  };
85
97
 
86
98
  const setStateAsync = async (state: string) => {
87
- storage[`oidc.state.${configurationName}`] = state;
99
+ loginStorage[`oidc.state.${configurationName}`] = state;
88
100
  };
89
101
 
90
102
  const getCodeVerifierAsync = async () => {
91
- return storage[`oidc.code_verifier.${configurationName}`];
103
+ return loginStorage[`oidc.code_verifier.${configurationName}`];
92
104
  };
93
105
 
94
106
  const setCodeVerifierAsync = async codeVerifier => {
95
- storage[`oidc.code_verifier.${configurationName}`] = codeVerifier;
107
+ loginStorage[`oidc.code_verifier.${configurationName}`] = codeVerifier;
96
108
  };
97
109
 
98
110
  return {
package/src/initWorker.ts CHANGED
@@ -151,6 +151,46 @@ const waitForControllerAsync = async (timeoutMs: number) => {
151
151
  });
152
152
  };
153
153
 
154
+ // Module-level guards to prevent:
155
+ // - registering multiple controllerchange listeners (one per initWorkerAsync call)
156
+ // - reloading more than once per page lifetime
157
+ let controllerChangeListenerRegistered = false;
158
+ let controllerChangeReloading = false;
159
+
160
+ // Session-level guard to prevent infinite reload loops caused by SW update cycles.
161
+ // The controllerchange listener triggers a page reload, but after reload the module-level
162
+ // guards above are reset. If the SW still hasn't been updated correctly (e.g. stale cache,
163
+ // Firefox issues), the cycle would repeat forever. This key tracks reloads across page loads
164
+ // via sessionStorage so we can break the loop.
165
+ const SW_RELOAD_SESSION_KEY = 'oidc.sw.controllerchange_reload_count';
166
+ const SW_RELOAD_MAX = 3;
167
+
168
+ const getControllerChangeReloadCount = (): number => {
169
+ try {
170
+ return parseInt(sessionStorage.getItem(SW_RELOAD_SESSION_KEY) ?? '0', 10);
171
+ } catch {
172
+ return 0;
173
+ }
174
+ };
175
+
176
+ const incrementControllerChangeReloadCount = (): number => {
177
+ const count = getControllerChangeReloadCount() + 1;
178
+ try {
179
+ sessionStorage.setItem(SW_RELOAD_SESSION_KEY, String(count));
180
+ } catch {
181
+ // ignore
182
+ }
183
+ return count;
184
+ };
185
+
186
+ const clearControllerChangeReloadCount = () => {
187
+ try {
188
+ sessionStorage.removeItem(SW_RELOAD_SESSION_KEY);
189
+ } catch {
190
+ // ignore
191
+ }
192
+ };
193
+
154
194
  export const initWorkerAsync = async (
155
195
  configuration: OidcConfiguration,
156
196
  configurationName: string,
@@ -183,25 +223,82 @@ export const initWorkerAsync = async (
183
223
 
184
224
  const versionMismatchKey = `oidc.sw.version_mismatch_reload.${configurationName}`;
185
225
 
186
- const sendSkipWaiting = async () => {
226
+ const sendSkipWaitingToWorker = async (targetSw: ServiceWorker) => {
187
227
  stopKeepAlive();
188
228
  console.log('New SW waiting – SKIP_WAITING');
189
229
  try {
190
- await sendMessageAsync(registration, { timeoutMs: 8000 })({
191
- type: 'SKIP_WAITING',
192
- configurationName,
193
- data: null,
230
+ await new Promise<void>((resolve, reject) => {
231
+ const messageChannel = new MessageChannel();
232
+ let timeoutId: any = null;
233
+
234
+ const cleanup = () => {
235
+ try {
236
+ if (timeoutId != null) {
237
+ timer.clearTimeout(timeoutId);
238
+ timeoutId = null;
239
+ }
240
+ messageChannel.port1.onmessage = null;
241
+ messageChannel.port1.close();
242
+ messageChannel.port2.close();
243
+ } catch (ex) {
244
+ console.error(ex);
245
+ }
246
+ };
247
+
248
+ timeoutId = timer.setTimeout(() => {
249
+ cleanup();
250
+ reject(new Error('SKIP_WAITING did not respond within 8000ms'));
251
+ }, 8000);
252
+
253
+ messageChannel.port1.onmessage = event => {
254
+ cleanup();
255
+ if (event?.data?.error) reject(event.data.error);
256
+ else resolve();
257
+ };
258
+
259
+ try {
260
+ targetSw.postMessage(
261
+ {
262
+ type: 'SKIP_WAITING',
263
+ configurationName,
264
+ data: null,
265
+ tabId: getTabId(configurationName ?? 'default'),
266
+ },
267
+ [messageChannel.port2],
268
+ );
269
+ } catch (err) {
270
+ cleanup();
271
+ reject(err);
272
+ }
194
273
  });
195
274
  } catch (e) {
196
275
  console.warn('SKIP_WAITING failed', e);
197
276
  }
198
277
  };
199
278
 
279
+ const sendSkipWaiting = async () => {
280
+ const waitingSw = registration.waiting;
281
+ if (waitingSw) {
282
+ await sendSkipWaitingToWorker(waitingSw);
283
+ } else {
284
+ console.warn('sendSkipWaiting called but no waiting service worker found');
285
+ }
286
+ };
287
+
200
288
  const trackInstallingWorker = (newSW: ServiceWorker) => {
201
289
  stopKeepAlive();
202
290
  newSW.addEventListener('statechange', async () => {
203
291
  if (newSW.state === 'installed' && navigator.serviceWorker.controller) {
204
- await sendSkipWaiting();
292
+ // Guard against infinite SKIP_WAITING → controllerchange → reload loops.
293
+ // If we've already exhausted the reload budget, don't force activation – let the
294
+ // browser handle it naturally on the next navigation instead.
295
+ if (getControllerChangeReloadCount() >= SW_RELOAD_MAX) {
296
+ console.warn(
297
+ 'SW trackInstallingWorker: skipping SKIP_WAITING because the reload budget is exhausted',
298
+ );
299
+ return;
300
+ }
301
+ await sendSkipWaitingToWorker(newSW);
205
302
  }
206
303
  });
207
304
  };
@@ -219,33 +316,25 @@ export const initWorkerAsync = async (
219
316
  if (registration.installing) {
220
317
  trackInstallingWorker(registration.installing);
221
318
  } else if (registration.waiting && navigator.serviceWorker.controller) {
222
- // A new SW is already waiting – activate it straight away
223
- sendSkipWaiting();
319
+ // A new SW is already waiting – activate it straight away (unless reload budget exhausted)
320
+ if (getControllerChangeReloadCount() < SW_RELOAD_MAX) {
321
+ sendSkipWaiting();
322
+ } else {
323
+ console.warn(
324
+ 'SW: a waiting worker exists but reload budget is exhausted – skipping activation',
325
+ );
326
+ }
224
327
  }
225
328
 
226
- // (Optional but useful on Safari) ask for update early
227
- try {
228
- await registration.update();
229
- } catch (ex) {
329
+ // (Optional but useful on Safari) ask for update early – non-blocking to avoid slowing init
330
+ registration.update().catch(ex => {
230
331
  console.error(ex);
231
- }
232
-
233
- // 2) Quand le SW actif change, on reload (once per session)
234
- const reloadKey = `oidc.sw.controllerchange.reloaded.${configurationName}`;
235
- navigator.serviceWorker.addEventListener('controllerchange', () => {
236
- try {
237
- if (sessionStorage.getItem(reloadKey) === '1') return;
238
- sessionStorage.setItem(reloadKey, '1');
239
- } catch {
240
- // ignore
241
- }
242
-
243
- console.log('SW controller changed – reloading page');
244
- stopKeepAlive();
245
- window.location.reload();
246
332
  });
247
333
 
248
- // 3) Claim + init classique (Safari-safe)
334
+ // 2) Claim + init classique (Safari-safe)
335
+ // IMPORTANT: claim() is done BEFORE registering the controllerchange listener,
336
+ // because claim() can trigger a controllerchange event on first visit and we don't
337
+ // want that initial claim to cause a reload loop.
249
338
  try {
250
339
  await navigator.serviceWorker.ready;
251
340
 
@@ -264,6 +353,37 @@ export const initWorkerAsync = async (
264
353
  return null;
265
354
  }
266
355
 
356
+ // 3) Register the controllerchange listener AFTER claim, and only once per page lifetime.
357
+ // This prevents:
358
+ // - claim() from triggering a reload on first visit
359
+ // - multiple listeners being stacked (initWorkerAsync is called many times)
360
+ // - more than one reload per page lifetime (guard via controllerChangeReloading)
361
+ // - infinite loops across page reloads (guard via sessionStorage counter)
362
+ if (!controllerChangeListenerRegistered) {
363
+ controllerChangeListenerRegistered = true;
364
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
365
+ if (controllerChangeReloading) {
366
+ return;
367
+ }
368
+
369
+ // Session-level guard: prevent infinite reload loops when the SW never converges
370
+ // to the expected version (e.g. stale cache, Firefox issues, Electron quirks).
371
+ const reloadCount = incrementControllerChangeReloadCount();
372
+ if (reloadCount > SW_RELOAD_MAX) {
373
+ console.warn(
374
+ `SW controllerchange: reload budget exhausted (${reloadCount - 1} reloads). ` +
375
+ 'Skipping reload to avoid infinite loop.',
376
+ );
377
+ return;
378
+ }
379
+
380
+ controllerChangeReloading = true;
381
+ console.log('SW controller changed – reloading page');
382
+ stopKeepAlive();
383
+ window.location.reload();
384
+ });
385
+ }
386
+
267
387
  const clearAsync = async status => {
268
388
  return sendMessageAsync(registration)({ type: 'clear', data: { status }, configurationName });
269
389
  };
@@ -297,9 +417,18 @@ export const initWorkerAsync = async (
297
417
  const reloadCount = parseInt(sessionStorage.getItem(versionMismatchKey) ?? '0', 10);
298
418
  if (reloadCount < 3) {
299
419
  sessionStorage.setItem(versionMismatchKey, String(reloadCount + 1));
300
- // If a new SW is already waiting, skip it into activation so controllerchange triggers reload
420
+
301
421
  if (registration.waiting) {
422
+ // A new SW is already waiting – activate it; controllerchange will trigger reload
302
423
  await sendSkipWaiting();
424
+ // If controllerchange did not reload yet, wait a moment then force reload
425
+ await sleepAsync({ milliseconds: 500 });
426
+ if (!controllerChangeReloading) {
427
+ controllerChangeReloading = true;
428
+ window.location.reload();
429
+ }
430
+ // Return a never-resolving promise to avoid returning stale tokens
431
+ return new Promise<never>(() => {});
303
432
  } else {
304
433
  // No waiting SW – force a fresh update and reload
305
434
  stopKeepAlive();
@@ -310,18 +439,24 @@ export const initWorkerAsync = async (
310
439
  }
311
440
  const isSuccess = await registration.unregister();
312
441
  console.log(`Service worker unregistering ${isSuccess}`);
313
- await sleepAsync({ milliseconds: 2000 });
314
- window.location.reload();
442
+ await sleepAsync({ milliseconds: 500 });
443
+ if (!controllerChangeReloading) {
444
+ controllerChangeReloading = true;
445
+ window.location.reload();
446
+ }
447
+ return new Promise<never>(() => {});
315
448
  }
316
449
  } else {
450
+ // Max retries reached – do NOT clear the key so future initAsync calls
451
+ // won't restart the cycle of 3 reloads
317
452
  console.error(
318
453
  `Service worker version mismatch persists after ${reloadCount} attempt(s). Continuing with mismatched version.`,
319
454
  );
320
- sessionStorage.removeItem(versionMismatchKey);
321
455
  }
322
456
  } else {
323
- // Version matches – clear any leftover mismatch counter
457
+ // Version matches – clear any leftover mismatch counter and reload counter
324
458
  sessionStorage.removeItem(versionMismatchKey);
459
+ clearControllerChangeReloadCount();
325
460
  }
326
461
 
327
462
  // @ts-ignore
@@ -1,4 +1,4 @@
1
- import { eventNames } from './events';
1
+ import { eventNames } from './events';
2
2
  import { initSession } from './initSession';
3
3
  import { initWorkerAsync } from './initWorker';
4
4
  import Oidc from './oidc';
@@ -62,7 +62,11 @@ export const tryKeepSessionAsync = async (oidc: Oidc) => {
62
62
  message: 'service worker is not supported by this browser',
63
63
  });
64
64
  }
65
- const session = initSession(oidc.configurationName, configuration.storage ?? sessionStorage);
65
+ const session = initSession(
66
+ oidc.configurationName,
67
+ configuration.storage ?? sessionStorage,
68
+ configuration.login_state_storage ?? configuration.storage ?? sessionStorage,
69
+ );
66
70
  const { tokens } = await session.initAsync();
67
71
  if (tokens) {
68
72
  // @ts-ignore
package/src/login.ts CHANGED
@@ -69,7 +69,11 @@ export const defaultLoginAsync =
69
69
  serviceWorker.startKeepAliveServiceWorker();
70
70
  storage = serviceWorker;
71
71
  } else {
72
- const session = initSession(configurationName, configuration.storage ?? sessionStorage);
72
+ const session = initSession(
73
+ configurationName,
74
+ configuration.storage ?? sessionStorage,
75
+ configuration.login_state_storage ?? configuration.storage ?? sessionStorage,
76
+ );
73
77
  session.setLoginParams({ callbackPath: url, extras: originExtras, scope: scope });
74
78
  await session.setNonceAsync(nonce);
75
79
  storage = session;
@@ -131,6 +135,7 @@ export const loginCallbackAsync =
131
135
  const session = initSession(
132
136
  oidc.configurationName,
133
137
  configuration.storage ?? sessionStorage,
138
+ configuration.login_state_storage ?? configuration.storage ?? sessionStorage,
134
139
  );
135
140
  await session.setSessionStateAsync(sessionState);
136
141
  nonceData = await session.getNonceAsync();
@@ -186,7 +191,11 @@ export const loginCallbackAsync =
186
191
  const jwk = await generateJwkAsync(window)(
187
192
  configuration.demonstrating_proof_of_possession_configuration.generateKeyAlgorithm,
188
193
  );
189
- const session = initSession(oidc.configurationName, configuration.storage);
194
+ const session = initSession(
195
+ oidc.configurationName,
196
+ configuration.storage,
197
+ configuration.login_state_storage ?? configuration.storage,
198
+ );
190
199
  await session.setDemonstratingProofOfPossessionJwkAsync(jwk);
191
200
  headersExtras['DPoP'] = await generateJwtDemonstratingProofOfPossessionAsync(window)(
192
201
  configuration.demonstrating_proof_of_possession_configuration,
@@ -251,7 +260,11 @@ export const loginCallbackAsync =
251
260
  );
252
261
  }
253
262
  } else {
254
- const session = initSession(oidc.configurationName, configuration.storage);
263
+ const session = initSession(
264
+ oidc.configurationName,
265
+ configuration.storage,
266
+ configuration.login_state_storage ?? configuration.storage,
267
+ );
255
268
  loginParams = session.getLoginParams();
256
269
  if (demonstratingProofOfPossessionNonce) {
257
270
  await session.setDemonstratingProofOfPossessionNonce(demonstratingProofOfPossessionNonce);
package/src/logout.ts CHANGED
@@ -46,7 +46,11 @@ export const destroyAsync = oidc => async status => {
46
46
  }
47
47
  const serviceWorker = await initWorkerAsync(oidc.configuration, oidc.configurationName);
48
48
  if (!serviceWorker) {
49
- const session = initSession(oidc.configurationName, oidc.configuration.storage);
49
+ const session = initSession(
50
+ oidc.configurationName,
51
+ oidc.configuration.storage,
52
+ oidc.configuration.login_state_storage ?? oidc.configuration.storage,
53
+ );
50
54
  await session.clearAsync(status);
51
55
  } else {
52
56
  await serviceWorker.clearAsync(status);
package/src/oidc.ts CHANGED
@@ -348,7 +348,11 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
348
348
  this.tokens = parsedTokens;
349
349
  const serviceWorker = await initWorkerAsync(this.configuration, this.configurationName);
350
350
  if (!serviceWorker) {
351
- const session = initSession(this.configurationName, this.configuration.storage);
351
+ const session = initSession(
352
+ this.configurationName,
353
+ this.configuration.storage,
354
+ this.configuration.login_state_storage ?? this.configuration.storage,
355
+ );
352
356
  session.setTokens(parsedTokens);
353
357
  }
354
358
  this.publishEvent(Oidc.eventNames.token_acquired, parsedTokens);
@@ -388,7 +392,11 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
388
392
  return `DPOP_SECURED_BY_OIDC_SERVICE_WORKER_${this.configurationName}#tabId=${getTabId(this.configurationName)}`;
389
393
  }
390
394
 
391
- const session = initSession(this.configurationName, configuration.storage);
395
+ const session = initSession(
396
+ this.configurationName,
397
+ configuration.storage,
398
+ configuration.login_state_storage ?? configuration.storage,
399
+ );
392
400
  const jwk = await session.getDemonstratingProofOfPossessionJwkAsync();
393
401
  const demonstratingProofOfPossessionNonce = session.getDemonstratingProofOfPossessionNonce();
394
402
 
@@ -28,7 +28,11 @@ async function syncTokens(
28
28
 
29
29
  const serviceWorker = await initWorkerAsync(oidc.configuration, oidc.configurationName);
30
30
  if (!serviceWorker) {
31
- const session = initSession(oidc.configurationName, oidc.configuration.storage);
31
+ const session = initSession(
32
+ oidc.configurationName,
33
+ oidc.configuration.storage,
34
+ oidc.configuration.login_state_storage ?? oidc.configuration.storage,
35
+ );
32
36
  session.setTokens(oidc.tokens);
33
37
  }
34
38
 
@@ -169,7 +173,11 @@ export const syncTokensInfoAsync =
169
173
  }
170
174
  nonce = await serviceWorker.getNonceAsync();
171
175
  } else {
172
- const session = initSession(configurationName, configuration.storage ?? sessionStorage);
176
+ const session = initSession(
177
+ configurationName,
178
+ configuration.storage ?? sessionStorage,
179
+ configuration.login_state_storage ?? configuration.storage ?? sessionStorage,
180
+ );
173
181
  const initAsyncResponse = await session.initAsync();
174
182
  let { tokens } = initAsyncResponse;
175
183
  const { status } = initAsyncResponse;
@@ -263,7 +271,11 @@ const synchroniseTokensAsync =
263
271
  if (serviceWorker) {
264
272
  loginParams = serviceWorker.getLoginParams();
265
273
  } else {
266
- const session = initSession(oidc.configurationName, configuration.storage);
274
+ const session = initSession(
275
+ oidc.configurationName,
276
+ configuration.storage,
277
+ configuration.login_state_storage ?? configuration.storage,
278
+ );
267
279
  loginParams = session.getLoginParams();
268
280
  }
269
281
  const silentLoginInput = {};
@@ -454,7 +466,11 @@ const synchroniseTokensAsync =
454
466
  tokenResponse.demonstratingProofOfPossessionNonce,
455
467
  );
456
468
  } else {
457
- const session = initSession(oidc.configurationName, configuration.storage);
469
+ const session = initSession(
470
+ oidc.configurationName,
471
+ configuration.storage,
472
+ configuration.login_state_storage ?? configuration.storage,
473
+ );
458
474
  await session.setDemonstratingProofOfPossessionNonce(
459
475
  tokenResponse.demonstratingProofOfPossessionNonce,
460
476
  );
package/src/types.ts CHANGED
@@ -39,6 +39,7 @@ export type OidcConfiguration = {
39
39
  extras?: StringMap;
40
40
  token_request_extras?: StringMap;
41
41
  storage?: Storage;
42
+ login_state_storage?: Storage;
42
43
  monitor_session?: boolean;
43
44
  token_renew_mode?: string;
44
45
  logout_tokens_to_invalidate?: Array<LogoutToken>;
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export default '7.26.8';
1
+ export default '7.27.1';