oidc-spa 6.4.0 → 6.5.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 (43) hide show
  1. package/oidc/createOidc.d.ts +0 -2
  2. package/oidc/createOidc.js +307 -355
  3. package/oidc/createOidc.js.map +1 -1
  4. package/oidc/{createIsUserActive.js → isUserActive.js} +1 -1
  5. package/oidc/isUserActive.js.map +1 -0
  6. package/oidc/loginOrGoToAuthServer.d.ts +41 -0
  7. package/oidc/loginOrGoToAuthServer.js +296 -0
  8. package/oidc/loginOrGoToAuthServer.js.map +1 -0
  9. package/oidc/loginSilent.d.ts +2 -2
  10. package/oidc/loginSilent.js +2 -2
  11. package/oidc/loginSilent.js.map +1 -1
  12. package/oidc/oidcClientTsUserToTokens.d.ts +1 -0
  13. package/oidc/oidcClientTsUserToTokens.js +16 -0
  14. package/oidc/oidcClientTsUserToTokens.js.map +1 -1
  15. package/oidc/persistedAuthState.d.ts +9 -0
  16. package/oidc/persistedAuthState.js +28 -0
  17. package/oidc/persistedAuthState.js.map +1 -0
  18. package/package.json +26 -11
  19. package/src/oidc/createOidc.ts +291 -353
  20. package/src/oidc/loginOrGoToAuthServer.ts +267 -0
  21. package/src/oidc/loginSilent.ts +4 -4
  22. package/src/oidc/oidcClientTsUserToTokens.ts +24 -0
  23. package/src/oidc/persistedAuthState.ts +36 -0
  24. package/src/tools/ephemeralSessionStorage.ts +191 -0
  25. package/src/tools/haveSharedParentDomain.ts +13 -0
  26. package/src/tools/parseKeycloakIssuerUri.ts +9 -2
  27. package/tools/ephemeralSessionStorage.d.ts +3 -0
  28. package/tools/ephemeralSessionStorage.js +133 -0
  29. package/tools/ephemeralSessionStorage.js.map +1 -0
  30. package/tools/haveSharedParentDomain.d.ts +4 -0
  31. package/tools/haveSharedParentDomain.js +14 -0
  32. package/tools/haveSharedParentDomain.js.map +1 -0
  33. package/tools/parseKeycloakIssuerUri.d.ts +1 -0
  34. package/tools/parseKeycloakIssuerUri.js +4 -1
  35. package/tools/parseKeycloakIssuerUri.js.map +1 -1
  36. package/vendor/frontend/oidc-client-ts-and-jwt-decode.js +1 -1
  37. package/oidc/createIsUserActive.js.map +0 -1
  38. package/oidc/persistedLogoutState.d.ts +0 -9
  39. package/oidc/persistedLogoutState.js +0 -25
  40. package/oidc/persistedLogoutState.js.map +0 -1
  41. package/src/oidc/persistedLogoutState.ts +0 -29
  42. /package/oidc/{createIsUserActive.d.ts → isUserActive.d.ts} +0 -0
  43. /package/src/oidc/{createIsUserActive.ts → isUserActive.ts} +0 -0
@@ -4,11 +4,11 @@ import {
4
4
  type User as OidcClientTsUser,
5
5
  InMemoryWebStorage
6
6
  } from "../vendor/frontend/oidc-client-ts-and-jwt-decode";
7
- import { id, type Param0, assert, is, type Equals, typeGuard } from "../vendor/frontend/tsafe";
7
+ import { id, assert, is, type Equals, typeGuard } 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 "./createIsUserActive";
11
+ import { create$isUserActive } from "./isUserActive";
12
12
  import { createStartCountdown } from "../tools/startCountdown";
13
13
  import type { StatefulObservable } from "../tools/StatefulObservable";
14
14
  import { toHumanReadableDuration } from "../tools/toHumanReadableDuration";
@@ -27,16 +27,15 @@ import {
27
27
  } from "./StateData";
28
28
  import { notifyOtherTabsOfLogout, getPrOtherTabLogout } from "./logoutPropagationToOtherTabs";
29
29
  import { getConfigId } from "./configId";
30
- import { oidcClientTsUserToTokens } from "./oidcClientTsUserToTokens";
30
+ import { oidcClientTsUserToTokens, getMsBeforeExpiration } from "./oidcClientTsUserToTokens";
31
31
  import { loginSilent, authResponseToUrl } from "./loginSilent";
32
32
  import { handleOidcCallback, AUTH_RESPONSE_KEY } from "./handleOidcCallback";
33
- import {
34
- clearPersistedLogoutState,
35
- getIsPersistedLogoutState,
36
- persistLogoutState
37
- } from "./persistedLogoutState";
33
+ import { getPersistedAuthState, persistAuthState } from "./persistedAuthState";
38
34
  import type { Oidc } from "./Oidc";
