oidc-spa 6.5.2 → 6.6.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.
Files changed (81) hide show
  1. package/mock/oidc.js +6 -5
  2. package/mock/oidc.js.map +1 -1
  3. package/oidc/AuthResponse.d.ts +6 -0
  4. package/oidc/AuthResponse.js +59 -0
  5. package/oidc/AuthResponse.js.map +1 -0
  6. package/oidc/Oidc.d.ts +19 -8
  7. package/oidc/createOidc.d.ts +4 -4
  8. package/oidc/createOidc.js +366 -298
  9. package/oidc/createOidc.js.map +1 -1
  10. package/oidc/evtIsUserActive.d.ts +15 -0
  11. package/oidc/{isUserActive.js → evtIsUserActive.js} +29 -12
  12. package/oidc/evtIsUserActive.js.map +1 -0
  13. package/oidc/handleOidcCallback.d.ts +8 -1
  14. package/oidc/handleOidcCallback.js +68 -13
  15. package/oidc/handleOidcCallback.js.map +1 -1
  16. package/oidc/loginOrGoToAuthServer.d.ts +5 -4
  17. package/oidc/loginOrGoToAuthServer.js +190 -227
  18. package/oidc/loginOrGoToAuthServer.js.map +1 -1
  19. package/oidc/loginPropagationToOtherTabs.d.ts +17 -0
  20. package/oidc/loginPropagationToOtherTabs.js +41 -0
  21. package/oidc/loginPropagationToOtherTabs.js.map +1 -0
  22. package/oidc/loginSilent.d.ts +1 -5
  23. package/oidc/loginSilent.js +3 -51
  24. package/oidc/loginSilent.js.map +1 -1
  25. package/oidc/logoutPropagationToOtherTabs.js +1 -1
  26. package/oidc/logoutPropagationToOtherTabs.js.map +1 -1
  27. package/oidc/oidcClientTsUserToTokens.d.ts +1 -1
  28. package/oidc/oidcClientTsUserToTokens.js +45 -23
  29. package/oidc/oidcClientTsUserToTokens.js.map +1 -1
  30. package/oidc/ongoingLoginOrRefreshProcesses.d.ts +16 -0
  31. package/oidc/ongoingLoginOrRefreshProcesses.js +102 -0
  32. package/oidc/ongoingLoginOrRefreshProcesses.js.map +1 -0
  33. package/oidc/persistedAuthState.d.ts +16 -3
  34. package/oidc/persistedAuthState.js +35 -4
  35. package/oidc/persistedAuthState.js.map +1 -1
  36. package/package.json +36 -21
  37. package/react/react.js +8 -14
  38. package/react/react.js.map +1 -1
  39. package/src/mock/oidc.ts +14 -3
  40. package/src/oidc/AuthResponse.ts +26 -0
  41. package/src/oidc/Oidc.ts +19 -4
  42. package/src/oidc/createOidc.ts +288 -251
  43. package/src/oidc/{isUserActive.ts → evtIsUserActive.ts} +36 -10
  44. package/src/oidc/handleOidcCallback.ts +73 -12
  45. package/src/oidc/loginOrGoToAuthServer.ts +94 -87
  46. package/src/oidc/loginPropagationToOtherTabs.ts +63 -0
  47. package/src/oidc/loginSilent.ts +2 -20
  48. package/src/oidc/logoutPropagationToOtherTabs.ts +2 -2
  49. package/src/oidc/oidcClientTsUserToTokens.ts +74 -35
  50. package/src/oidc/ongoingLoginOrRefreshProcesses.ts +60 -0
  51. package/src/oidc/persistedAuthState.ts +66 -8
  52. package/src/react/react.tsx +8 -16
  53. package/src/tools/{ephemeralSessionStorage.ts → EphemeralSessionStorage.ts} +59 -27
  54. package/src/tools/Evt.ts +56 -0
  55. package/src/tools/StatefulEvt.ts +38 -0
  56. package/src/tools/subscribeToUserInteraction.ts +0 -1
  57. package/src/tools/workerTimers.ts +10 -12
  58. package/tools/EphemeralSessionStorage.d.ts +12 -0
  59. package/tools/{ephemeralSessionStorage.js → EphemeralSessionStorage.js} +29 -16
  60. package/tools/EphemeralSessionStorage.js.map +1 -0
  61. package/tools/Evt.d.ts +11 -0
  62. package/tools/{AwaitableEventEmitter.js → Evt.js} +24 -8
  63. package/tools/Evt.js.map +1 -0
  64. package/tools/StatefulEvt.d.ts +12 -0
  65. package/tools/StatefulEvt.js +24 -0
  66. package/tools/StatefulEvt.js.map +1 -0
  67. package/tools/subscribeToUserInteraction.js +2 -3
  68. package/tools/subscribeToUserInteraction.js.map +1 -1
  69. package/tools/workerTimers.js +11 -13
  70. package/tools/workerTimers.js.map +1 -1
  71. package/oidc/isUserActive.d.ts +0 -13
  72. package/oidc/isUserActive.js.map +0 -1
  73. package/src/tools/AwaitableEventEmitter.ts +0 -33
  74. package/src/tools/StatefulObservable.ts +0 -52
  75. package/tools/AwaitableEventEmitter.d.ts +0 -5
  76. package/tools/AwaitableEventEmitter.js.map +0 -1
  77. package/tools/StatefulObservable.d.ts +0 -12
  78. package/tools/StatefulObservable.js +0 -33
  79. package/tools/StatefulObservable.js.map +0 -1
  80. package/tools/ephemeralSessionStorage.d.ts +0 -3
  81. package/tools/ephemeralSessionStorage.js.map +0 -1