39
35
  import { type AwaitableEventEmitter, createAwaitableEventEmitter } from "../tools/AwaitableEventEmitter";
36
+ import { getHaveSharedParentDomain } from "../tools/haveSharedParentDomain";
37
+ import { createLoginOrGoToAuthServer } from "./loginOrGoToAuthServer";
38
+ import { createEphemeralSessionStorage } from "../tools/ephemeralSessionStorage";
40
39
 
41
40
  // NOTE: Replaced at build time
42
41
  const VERSION = "{{OIDC_SPA_VERSION}}";
@@ -130,9 +129,7 @@ declare global {
130
129
  [GLOBAL_CONTEXT_KEY]: {
131
130
  prOidcByConfigId: Map<string, Promise<Oidc<any>>>;
132
131
  evtAuthResponseHandled: AwaitableEventEmitter<void>;
133
- URL_real: typeof URL;
134
132
  $isUserActive: StatefulObservable<boolean> | undefined;
135
- hasLoginBeenCalled: boolean;
136
133
  hasLogoutBeenCalled: boolean;
137
134
  };
138
135
  }
@@ -141,14 +138,14 @@ declare global {
141
138
  window[GLOBAL_CONTEXT_KEY] ??= {
142
139
  prOidcByConfigId: new Map(),
143
140
  evtAuthResponseHandled: createAwaitableEventEmitter<void>(),
144
- URL_real: window.URL,
145
141
  $isUserActive: undefined,
146
- hasLoginBeenCalled: false,
147
142
  hasLogoutBeenCalled: false
148
143
  };
149
144
 
150
145
  const globalContext = window[GLOBAL_CONTEXT_KEY];
151
146
 
147
+ const MIN_RENEW_BEFORE_EXPIRE_MS = 2_000;
148
+
152
149
  /** @see: https://docs.oidc-spa.dev/v/v6/usage */
153
150
  export async function createOidc<
154
151
  DecodedIdToken extends Record<string, unknown> = Record<string, unknown>,
@@ -256,7 +253,7 @@ export async function createOidc_nonMemoized<
256
253
  __unsafe_ssoSessionIdleSeconds,
257
254
  autoLogoutParams = { redirectTo: "current page" },
258
255
  autoLogin = false,
259
- postLoginRedirectUrl,
256
+ postLoginRedirectUrl: postLoginRedirectUrl_default,
260
257
  __unsafe_clientSecret,
261
258
  __unsafe_useIdTokenAsAccessToken = false
262
259
  } = params;
@@ -299,11 +296,32 @@ export async function createOidc_nonMemoized<
299
296
  }
300
297
  }
301
298
 
302
- const USER_LOGGED_IN_KEY = `oidc-spa.user-logged-in:${configId}`;
299
+ const stateQueryParamValue_instance = generateStateQueryParamValue();
303
300
 
304
- localStorage.removeItem(USER_LOGGED_IN_KEY);
301
+ let areThirdPartyCookiesAllowed: boolean;
302
+ {
303
+ const url1 = window.location.origin;
304
+ const url2 = issuerUri;
305
305
 
306
- const stateQueryParamValue_instance = generateStateQueryParamValue();
306
+ areThirdPartyCookiesAllowed = getHaveSharedParentDomain({
307
+ url1,
308
+ url2
309
+ });
310
+
311
+ if (areThirdPartyCookiesAllowed) {
312
+ log?.(`${url1} and ${url2} have shared parent domain, third party cookies are allowed`);
313
+ } else {
314
+ log?.(
315
+ [
316
+ `${url1} and ${url2} don't have shared parent domain, setting third party cookies`,
317
+ `on the auth server domain might not work. Making sure that everything works smoothly regardless`,
318
+ `by allowing oidc-spa to store the auth state in the session storage for a limited period of time.`
319
+ ].join(" ")
320
+ );
321
+ }
322
+ }
323
+
324
+ const isUserStorePersistent = !areThirdPartyCookiesAllowed;
307
325
 
308
326
  const oidcClientTsUserManager = new OidcClientTsUserManager({
309
327
  stateQueryParamValue: stateQueryParamValue_instance,
@@ -315,214 +333,29 @@ export async function createOidc_nonMemoized<
315
333
  response_type: "code",
316
334
  scope: Array.from(new Set(["openid", ...scopes])).join(" "),
317
335
  automaticSilentRenew: false,
318
- userStore: new WebStorageStateStore({ store: new InMemoryWebStorage() }),
336
+ userStore: new WebStorageStateStore({
337
+ store: areThirdPartyCookiesAllowed
338
+ ? new InMemoryWebStorage()
339
+ : createEphemeralSessionStorage({
340
+ sessionStorageTtlMs: 3 * 60_1000
341
+ })
342
+ }),
319
343
  stateStore: new WebStorageStateStore({ store: localStorage, prefix: STATE_STORE_KEY_PREFIX }),
320
344
  client_secret: __unsafe_clientSecret
321
345
  });
322
346
 
323
- let lastPublicUrl: string | undefined = undefined;
324
-
325
- // NOTE: To call only if not logged in.
326
- const startTrackingLastPublicUrl = () => {
327
- const realPushState = history.pushState.bind(history);
328
- history.pushState = function pushState(...args) {
329
- lastPublicUrl = window.location.href;
330
- return realPushState(...args);
331
- };
332
- };
333
-
334
- type ParamsOfLoginOrGoToAuthServer = Omit<
335
- Param0<Oidc.NotLoggedIn["login"]>,
336
- "doesCurrentHrefRequiresAuth"
337
- > &
338
- ({ action: "login"; doesCurrentHrefRequiresAuth: boolean } | { action: "go to auth server" });
339
-
340
- const loginOrGoToAuthServer = async (params: ParamsOfLoginOrGoToAuthServer): Promise<never> => {
341
- const {
342
- extraQueryParams: extraQueryParams_fromLoginFn,
343
- redirectUrl: redirectUrl_params,
344
- transformUrlBeforeRedirect: transformUrlBeforeRedirect_fromLoginFn,
345
- ...rest
346
- } = params;
347
-
348
- log?.("Calling loginOrGoToAuthServer", { params });
349
-
350
- // NOTE: This is for handling cases when user press the back button on the login pages.
351
- // When the app is hosted on https (so not in dev mode) the browser will restore the state of the app
352
- // instead of reloading the page.
353
- if (rest.action === "login") {
354
- if (globalContext.hasLoginBeenCalled) {
355
- log?.("login() has already been called, ignoring the call");
356
- return new Promise<never>(() => {});
357
- }
358
-
359
- globalContext.hasLoginBeenCalled = true;
360
-
361
- const callback = () => {
362
- if (document.visibilityState === "visible") {
363
- document.removeEventListener("visibilitychange", callback);
364
-
365
- log?.(
366
- "We came back from the login pages and the state of the app has been restored"
367
- );
368
-
369
- if (rest.doesCurrentHrefRequiresAuth) {
370
- if (lastPublicUrl !== undefined) {
371
- log?.(`Loading last public route: ${lastPublicUrl}`);
372
- window.location.href = lastPublicUrl;
373
- } else {
374
- log?.("We don't know the last public route, navigating back in history");
375
- window.history.back();
376
- }
377
- } else {
378
- log?.("The current page doesn't require auth...");
379
-
380
- if (localStorage.getItem(USER_LOGGED_IN_KEY)) {
381
- log?.("but the user is now authenticated, reloading the page");
382
- location.reload();
383
- } else {
384
- log?.("and the user doesn't seem to be authenticated, avoiding a reload");
385
- globalContext.hasLoginBeenCalled = false;
386
- }
387
- }
388
- }
389
- };
390
-
391
- log?.("Start listening to visibility change event");
392
-
393
- document.addEventListener("visibilitychange", callback);
394
- }
395
-
396
- const redirectUrl =
397
- redirectUrl_params === undefined
398
- ? window.location.href
399
- : toFullyQualifiedUrl({
400
- urlish: redirectUrl_params,
401
- doAssertNoQueryParams: false
402
- });
403
-
404
- log?.(`redirectUrl: ${redirectUrl}`);
405
-
406
- //NOTE: We know there is a extraQueryParameter option but it doesn't allow
407
- // to control the encoding so we have to highjack global URL Class that is
408
- // used internally by oidc-client-ts. It's save to do so since this is the
409
- // last thing that will be done before the redirect.
410
- {
411
- const { URL_real } = globalContext;
412
-
413
- const URL = (...args: ConstructorParameters<typeof URL_real>) => {
414
- const urlInstance = new URL_real(...args);
415
-
416
- return new Proxy(urlInstance, {
417
- get: (target, prop) => {
418
- if (prop === "href") {
419
- Object.defineProperty(window, "URL", { value: URL_real });
420
-
421
- let url = urlInstance.href;
422
-
423
- (
424
- [
425
- [getExtraQueryParams?.(), transformUrlBeforeRedirect],
426
- [
427
- extraQueryParams_fromLoginFn,
428
- transformUrlBeforeRedirect_fromLoginFn
429
- ]
430
- ] as const
431
- ).forEach(([extraQueryParams, transformUrlBeforeRedirect]) => {
432
- add_extra_query_params: {
433
- if (extraQueryParams === undefined) {
434
- break add_extra_query_params;
435
- }
436
-
437
- const url_obj = new URL_real(url);
438
-
439
- for (const [name, value] of Object.entries(extraQueryParams)) {
440
- url_obj.searchParams.set(name, value);
441
- }
442
-
443
- url = url_obj.href;
444
- }
445
-
446
- apply_transform_before_redirect: {
447
- if (transformUrlBeforeRedirect === undefined) {
448
- break apply_transform_before_redirect;
449
- }
450
- url = transformUrlBeforeRedirect(url);
451
- }
452
- });
453
-
454
- return url;
455
- }
456
-
457
- //@ts-expect-error
458
- return target[prop];
459
- }
460
- });
461
- };
462
-
463
- Object.defineProperty(window, "URL", { value: URL });
464
- }
465
-
466
- // NOTE: This is for the behavior when the use presses the back button on the login pages.
467
- // This is what happens when the user gave up the login process.
468
- // We want to that to redirect to the last public page.
469
- const redirectMethod = (() => {
470
- switch (rest.action) {
471
- case "login":
472
- return rest.doesCurrentHrefRequiresAuth ? "replace" : "assign";
473
- case "go to auth server":
474
- return "assign";
475
- }
476
- })();
477
-
478
- log?.(`redirectMethod: ${redirectMethod}`);
479
-
480
- const { extraQueryParams } = (() => {
481
- const extraQueryParams: Record<string, string> = extraQueryParams_fromLoginFn ?? {};
482
-
483
- read_query_params_added_by_transform_before_redirect: {
484
- if (transformUrlBeforeRedirect_fromLoginFn === undefined) {
485
- break read_query_params_added_by_transform_before_redirect;
486
- }
487
-
488
- let url_afterTransform;
489
-
490
- try {
491
- url_afterTransform = transformUrlBeforeRedirect_fromLoginFn("https://dummy.com");
492
- } catch {
493
- break read_query_params_added_by_transform_before_redirect;
494
- }
495
-
496
- for (const [name, value] of new URL(url_afterTransform).searchParams) {
497
- extraQueryParams[name] = value;
498
- }
499
- }
500
-
501
- return { extraQueryParams };
502
- })();
503
-
504
- await oidcClientTsUserManager.signinRedirect({
505
- state: id<StateData>({
506
- context: "redirect",
507
- redirectUrl,
508
- extraQueryParams,
509
- hasBeenProcessedByCallback: false,
510
- configId,
511
- action: "login",
512
- redirectUrl_consentRequiredCase: (() => {
513
- switch (rest.action) {
514
- case "login":
515
- return lastPublicUrl ?? homeAndCallbackUrl;
516
- case "go to auth server":
517
- return redirectUrl;
518
- }
519
- })()
520
- }),
521
- redirectMethod,
522
- prompt: getIsPersistedLogoutState({ configId }) ? "consent" : undefined
523
- });
524
- return new Promise<never>(() => {});
525
- };
347
+ const {
348
+ loginOrGoToAuthServer,
349
+ toCallBeforeReturningOidcLoggedIn,
350
+ toCallBeforeReturningOidcNotLoggedIn
351
+ } = createLoginOrGoToAuthServer({
352
+ configId,
353
+ oidcClientTsUserManager,
354
+ getExtraQueryParams,
355
+ transformUrlBeforeRedirect,
356
+ homeAndCallbackUrl,
357
+ log
358
+ });
526
359
 