@@ -4,13 +4,12 @@ import {
4
4
  type User as OidcClientTsUser,
5
5
  InMemoryWebStorage
6
6
  } from "../vendor/frontend/oidc-client-ts-and-jwt-decode";
7
- import { id, assert, is, type Equals, typeGuard } from "../vendor/frontend/tsafe";
7
+ import { id, assert, is, type Equals } from "../vendor/frontend/tsafe";
8
8
  import { setTimeout, clearTimeout } from "../tools/workerTimers";
9
9
  import { Deferred } from "../tools/Deferred";
10
10
  import { decodeJwt } from "../tools/decodeJwt";
11
- import { create$isUserActive } from "./isUserActive";
11
+ import { createEvtIsUserActive } from "./evtIsUserActive";
12
12
  import { createStartCountdown } from "../tools/startCountdown";
13
- import type { StatefulObservable } from "../tools/StatefulObservable";
14
13
  import { toHumanReadableDuration } from "../tools/toHumanReadableDuration";
15
14
  import { toFullyQualifiedUrl } from "../tools/toFullyQualifiedUrl";
16
15
  import {
@@ -19,23 +18,29 @@ import {
19
18
  createIframeTimeoutInitializationError,
20
19
  createWellKnownOidcConfigurationEndpointUnreachableInitializationError
21
20
  } from "./OidcInitializationError";
22
- import {
23
- getStateData,
24
- type StateData,
25
- generateStateQueryParamValue,
26
- STATE_STORE_KEY_PREFIX
27
- } from "./StateData";
21
+ import { type StateData, generateStateQueryParamValue, STATE_STORE_KEY_PREFIX } from "./StateData";
28
22
  import { notifyOtherTabsOfLogout, getPrOtherTabLogout } from "./logoutPropagationToOtherTabs";
23
+ import { notifyOtherTabsOfLogin, getPrOtherTabLogin } from "./loginPropagationToOtherTabs";
29
24
  import { getConfigId } from "./configId";
30
25
  import { oidcClientTsUserToTokens, getMsBeforeExpiration } from "./oidcClientTsUserToTokens";
31
- import { loginSilent, authResponseToUrl } from "./loginSilent";
32
- import { handleOidcCallback, AUTH_RESPONSE_KEY } from "./handleOidcCallback";
26
+ import { loginSilent } from "./loginSilent";
27
+ import { authResponseToUrl } from "./AuthResponse";
28
+ import { handleOidcCallback, retrieveRedirectAuthResponseAndStateData } from "./handleOidcCallback";
33
29
  import { getPersistedAuthState, persistAuthState } from "./persistedAuthState";
34
30
  import type { Oidc } from "./Oidc";
35
- import { type AwaitableEventEmitter, createAwaitableEventEmitter } from "../tools/AwaitableEventEmitter";
31
+ import { createEvt, type Evt } from "../tools/Evt";
36
32
  import { getHaveSharedParentDomain } from "../tools/haveSharedParentDomain";
37
- import { createLoginOrGoToAuthServer } from "./loginOrGoToAuthServer";
38
- import { createEphemeralSessionStorage } from "../tools/ephemeralSessionStorage";
33
+ import {
34
+ createLoginOrGoToAuthServer,
35
+ getPrSafelyRestoredFromBfCacheAfterLoginBackNavigation
36
+ } from "./loginOrGoToAuthServer";
37
+ import { createEphemeralSessionStorage } from "../tools/EphemeralSessionStorage";
38
+ import {
39
+ startLoginOrRefreshProcess,
40
+ waitForAllOtherOngoingLoginOrRefreshProcessesToComplete
41
+ } from "./ongoingLoginOrRefreshProcesses";
42
+
43
+ handleOidcCallback();
39
44
 
40
45
  // NOTE: Replaced at build time
41
46
  const VERSION = "{{OIDC_SPA_VERSION}}";
@@ -120,26 +125,22 @@ export type ParamsOfCreateOidc<
120
125
  __unsafe_useIdTokenAsAccessToken?: boolean;
121
126
  };
122
127
 
123
- handleOidcCallback();
124
-
125
128
  const GLOBAL_CONTEXT_KEY = "__oidc-spa.createOidc.globalContext";
126
129
 
127
130
  declare global {
128
131
  interface Window {
129
132
  [GLOBAL_CONTEXT_KEY]: {
130
133
  prOidcByConfigId: Map<string, Promise<Oidc<any>>>;
131
- evtAuthResponseHandled: AwaitableEventEmitter<void>;
132
- $isUserActive: StatefulObservable<boolean> | undefined;
133
134
  hasLogoutBeenCalled: boolean;
135
+ evtRequestToPersistTokens: Evt<{ configIdOfInstancePostingTheRequest: string }>;
134
136
  };
135
137
  }
136
138
  }
137
139
 
138
140
  window[GLOBAL_CONTEXT_KEY] ??= {
139
141
  prOidcByConfigId: new Map(),
140
- evtAuthResponseHandled: createAwaitableEventEmitter<void>(),
141
- $isUserActive: undefined,
142
- hasLogoutBeenCalled: false
142
+ hasLogoutBeenCalled: false,
143
+ evtRequestToPersistTokens: createEvt()
143
144
  };
144
145
 
145
146
  const globalContext = window[GLOBAL_CONTEXT_KEY];
@@ -321,7 +322,7 @@ export async function createOidc_nonMemoized<
321
322
  }
322
323
  }
323
324
 
324
- const isUserStorePersistent = !areThirdPartyCookiesAllowed;
325
+ let isUserStoreInMemoryOnly: boolean;
325
326
 
326
327
  const oidcClientTsUserManager = new OidcClientTsUserManager({
327
328
  stateQueryParamValue: stateQueryParamValue_instance,
@@ -334,31 +335,51 @@ export async function createOidc_nonMemoized<
334
335
  scope: Array.from(new Set(["openid", ...scopes])).join(" "),
335
336
  automaticSilentRenew: false,
336
337
  userStore: new WebStorageStateStore({
337
- store: areThirdPartyCookiesAllowed
338
- ? new InMemoryWebStorage()
339
- : createEphemeralSessionStorage({
340
- sessionStorageTtlMs: 3 * 60_1000
341
- })
338
+ store: (() => {
339
+ if (areThirdPartyCookiesAllowed) {
340
+ isUserStoreInMemoryOnly = true;
341
+ return new InMemoryWebStorage();
342
+ }
343
+
344
+ isUserStoreInMemoryOnly = false;
345
+
346
+ const storage = createEphemeralSessionStorage({
347
+ sessionStorageTtlMs: 3 * 60_000
348
+ });
349
+
350
+ const { evtRequestToPersistTokens } = globalContext;
351
+
352
+ evtRequestToPersistTokens.subscribe(({ configIdOfInstancePostingTheRequest }) => {
353
+ if (configIdOfInstancePostingTheRequest === configId) {
354
+ return;
355
+ }
356
+
357
+ storage.persistCurrentStateAndSubsequentChanges();
358
+ });
359
+
360
+ return storage;
361
+ })()
342
362
  }),
343
363
  stateStore: new WebStorageStateStore({ store: localStorage, prefix: STATE_STORE_KEY_PREFIX }),
344
364
  client_secret: __unsafe_clientSecret
345
365
  });
346
366
 