527
360
  const BROWSER_SESSION_NOT_FIRST_INIT_KEY = `oidc-spa.browser-session-not-first-init:${configId}`;
528
361
 
@@ -628,7 +461,6 @@ export async function createOidc_nonMemoized<
628
461
  }
629
462
 
630
463
  sessionStorage.removeItem(BROWSER_SESSION_NOT_FIRST_INIT_KEY);
631
- clearPersistedLogoutState({ configId });
632
464
 
633
465
  return {
634
466
  oidcClientTsUser,
@@ -672,10 +504,41 @@ export async function createOidc_nonMemoized<
672
504
  }
673
505
  }
674
506
 
507
+ restore_from_session_storage: {
508
+ if (!isUserStorePersistent) {
509
+ break restore_from_session_storage;
510
+ }
511
+
512
+ let oidcClientTsUser: OidcClientTsUser | null;
513
+
514
+ try {
515
+ oidcClientTsUser = await oidcClientTsUserManager.getUser();
516
+ } catch {
517
+ // NOTE: Not sure if it can throw, but let's be safe.
518
+ oidcClientTsUser = null;
519
+ try {
520
+ await oidcClientTsUserManager.removeUser();
521
+ } catch {}
522
+ }
523
+
524
+ if (oidcClientTsUser === null) {
525
+ break restore_from_session_storage;
526
+ }
527
+
528
+ log?.("Restored the auth from ephemeral session storage");
529
+
530
+ return {
531
+ oidcClientTsUser,
532
+ backFromAuthServer: undefined
533
+ };
534
+ }
535
+
675
536
  restore_from_http_only_cookie: {
676
537
  log?.("Trying to restore the auth from the http only cookie (silent signin with iframe)");
677
538
 
678
- if (getIsPersistedLogoutState({ configId })) {
539
+ const persistedAuthState = getPersistedAuthState({ configId });
540
+
541
+ if (persistedAuthState === "explicitly logged out") {
679
542
  log?.("Skipping silent signin with iframe, the user has logged out");
680
543
  break restore_from_http_only_cookie;
681
544
  }
@@ -687,7 +550,7 @@ export async function createOidc_nonMemoized<
687
550
  getExtraTokenParams
688
551
  });
689
552
 
690
- assert(result_loginSilent.outcome !== "refresh token used");
553
+ assert(result_loginSilent.outcome !== "token refreshed using refresh token");
691
554
 
692
555
  if (result_loginSilent.outcome === "failure") {
693
556
  switch (result_loginSilent.cause) {
@@ -706,12 +569,13 @@ export async function createOidc_nonMemoized<
706
569
  assert<Equals<typeof result_loginSilent.cause, never>>(false);
707
570
  }
708
571
 
709
- assert<Equals<typeof result_loginSilent.outcome, "success iframe">>();
572
+ assert<Equals<typeof result_loginSilent.outcome, "got auth response from iframe">>();
710
573
 
711
574
  const { authResponse } = result_loginSilent;
712
575
 
713
576
  log?.("Silent signin auth response", authResponse);
714
577
 
578
+ const authResponse_error: string | undefined = authResponse["error"];
715
579
  let oidcClientTsUser: OidcClientTsUser | undefined = undefined;
716
580
 
717
581
  try {
@@ -728,24 +592,45 @@ export async function createOidc_nonMemoized<
728
592
  });
729
593
  }
730
594
 
731
- {
732
- const error: string | undefined = authResponse["error"];
733
-
734
- if (error !== undefined) {
735
- // NOTE: This is a very expected case, it happens each time there's no active session.
736
- log?.(
737
- [
738
- `The auth server responded with: ${error} `,
739
- "login_required" === error
740
- ? `(authentication_required just means that there's no active session for the user)`
741
- : ""
742
- ].join("")
743
- );
744
- break restore_from_http_only_cookie;
745
- }
595
+ if (authResponse_error === undefined) {
596
+ return error;
597
+ }
598
+ }
599
+
600
+ if (oidcClientTsUser === undefined) {
601
+ if (
602
+ autoLogin ||
603
+ (persistedAuthState === "logged in" &&
604
+ (authResponse_error === "interaction_required" ||
605
+ authResponse_error === "login_required" ||
606
+ authResponse_error === "consent_required" ||
607
+ authResponse_error === "account_selection_required"))
608
+ ) {
609
+ persistAuthState({ configId, state: undefined });
610
+
611
+ await loginOrGoToAuthServer({
612
+ action: "login",
613
+ doForceReloadOnBfCache: true,
614
+ redirectUrl: window.location.href,
615
+ doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack: autoLogin,
616
+ extraQueryParams_local: undefined,
617
+ transformUrlBeforeRedirect_local: undefined,
618
+ doForceInteraction: false
619
+ });
620
+
621
+ // NOTE: Never here
746
622
  }
747
623
 
748
- return error;
624
+ log?.(
625
+ [
626
+ `The auth server responded with: ${authResponse_error} `,
627
+ "login_required" === authResponse_error
628
+ ? `(login_required just means that there's no active session for the user)`
629
+ : ""
630
+ ].join("")
631
+ );
632
+
633
+ break restore_from_http_only_cookie;
749
634
  }
750
635
 
751
636
  log?.("Successful silent signed in");
@@ -829,118 +714,107 @@ export async function createOidc_nonMemoized<
829
714
  }
830
715
  };
831
716
 