347
- const {
348
- loginOrGoToAuthServer,
349
- toCallBeforeReturningOidcLoggedIn,
350
- toCallBeforeReturningOidcNotLoggedIn
351
- } = createLoginOrGoToAuthServer({
367
+ const evtIsUserLoggedIn = createEvt<boolean>();
368
+
369
+ const { loginOrGoToAuthServer } = createLoginOrGoToAuthServer({
352
370
  configId,
353
371
  oidcClientTsUserManager,
354
372
  getExtraQueryParams,
355
373
  transformUrlBeforeRedirect,
356
374
  homeAndCallbackUrl,
375
+ evtIsUserLoggedIn,
357
376
  log
358
377
  });
359
378
 
360
379
  const BROWSER_SESSION_NOT_FIRST_INIT_KEY = `oidc-spa.browser-session-not-first-init:${configId}`;
361
380
 
381
+ const { completeLoginOrRefreshProcess } = await startLoginOrRefreshProcess();
382
+
362
383
  const resultOfLoginProcess = await (async (): Promise<
363
384
  | undefined // User is currently not logged in
364
385
  | Error // Initialization error
@@ -368,58 +389,13 @@ export async function createOidc_nonMemoized<
368
389
  }
369
390
  > => {
370
391
  handle_redirect_auth_response: {
371
- const authResponse = (() => {
372
- const value = sessionStorage.getItem(AUTH_RESPONSE_KEY);
373
-
374
- if (value === null) {
375
- return undefined;
376
- }
377
-
378
- let authResponse: unknown;
392
+ const authResponseAndStateData = retrieveRedirectAuthResponseAndStateData({ configId });
379
393
 
380
- try {
381
- authResponse = JSON.parse(value);
382
-
383
- assert(
384
- typeGuard<{ state: string; [key: string]: string }>(
385
- authResponse,
386
- authResponse instanceof Object &&
387
- Object.values(authResponse).every(value => typeof value === "string")
388
- ),
389
- "Valid json but not expected shape"
390
- );
391
- } catch (error) {
392
- console.error(`Failed to parse auth response from callback URL ${String(error)}`);
393
- return undefined;
394
- }
395
-
396
- return authResponse;
397
- })();
398
-
399
- if (authResponse === undefined) {
400
- break handle_redirect_auth_response;
401
- }
402
-
403
- const stateData = getStateData({ stateQueryParamValue: authResponse["state"] });
404
-
405
- assert(stateData !== undefined);
406
- assert(stateData.context === "redirect");
407
-
408
- const { evtAuthResponseHandled } = globalContext;
409
-
410
- if (stateData.configId !== configId) {
411
- // NOTE: Best attempt at letting the other client handle the request synchronously
412
- // but we won't wait for it because the initialization of the other client might
413
- // be contingent on the initialization of this client.
414
- const prHandled = evtAuthResponseHandled.waitFor();
415
- await Promise.resolve();
416
- if (sessionStorage.getItem(AUTH_RESPONSE_KEY) === null) {
417
- await prHandled;
418
- }
394
+ if (authResponseAndStateData === undefined) {
419
395
  break handle_redirect_auth_response;
420
396
  }
421
397
 
422
- sessionStorage.removeItem(AUTH_RESPONSE_KEY);
398
+ const { authResponse, stateData } = authResponseAndStateData;
423
399
 
424
400
  switch (stateData.action) {
425
401
  case "login":
@@ -431,11 +407,9 @@ export async function createOidc_nonMemoized<
431
407
  let oidcClientTsUser: OidcClientTsUser | undefined = undefined;
432
408
 
433
409
  try {
434
- oidcClientTsUser = await oidcClientTsUserManager
435
- .signinRedirectCallback(authResponseUrl)
436
- .finally(() => {
437
- evtAuthResponseHandled.post();
438
- });
410
+ oidcClientTsUser = await oidcClientTsUserManager.signinRedirectCallback(
411
+ authResponseUrl
412
+ );
439
413
  } catch (error) {
440
414
  assert(error instanceof Error);
441
415
 
@@ -447,11 +421,11 @@ export async function createOidc_nonMemoized<
447
421
  }
448
422
 
449
423
  {
450
- const error: string | undefined = authResponse["error"];
424
+ const authResponse_error = authResponse.error;
451
425
 
452
- if (error !== undefined) {
426
+ if (authResponse_error !== undefined) {
453
427
  log?.(
454
- `The auth server responded with: ${error}, trying to restore from the http only cookie`
428
+ `The auth server responded with: ${authResponse_error}, trying to restore from the http only cookie`
455
429
  );
456
430
  break handle_redirect_auth_response;
457
431
  }
@@ -462,18 +436,31 @@ export async function createOidc_nonMemoized<
462
436
 
463
437
  sessionStorage.removeItem(BROWSER_SESSION_NOT_FIRST_INIT_KEY);
464
438
 
439
+ notifyOtherTabsOfLogin({ configId });
440
+
465
441
  return {
466
442
  oidcClientTsUser,
467
443
  backFromAuthServer: {
468
444
  extraQueryParams: stateData.extraQueryParams,
469
445
  result: Object.fromEntries(
470
- Object.entries(authResponse).filter(
471
- ([name]) =>
472
- name !== "state" &&
473
- name !== "session_state" &&
474
- name !== "iss" &&
475
- name !== "code"
476
- )
446
+ Object.entries(authResponse)
447
+ .map(([name, value]) => {
448
+ if (
449
+ name === "state" ||
450
+ name === "session_state" ||
451
+ name === "iss" ||
452
+ name === "code"
453
+ ) {
454
+ return undefined;
455
+ }
456
+
457
+ if (value === undefined) {
458
+ return undefined;
459
+ }
460
+
461
+ return [name, value];
462
+ })
463
+ .filter(entry => entry !== undefined)
477
464
  )
478
465
  }
479
466
  };
@@ -489,8 +476,6 @@ export async function createOidc_nonMemoized<
489
476
  await oidcClientTsUserManager.signoutRedirectCallback(authResponseUrl);
490
477
  } catch {}
491
478
 
492
- evtAuthResponseHandled.post();
493
-
494
479
  notifyOtherTabsOfLogout({
495
480
  configId,
496
481
  redirectUrl: stateData.redirectUrl,
@@ -505,7 +490,7 @@ export async function createOidc_nonMemoized<
505
490
  }
506
491
 
507
492
  restore_from_session_storage: {
508
- if (!isUserStorePersistent) {
493
+ if (isUserStoreInMemoryOnly) {
509
494
  break restore_from_session_storage;
510
495
  }
511
496
 
@@ -533,67 +518,77 @@ export async function createOidc_nonMemoized<
533
518
  };
534
519
  }
535
520
 
536
- restore_from_http_only_cookie: {
521
+ silent_login_if_possible_and_auto_login: {
537
522
  log?.("Trying to restore the auth from the http only cookie (silent signin with iframe)");
538
523
 
539
524
  const persistedAuthState = getPersistedAuthState({ configId });
540
525
 
541
- if (persistedAuthState === "explicitly logged out") {
526
+ if (persistedAuthState === "explicitly logged out" && !autoLogin) {
542
527
  log?.("Skipping silent signin with iframe, the user has logged out");
543
- break restore_from_http_only_cookie;
528
+ break silent_login_if_possible_and_auto_login;
544
529
  }
545
530
 
546
- const result_loginSilent = await loginSilent({
547
- oidcClientTsUserManager,
548
- stateQueryParamValue_instance,
549
- configId,
550
- getExtraTokenParams
551
- });
552
-
553
- assert(result_loginSilent.outcome !== "token refreshed using refresh token");
531
+ let authResponse_error: string | undefined = undefined;
532
+ let oidcClientTsUser: OidcClientTsUser | undefined = undefined;
554
533
 
555
- if (result_loginSilent.outcome === "failure") {
556
- switch (result_loginSilent.cause) {
557
- case "can't reach well-known oidc endpoint":
558
- return createWellKnownOidcConfigurationEndpointUnreachableInitializationError({
559
- issuerUri
560
- });
561
- case "timeout":
562
- return createIframeTimeoutInitializationError({
563
- homeAndCallbackUrl,
564
- clientId,
565
- issuerUri
566
- });
534
+ actual_silent_signin: {
535
+ if (persistedAuthState === "explicitly logged out") {
536
+ break actual_silent_signin;
567
537
  }
568
538
 
569
- assert<Equals<typeof result_loginSilent.cause, never>>(false);
570
- }
539
+ const result_loginSilent = await loginSilent({
540
+ oidcClientTsUserManager,
541
+ stateQueryParamValue_instance,
542
+ configId,
543
+ getExtraTokenParams
544
+ });
545
+
546
+ assert(result_loginSilent.outcome !== "token refreshed using refresh token");
547
+
548
+ if (result_loginSilent.outcome === "failure") {
549
+ switch (result_loginSilent.cause) {
550
+ case "can't reach well-known oidc endpoint":
551
+ return createWellKnownOidcConfigurationEndpointUnreachableInitializationError(
552
+ {
553
+ issuerUri
554
+ }
555
+ );
556
+ case "timeout":
557
+ return createIframeTimeoutInitializationError({
558
+ homeAndCallbackUrl,
559
+ clientId,
560
+ issuerUri
561
+ });
562
+ }
571
563
 
572
- assert<Equals<typeof result_loginSilent.outcome, "got auth response from iframe">>();
564
+ assert<Equals<typeof result_loginSilent.cause, never>>(false);
565
+ }
573
566
 
574
- const { authResponse } = result_loginSilent;
567
+ assert<Equals<typeof result_loginSilent.outcome, "got auth response from iframe">>();
575
568
 
576
- log?.("Silent signin auth response", authResponse);
569
+ const { authResponse } = result_loginSilent;
577
570
 
578
- const authResponse_error: string | undefined = authResponse["error"];
579
- let oidcClientTsUser: OidcClientTsUser | undefined = undefined;
571
+ log?.("Silent signin auth response", authResponse);
580
572
 
581
- try {
582
- oidcClientTsUser = await oidcClientTsUserManager.signinRedirectCallback(
583
- authResponseToUrl(authResponse)
584
- );
585
- } catch (error) {
586
- assert(error instanceof Error);
573
+ authResponse_error = authResponse.error;
587
574
 
588
- if (error.message === "Failed to fetch") {
589
- return createFailedToFetchTokenEndpointInitializationError({
590
- clientId,
591
- issuerUri
592
- });
593
- }
575
+ try {
576
+ oidcClientTsUser = await oidcClientTsUserManager.signinRedirectCallback(
577
+ authResponseToUrl(authResponse)
578
+ );
579
+ } catch (error) {
580
+ assert(error instanceof Error);
594
581
 
595
- if (authResponse_error === undefined) {
596
- return error;
582
+ if (error.message === "Failed to fetch") {
583
+ return createFailedToFetchTokenEndpointInitializationError({
584
+ clientId,
585
+ issuerUri
586
+ });
587
+ }
588
+
589
+ if (authResponse_error === undefined) {
590
+ return error;
591
+ }
597
592
  }
598
593
  }
599
594
 
@@ -608,6 +603,18 @@ export async function createOidc_nonMemoized<
608
603
  ) {
609
604
  persistAuthState({ configId, state: undefined });
610
605
 
606
+ completeLoginOrRefreshProcess();
607
+
608
+ await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
609
+ prUnlock: new Promise<never>(() => {})
610
+ });
611
+
612
+ if (persistedAuthState === "logged in") {
613
+ globalContext.evtRequestToPersistTokens.post({
614
+ configIdOfInstancePostingTheRequest: configId
615
+ });
616
+ }
617
+
611
618
  await loginOrGoToAuthServer({
612
619
  action: "login",
613
620
  doForceReloadOnBfCache: true,
@@ -615,10 +622,9 @@ export async function createOidc_nonMemoized<
615
622
  doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack: autoLogin,
616
623
  extraQueryParams_local: undefined,
617
624
  transformUrlBeforeRedirect_local: undefined,
618
- doForceInteraction: false
625
+ doForceInteraction: persistedAuthState === "explicitly logged out"
619
626
  });
620
-
621
- // NOTE: Never here
627
+ assert(false);
622
628
  }
623
629
 
624
630
  log?.(
@@ -630,7 +636,7 @@ export async function createOidc_nonMemoized<
630
636
  ].join("")
631
637
  );
632
638
 
633
- break restore_from_http_only_cookie;
639
+ break silent_login_if_possible_and_auto_login;
634
640
  }
635
641
 
636
642
  log?.("Successful silent signed in");
@@ -643,71 +649,15 @@ export async function createOidc_nonMemoized<
643
649
 
644
650
  // NOTE: The user is not logged in.
645
651
  return undefined;
646
- })().then(result => {
647
- if (result === undefined) {
648
- return undefined;
649
- }
650
-
651
- if (result instanceof Error) {
652
- return result;
653
- }
654
-
655
- const { oidcClientTsUser, backFromAuthServer } = result;
656
-
657
- log_real_decoded_id_token: {
658
- if (log === undefined) {
659
- break log_real_decoded_id_token;
660
- }
661
- const idToken = oidcClientTsUser.id_token;
662
-
663
- if (idToken === undefined) {
664
- break log_real_decoded_id_token;
665
- }
666
-
667
- const decodedIdToken = decodeJwt(idToken);
668
-
669
- log(
670
- [
671
- `Decoded ID token`,
672
- decodedIdTokenSchema === undefined ? "" : " before `decodedIdTokenSchema.parse()`\n",
673
- JSON.stringify(decodedIdToken, null, 2)
674
- ].join("")
675
- );
676
-
677
- if (decodedIdTokenSchema === undefined) {
678
- break log_real_decoded_id_token;
679
- }
680
-
681
- log(
682
- [
683
- "Decoded ID token after `decodedIdTokenSchema.parse()`\n",
684
- JSON.stringify(decodedIdTokenSchema.parse(decodedIdToken), null, 2)
685
- ].join("")
686
- );
687
- }
688
-
689
- const tokens = oidcClientTsUserToTokens({
690
- oidcClientTsUser,
691
- decodedIdTokenSchema,
692
- __unsafe_useIdTokenAsAccessToken,
693
- decodedIdToken_previous: undefined,
694
- log
695
- });
652
+ })();
696
653
 
697
- if (tokens.refreshTokenExpirationTime < tokens.accessTokenExpirationTime) {
698
- console.warn(
699
- [
700
- "The OIDC refresh token shorter than the one of the access token.",
701
- "This is very unusual and probably a misconfiguration.",
702
- `Check your oidc server configuration for ${clientId} ${issuerUri}`
703
- ].join(" ")
704
- );
705
- }
654
+ completeLoginOrRefreshProcess();
706
655
 
707
- return { tokens, backFromAuthServer };
656
+ await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
657
+ prUnlock: Promise.resolve()
708
658
  });
709
659
 
710
- const common: Oidc.Common = {
660
+ const oidc_common: Oidc.Common = {
711
661
  params: {
712
662
  issuerUri,
713
663
  clientId
@@ -719,6 +669,12 @@ export async function createOidc_nonMemoized<
719
669
  break not_loggedIn_case;
720
670
  }
721
671
 
672
+ evtIsUserLoggedIn.post(false);
673
+
674
+ if (getPersistedAuthState({ configId }) !== "explicitly logged out") {
675
+ persistAuthState({ configId, state: undefined });
676
+ }
677
+
722
678
  const oidc_notLoggedIn: Oidc.NotLoggedIn = (() => {
723
679
  if (resultOfLoginProcess instanceof Error) {
724
680
  log?.("User not logged in and there was an initialization error");
@@ -747,7 +703,7 @@ export async function createOidc_nonMemoized<
747
703
  );
748
704
 
749
705
  return id<Oidc.NotLoggedIn>({
750
- ...common,
706
+ ...oidc_common,
751
707
  isUserLoggedIn: false,
752
708
  login: async () => {
753
709
  alert("Authentication is currently unavailable. Please try again later.");
@@ -761,15 +717,19 @@ export async function createOidc_nonMemoized<
761
717
  log?.("User not logged in");
762
718
 
763
719
  return id<Oidc.NotLoggedIn>({
764
- ...common,
720
+ ...oidc_common,
765
721
  isUserLoggedIn: false,
766
- login: ({
722
+ login: async ({
767
723
  doesCurrentHrefRequiresAuth,
768
724
  extraQueryParams,
769
725
  redirectUrl,
770
726
  transformUrlBeforeRedirect
771
- }) =>
772
- loginOrGoToAuthServer({
727
+ }) => {
728
+ await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
729
+ prUnlock: getPrSafelyRestoredFromBfCacheAfterLoginBackNavigation()
730
+ });
731
+
732
+ return loginOrGoToAuthServer({
773
733
  action: "login",
774
734
  doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack:
775
735
  doesCurrentHrefRequiresAuth,
@@ -780,7 +740,8 @@ export async function createOidc_nonMemoized<
780
740
  transformUrlBeforeRedirect_local: transformUrlBeforeRedirect,
781
741
  doForceInteraction:
782
742
  getPersistedAuthState({ configId }) === "explicitly logged out"
783
- }),
743
+ });
744
+ },
784
745
  initializationError: undefined
785
746
  });
786
747
  }
@@ -788,11 +749,21 @@ export async function createOidc_nonMemoized<
788
749
  assert<Equals<typeof resultOfLoginProcess, never>>(false);
789
750
  })();
790
751
 
791
- if (getPersistedAuthState({ configId }) !== "explicitly logged out") {
792
- persistAuthState({ configId, state: undefined });
793
- }
752
+ {
753
+ const { prOtherTabLogin } = getPrOtherTabLogin({
754
+ configId
755
+ });
756
+
757
+ prOtherTabLogin.then(async () => {
758
+ log?.(`Other tab has logged in, reloading this tab`);
759
+
760
+ await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
761
+ prUnlock: new Promise<never>(() => {})
762
+ });
794
763
 
795
- toCallBeforeReturningOidcNotLoggedIn();
764
+ window.location.reload();
765
+ });
766
+ }
796
767
 
797
768
  // @ts-expect-error: We know what we're doing
798
769
  return oidc_notLoggedIn;
@@ -800,7 +771,31 @@ export async function createOidc_nonMemoized<
800
771
 
801
772
  log?.("User is logged in");
802
773
 
803
- let currentTokens = resultOfLoginProcess.tokens;
774
+ evtIsUserLoggedIn.post(true);
775
+
776
+ let currentTokens = oidcClientTsUserToTokens({
777
+ oidcClientTsUser: resultOfLoginProcess.oidcClientTsUser,
778
+ decodedIdTokenSchema,
779
+ __unsafe_useIdTokenAsAccessToken,
780
+ decodedIdToken_previous: undefined,
781
+ log
782
+ });
783
+
784
+ {
785
+ if (getPersistedAuthState({ configId }) !== undefined) {
786
+ persistAuthState({ configId, state: undefined });
787
+ }
788
+
789
+ if (!areThirdPartyCookiesAllowed) {
790
+ persistAuthState({
791
+ configId,
792
+ state: {
793
+ stateDescription: "logged in",
794
+ untilTime: currentTokens.refreshTokenExpirationTime
795
+ }
796
+ });
797
+ }
798
+ }
804
799
 
805
800
  const autoLogoutCountdownTickCallbacks = new Set<
806
801
  (params: { secondsLeft: number | undefined }) => void
@@ -809,7 +804,7 @@ export async function createOidc_nonMemoized<
809
804
  const onTokenChanges = new Set<(tokens: Oidc.Tokens<DecodedIdToken>) => void>();
810
805
 
811
806
  const oidc_loggedIn = id<Oidc.LoggedIn<DecodedIdToken>>({
812
- ...common,
807
+ ...oidc_common,
813
808
  isUserLoggedIn: true,
814
809
  getTokens: () => currentTokens,
815
810
  getTokens_next: async () => {
@@ -828,12 +823,6 @@ export async function createOidc_nonMemoized<
828
823
 
829
824
  globalContext.hasLogoutBeenCalled = true;
830
825
 
831
- document.addEventListener("visibilitychange", () => {
832
- if (document.visibilityState === "visible") {
833
- location.reload();
834
- }
835
- });
836
-
837
826
  const postLogoutRedirectUrl: string = (() => {
838
827
  switch (params.redirectTo) {
839
828
  case "current page":
@@ -848,6 +837,14 @@ export async function createOidc_nonMemoized<
848
837
  }
849
838
  })();
850
839
 
840
+ await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
841
+ prUnlock: new Promise<never>(() => {})
842
+ });
843
+
844
+ window.addEventListener("pageshow", () => {
845
+ location.reload();
846
+ });
847
+
851
848
  try {
852
849
  await oidcClientTsUserManager.signoutRedirect({
853
850
  state: id<StateData>({
@@ -866,7 +863,7 @@ export async function createOidc_nonMemoized<
866
863
  if (error.message === "No end session endpoint") {
867
864
  log?.("No end session endpoint, managing logging state locally");
868
865
 
869
- persistAuthState({ configId, state: "explicitly logged out" });
866
+ persistAuthState({ configId, state: { stateDescription: "explicitly logged out" } });
870
867
 
871
868
  try {
872
869
  await oidcClientTsUserManager.removeUser();
@@ -874,6 +871,12 @@ export async function createOidc_nonMemoized<
874
871
  // NOTE: Not sure if it can throw
875
872
  }
876
873
 
874
+ notifyOtherTabsOfLogout({
875
+ configId,
876
+ redirectUrl: postLogoutRedirectUrl,
877
+ sessionId
878
+ });
879
+
877
880
  window.location.href = postLogoutRedirectUrl;
878
881
  } else {
879
882
  throw error;
@@ -888,6 +891,8 @@ export async function createOidc_nonMemoized<
888
891
 
889
892
  log?.("Renewing tokens");
890
893
 
894
+ const { completeLoginOrRefreshProcess } = await startLoginOrRefreshProcess();
895
+
891
896
  const result_loginSilent = await loginSilent({
892
897
  oidcClientTsUserManager,
893
898
  stateQueryParamValue_instance,
@@ -896,6 +901,7 @@ export async function createOidc_nonMemoized<
896
901
  });
897
902
 
898
903
  if (result_loginSilent.outcome === "failure") {
904
+ completeLoginOrRefreshProcess();
899
905
  throw new Error(result_loginSilent.cause);
900
906
  }
901
907
 
@@ -914,7 +920,7 @@ export async function createOidc_nonMemoized<
914
920
 
915
921
  log?.("Tokens refresh using iframe", authResponse);
916
922
 
917
- const authResponse_error: string | undefined = authResponse["error"];
923
+ const authResponse_error = authResponse.error;
918
924
 
919
925
  let oidcClientTsUser_scope: OidcClientTsUser | undefined = undefined;
920
926
 
@@ -927,6 +933,7 @@ export async function createOidc_nonMemoized<
927
933
  assert(error instanceof Error);
928
934
 
929
935
  if (authResponse_error === undefined) {
936
+ completeLoginOrRefreshProcess();
930
937
  throw error;
931
938
  }
932
939
 
@@ -936,6 +943,16 @@ export async function createOidc_nonMemoized<
936
943
  if (oidcClientTsUser_scope === undefined) {
937
944
  persistAuthState({ configId, state: undefined });
938
945
 
946
+ completeLoginOrRefreshProcess();
947
+
948
+ await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
949
+ prUnlock: new Promise<never>(() => {})
950
+ });
951
+
952
+ globalContext.evtRequestToPersistTokens.post({
953
+ configIdOfInstancePostingTheRequest: configId
954
+ });
955
+
939
956
  await loginOrGoToAuthServer({
940
957
  action: "login",
941
958
  redirectUrl: window.location.href,
@@ -964,7 +981,19 @@ export async function createOidc_nonMemoized<
964
981
  log
965
982
  });
966
983
 
984
+ if (getPersistedAuthState({ configId }) !== undefined) {
985
+ persistAuthState({
986
+ configId,
987
+ state: {
988
+ stateDescription: "logged in",
989
+ untilTime: currentTokens.refreshTokenExpirationTime
990
+ }
991
+ });
992
+ }
993
+
967
994
  Array.from(onTokenChanges).forEach(onTokenChange => onTokenChange(currentTokens));
995
+
996
+ completeLoginOrRefreshProcess();
968
997
  }
969
998
 
970
999
  let ongoingCall:
@@ -1079,14 +1108,27 @@ export async function createOidc_nonMemoized<
1079
1108
  sessionId
1080
1109
  });
1081
1110
 
1082
- prOtherTabLogout.then(({ redirectUrl }) => {
1111
+ prOtherTabLogout.then(async ({ redirectUrl }) => {
1083
1112
  log?.(`Other tab has logged out, redirecting to ${redirectUrl}`);
1113
+
1114
+ await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
1115
+ prUnlock: new Promise<never>(() => {})
1116
+ });
1117
+
1118
+ window.addEventListener("pageshow", () => {
1119
+ location.reload();
1120
+ });
1121
+
1084
1122
  window.location.href = redirectUrl;
1085
1123
  });
1086
1124
  }
1087
1125
 
1088
1126
  (function scheduleRenew() {
1089
- const login_dueToExpiration = () => {
1127
+ const login_dueToExpiration = async () => {
1128
+ await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
1129
+ prUnlock: new Promise<never>(() => {})
1130
+ });
1131
+
1090
1132
  persistAuthState({ configId, state: undefined });
1091
1133
 
1092
1134
  return loginOrGoToAuthServer({
@@ -1149,9 +1191,16 @@ export async function createOidc_nonMemoized<
1149
1191
  })();
1150
1192
 
1151
1193
  auto_logout: {
1152
- if (currentTokens.refreshToken === "" && __unsafe_ssoSessionIdleSeconds === undefined) {
1194
+ if (
1195
+ (!currentTokens.hasRefreshToken || currentTokens.refreshTokenExpirationTime === undefined) &&
1196
+ __unsafe_ssoSessionIdleSeconds === undefined
1197
+ ) {
1153
1198
  log?.(
1154
- "No refresh token, and ____unsafe_ssoSessionIdleSeconds was not set, auto logout non applicable"
1199
+ `${
1200
+ currentTokens.hasRefreshToken
1201
+ ? "The refresh token is opaque, we can't read it's expiration time"
1202
+ : "No refresh token"
1203
+ }, and __unsafe_ssoSessionIdleSeconds was not set, can't implement auto logout mechanism`
1155
1204
  );
1156
1205
  break auto_logout;
1157
1206
  }
@@ -1161,7 +1210,9 @@ export async function createOidc_nonMemoized<
1161
1210
  const getCountdownEndTime = () =>
1162
1211
  __unsafe_ssoSessionIdleSeconds !== undefined
1163
1212
  ? Date.now() + __unsafe_ssoSessionIdleSeconds * 1000
1164
- : currentTokens.refreshTokenExpirationTime;
1213
+ : (assert(currentTokens.hasRefreshToken),
1214
+ assert(currentTokens.refreshTokenExpirationTime !== undefined),
1215
+ currentTokens.refreshTokenExpirationTime);
1165
1216
 
1166
1217
  const durationBeforeAutoLogout = toHumanReadableDuration(
1167
1218
  getCountdownEndTime() - Date.now()
@@ -1193,14 +1244,12 @@ export async function createOidc_nonMemoized<
1193
1244
 
1194
1245
  let stopCountdown: (() => void) | undefined = undefined;
1195
1246
 
1196
- if (globalContext.$isUserActive === undefined) {
1197
- globalContext.$isUserActive = create$isUserActive({
1198
- configId,
1199
- sessionId
1200
- });
1201
- }
1247
+ const evtIsUserActive = createEvtIsUserActive({
1248
+ configId,
1249
+ sessionId
1250
+ });
1202
1251
 
1203
- globalContext.$isUserActive.subscribe(isUserActive => {
1252
+ evtIsUserActive.subscribe(isUserActive => {
1204
1253
  if (isUserActive) {
1205
1254
  if (stopCountdown !== undefined) {
1206
1255
  stopCountdown();
@@ -1213,17 +1262,5 @@ export async function createOidc_nonMemoized<
1213
1262
  });
1214
1263
  }
1215
1264
 
1216
- {
1217
- if (getPersistedAuthState({ configId }) !== undefined) {
1218
- persistAuthState({ configId, state: undefined });
1219
- }
1220
-
1221
- if (!areThirdPartyCookiesAllowed) {
1222
- persistAuthState({ configId, state: "logged in" });
1223
- }
1224
- }
1225
-
1226
- toCallBeforeReturningOidcLoggedIn();
1227
-
1228
1265
  return oidc_loggedIn;
1229
1266
  }