832
- if (resultOfLoginProcess instanceof Error) {
833
- log?.("User not logged in and there was an initialization error");
717
+ not_loggedIn_case: {
718
+ if (!(resultOfLoginProcess instanceof Error) && resultOfLoginProcess !== undefined) {
719
+ break not_loggedIn_case;
720
+ }
834
721
 
835
- const error = resultOfLoginProcess;
722
+ const oidc_notLoggedIn: Oidc.NotLoggedIn = (() => {
723
+ if (resultOfLoginProcess instanceof Error) {
724
+ log?.("User not logged in and there was an initialization error");
836
725
 
837
- const initializationError =
838
- error instanceof OidcInitializationError
839
- ? error
840
- : new OidcInitializationError({
841
- isAuthServerLikelyDown: false,
842
- messageOrCause: error
843
- });
726
+ const error = resultOfLoginProcess;
844
727
 
845
- if (autoLogin) {
846
- throw initializationError;
847
- }
728
+ const initializationError =
729
+ error instanceof OidcInitializationError
730
+ ? error
731
+ : new OidcInitializationError({
732
+ isAuthServerLikelyDown: false,
733
+ messageOrCause: error
734
+ });
848
735
 
849
- console.error(
850
- [
851
- `oidc-spa Initialization Error: `,
852
- `isAuthServerLikelyDown: ${initializationError.isAuthServerLikelyDown}`,
853
- ``,
854
- initializationError.message
855
- ].join("\n")
856
- );
736
+ if (autoLogin) {
737
+ throw initializationError;
738
+ }
857
739
 
858
- startTrackingLastPublicUrl();
740
+ console.error(
741
+ [
742
+ `oidc-spa Initialization Error: `,
743
+ `isAuthServerLikelyDown: ${initializationError.isAuthServerLikelyDown}`,
744
+ ``,
745
+ initializationError.message
746
+ ].join("\n")
747
+ );
859
748
 
860
- const oidc = id<Oidc.NotLoggedIn>({
861
- ...common,
862
- isUserLoggedIn: false,
863
- login: async () => {
864
- alert("Authentication is currently unavailable. Please try again later.");
865
- return new Promise<never>(() => {});
866
- },
867
- initializationError
868
- });
749
+ return id<Oidc.NotLoggedIn>({
750
+ ...common,
751
+ isUserLoggedIn: false,
752
+ login: async () => {
753
+ alert("Authentication is currently unavailable. Please try again later.");
754
+ return new Promise<never>(() => {});
755
+ },
756
+ initializationError
757
+ });
758
+ }
869
759
 
870
- // @ts-expect-error: We know what we are doing.
871
- return oidc;
872
- }
760
+ if (resultOfLoginProcess === undefined) {
761
+ log?.("User not logged in");
762
+
763
+ return id<Oidc.NotLoggedIn>({
764
+ ...common,
765
+ isUserLoggedIn: false,
766
+ login: ({
767
+ doesCurrentHrefRequiresAuth,
768
+ extraQueryParams,
769
+ redirectUrl,
770
+ transformUrlBeforeRedirect
771
+ }) =>
772
+ loginOrGoToAuthServer({
773
+ action: "login",
774
+ doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack:
775
+ doesCurrentHrefRequiresAuth,
776
+ doForceReloadOnBfCache: false,
777
+ redirectUrl:
778
+ redirectUrl ?? postLoginRedirectUrl_default ?? window.location.href,
779
+ extraQueryParams_local: extraQueryParams,
780
+ transformUrlBeforeRedirect_local: transformUrlBeforeRedirect,
781
+ doForceInteraction:
782
+ getPersistedAuthState({ configId }) === "explicitly logged out"
783
+ }),
784
+ initializationError: undefined
785
+ });
786
+ }
873
787
 
874
- if (resultOfLoginProcess === undefined) {
875
- log?.("User not logged in");
788
+ assert<Equals<typeof resultOfLoginProcess, never>>(false);
789
+ })();
876
790
 
877
- if (autoLogin) {
878
- log?.("Authentication is required everywhere on this app, redirecting to the login page");
879
- await loginOrGoToAuthServer({
880
- action: "login",
881
- doesCurrentHrefRequiresAuth: true,
882
- redirectUrl: postLoginRedirectUrl
883
- });
884
- // Never here
791
+ if (getPersistedAuthState({ configId }) !== "explicitly logged out") {
792
+ persistAuthState({ configId, state: undefined });
885
793
  }
886
794
 
887
- startTrackingLastPublicUrl();
795
+ toCallBeforeReturningOidcNotLoggedIn();
888
796
 
889
- const oidc = id<Oidc.NotLoggedIn>({
890
- ...common,
891
- isUserLoggedIn: false,
892
- login: params => loginOrGoToAuthServer({ action: "login", ...params }),
893
- initializationError: undefined
894
- });
895
-
896
- // @ts-expect-error: We know what we are doing.
897
- return oidc;
797
+ // @ts-expect-error: We know what we're doing
798
+ return oidc_notLoggedIn;
898
799
  }
899
800
 
900
801
  log?.("User is logged in");
901
802
 
902
- localStorage.setItem(USER_LOGGED_IN_KEY, "true");
903
-
904
803
  let currentTokens = resultOfLoginProcess.tokens;
905
804
 
906
- function getMsBeforeExpiration() {
907
- // NOTE: In general the access token is supposed to have a shorter
908
- // lifespan than the refresh token but we don't want to make any
909
- // assumption here.
910
- const tokenExpirationTime = Math.min(
911
- currentTokens.accessTokenExpirationTime,
912
- currentTokens.refreshTokenExpirationTime
913
- );
914
-
915
- const msBeforeExpiration = Math.min(
916
- tokenExpirationTime - Date.now(),
917
- // NOTE: We want to make sure we do not overflow the setTimeout
918
- // that must be a 32 bit unsigned integer.
919
- // This can happen if the tokenExpirationTime is more than 24.8 days in the future.
920
- Math.pow(2, 31) - 1
921
- );
922
-
923
- if (msBeforeExpiration < 0) {
924
- log?.("Token has already expired");
925
- return 0;
926
- }
927
-
928
- return msBeforeExpiration;
929
- }
930
-
931
805
  const autoLogoutCountdownTickCallbacks = new Set<
932
806
  (params: { secondsLeft: number | undefined }) => void
933
807
  >();
934
808
 
935
809
  const onTokenChanges = new Set<(tokens: Oidc.Tokens<DecodedIdToken>) => void>();
936
810
 
937
- const oidc = id<Oidc.LoggedIn<DecodedIdToken>>({
811
+ const oidc_loggedIn = id<Oidc.LoggedIn<DecodedIdToken>>({
938
812
  ...common,
939
813
  isUserLoggedIn: true,
940
814
  getTokens: () => currentTokens,
941
815
  getTokens_next: async () => {
942
- if (getMsBeforeExpiration() <= 5_000) {
943
- await oidc.renewTokens();
816
+ if (getMsBeforeExpiration(currentTokens) <= MIN_RENEW_BEFORE_EXPIRE_MS) {
817
+ await oidc_loggedIn.renewTokens();
944
818
  }
945
819
 
946
820
  return currentTokens;
@@ -989,14 +863,21 @@ export async function createOidc_nonMemoized<
989
863
  } catch (error) {
990
864
  assert(is<Error>(error));
991
865
 
992
- if (error.message !== "No end session endpoint") {
993
- throw error;
994
- }
866
+ if (error.message === "No end session endpoint") {
867
+ log?.("No end session endpoint, managing logging state locally");
995
868
 
996
- log?.("No end session endpoint, managing logging state locally");
869
+ persistAuthState({ configId, state: "explicitly logged out" });
997
870
 
998
- persistLogoutState({ configId });
999
- window.location.href = postLogoutRedirectUrl;
871
+ try {
872
+ await oidcClientTsUserManager.removeUser();
873
+ } catch {
874
+ // NOTE: Not sure if it can throw
875
+ }
876
+
877
+ window.location.href = postLogoutRedirectUrl;
878
+ } else {
879
+ throw error;
880
+ }
1000
881
  }
1001
882
 
1002
883
  return new Promise<never>(() => {});
@@ -1021,21 +902,53 @@ export async function createOidc_nonMemoized<
1021
902
  let oidcClientTsUser: OidcClientTsUser;
1022
903
 
1023
904
  switch (result_loginSilent.outcome) {
1024
- case "refresh token used":
905
+ case "token refreshed using refresh token":
1025
906
  {
1026
907
  log?.("Refresh token used");
1027
908
  oidcClientTsUser = result_loginSilent.oidcClientTsUser;
1028
909
  }
1029
910
  break;
1030
- case "success iframe":
911
+ case "got auth response from iframe":
1031
912
  {
1032
913
  const { authResponse } = result_loginSilent;
1033
914
 
1034
915
  log?.("Tokens refresh using iframe", authResponse);
1035
916
 
1036
- oidcClientTsUser = await oidcClientTsUserManager.signinRedirectCallback(
1037
- authResponseToUrl(authResponse)
1038
- );
917
+ const authResponse_error: string | undefined = authResponse["error"];
918
+
919
+ let oidcClientTsUser_scope: OidcClientTsUser | undefined = undefined;
920
+
921
+ try {
922
+ oidcClientTsUser_scope =
923
+ await oidcClientTsUserManager.signinRedirectCallback(
924
+ authResponseToUrl(authResponse)
925
+ );
926
+ } catch (error) {
927
+ assert(error instanceof Error);
928
+
929
+ if (authResponse_error === undefined) {
930
+ throw error;
931
+ }
932
+
933
+ oidcClientTsUser_scope = undefined;
934
+ }
935
+
936
+ if (oidcClientTsUser_scope === undefined) {
937
+ persistAuthState({ configId, state: undefined });
938
+
939
+ await loginOrGoToAuthServer({
940
+ action: "login",
941
+ redirectUrl: window.location.href,
942
+ doForceReloadOnBfCache: true,
943
+ extraQueryParams_local: undefined,
944
+ transformUrlBeforeRedirect_local: undefined,
945
+ doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack: false,
946
+ doForceInteraction: false
947
+ });
948
+ assert(false);
949
+ }
950
+
951
+ oidcClientTsUser = oidcClientTsUser_scope;
1039
952
  }
1040
953
  break;
1041
954
  default:
@@ -1134,7 +1047,13 @@ export async function createOidc_nonMemoized<
1134
1047
 
1135
1048
  return { unsubscribeFromAutoLogoutCountdown };
1136
1049
  },
1137
- goToAuthServer: params => loginOrGoToAuthServer({ action: "go to auth server", ...params }),
1050
+ goToAuthServer: ({ extraQueryParams, redirectUrl, transformUrlBeforeRedirect }) =>
1051
+ loginOrGoToAuthServer({
1052
+ action: "go to auth server",
1053
+ redirectUrl: redirectUrl ?? window.location.href,
1054
+ extraQueryParams_local: extraQueryParams,
1055
+ transformUrlBeforeRedirect_local: transformUrlBeforeRedirect
1056
+ }),
1138
1057
  backFromAuthServer: resultOfLoginProcess.backFromAuthServer,
1139
1058
  isNewBrowserSession: (() => {
1140
1059
  if (sessionStorage.getItem(BROWSER_SESSION_NOT_FIRST_INIT_KEY) === null) {
@@ -1167,21 +1086,25 @@ export async function createOidc_nonMemoized<
1167
1086
  }
1168
1087
 
1169
1088
  (function scheduleRenew() {
1170
- const msBeforeExpiration = getMsBeforeExpiration();
1171
-
1172
- // NOTE: Here semantically `"doesCurrentHrefRequiresAuth": false` is wrong.
1173
- // The user may very well be on a page that require auth.
1174
- // However there's no way to enforce the browser to redirect back to
1175
- // the last public route if the user press back on the login page.
1176
- // This is due to the fact that pushing to history only works if it's
1177
- // triggered by a user interaction.
1178
- const login_dueToExpiration = () =>
1179
- loginOrGoToAuthServer({
1089
+ const login_dueToExpiration = () => {
1090
+ persistAuthState({ configId, state: undefined });
1091
+
1092
+ return loginOrGoToAuthServer({
1180
1093
  action: "login",
1181
- doesCurrentHrefRequiresAuth: false
1094
+ redirectUrl: window.location.href,
1095
+ doForceReloadOnBfCache: true,
1096
+ extraQueryParams_local: undefined,
1097
+ transformUrlBeforeRedirect_local: undefined,
1098
+ // NOTE: Wether or not it's the preferred behavior, pushing to history
1099
+ // only works on user interaction so it have to be false
1100
+ doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack: false,
1101
+ doForceInteraction: true
1182
1102
  });
1103
+ };
1183
1104
 
1184
- if (msBeforeExpiration <= 2_000) {
1105
+ const msBeforeExpiration = getMsBeforeExpiration(currentTokens);
1106
+
1107
+ if (msBeforeExpiration <= MIN_RENEW_BEFORE_EXPIRE_MS) {
1185
1108
  // NOTE: We just got a new token that is about to expire. This means that
1186
1109
  // the refresh token has reached it's max SSO time.
1187
1110
  login_dueToExpiration();
@@ -1191,7 +1114,10 @@ export async function createOidc_nonMemoized<
1191
1114
  // NOTE: We refresh the token 25 seconds before it expires.
1192
1115
  // If the token expiration time is less than 25 seconds we refresh the token when
1193
1116
  // only 1/10 of the token time is left.
1194
- const renewMsBeforeExpires = Math.min(25_000, msBeforeExpiration * 0.1);
1117
+ const renewMsBeforeExpires = Math.max(
1118
+ Math.min(25_000, msBeforeExpiration * 0.1),
1119
+ MIN_RENEW_BEFORE_EXPIRE_MS
1120
+ );
1195
1121
 
1196
1122
  log?.(
1197
1123
  [
@@ -1209,13 +1135,13 @@ export async function createOidc_nonMemoized<
1209
1135
  );
1210
1136
 
1211
1137
  try {
1212
- await oidc.renewTokens();
1138
+ await oidc_loggedIn.renewTokens();
1213
1139
  } catch {
1214
1140
  await login_dueToExpiration();
1215
1141
  }
1216
1142
  }, msBeforeExpiration - renewMsBeforeExpires);
1217
1143
 
1218
- const { unsubscribe: tokenChangeUnsubscribe } = oidc.subscribeToTokensChange(() => {
1144
+ const { unsubscribe: tokenChangeUnsubscribe } = oidc_loggedIn.subscribeToTokensChange(() => {
1219
1145
  clearTimeout(timer);
1220
1146
  tokenChangeUnsubscribe();
1221
1147
  scheduleRenew();
@@ -1260,7 +1186,7 @@ export async function createOidc_nonMemoized<
1260
1186
  );
1261
1187
 
1262
1188
  if (secondsLeft === 0) {
1263
- oidc.logout(autoLogoutParams);
1189
+ oidc_loggedIn.logout(autoLogoutParams);
1264
1190
  }
1265
1191
  }
1266
1192
  });
@@ -1287,5 +1213,17 @@ export async function createOidc_nonMemoized<
1287
1213
  });
1288
1214
  }
1289
1215
 
1290
- return oidc;
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
+ return oidc_loggedIn;
1291
1229
  }