oidc-spa 8.2.2 → 8.2.3

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 (61) hide show
  1. package/core/createOidc.d.ts +48 -16
  2. package/core/createOidc.js +22 -11
  3. package/core/createOidc.js.map +1 -1
  4. package/core/desiredPostLoginRedirectUrl.d.ts +4 -0
  5. package/core/desiredPostLoginRedirectUrl.js +12 -0
  6. package/core/desiredPostLoginRedirectUrl.js.map +1 -0
  7. package/core/diagnostic.d.ts +1 -1
  8. package/core/diagnostic.js +3 -3
  9. package/core/diagnostic.js.map +1 -1
  10. package/esm/angular.d.ts +27 -4
  11. package/esm/angular.js +28 -6
  12. package/esm/angular.js.map +1 -1
  13. package/esm/core/createOidc.d.ts +48 -16
  14. package/esm/core/createOidc.js +22 -11
  15. package/esm/core/createOidc.js.map +1 -1
  16. package/esm/core/desiredPostLoginRedirectUrl.d.ts +4 -0
  17. package/esm/core/desiredPostLoginRedirectUrl.js +8 -0
  18. package/esm/core/desiredPostLoginRedirectUrl.js.map +1 -0
  19. package/esm/core/diagnostic.d.ts +1 -1
  20. package/esm/core/diagnostic.js +3 -3
  21. package/esm/core/diagnostic.js.map +1 -1
  22. package/esm/keycloak/keycloak-js/Keycloak.d.ts +40 -0
  23. package/esm/keycloak/keycloak-js/Keycloak.js +2 -1
  24. package/esm/keycloak/keycloak-js/Keycloak.js.map +1 -1
  25. package/esm/react/react.js +24 -2
  26. package/esm/react/react.js.map +1 -1
  27. package/esm/react-spa/createOidcSpaApi.js +26 -4
  28. package/esm/react-spa/createOidcSpaApi.js.map +1 -1
  29. package/esm/react-spa/types.d.ts +26 -3
  30. package/esm/tanstack-start/react/createOidcSpaApi.js +25 -3
  31. package/esm/tanstack-start/react/createOidcSpaApi.js.map +1 -1
  32. package/esm/tanstack-start/react/types.d.ts +26 -3
  33. package/esm/tools/lazySessionStorage.d.ts +3 -1
  34. package/esm/tools/lazySessionStorage.js +8 -6
  35. package/esm/tools/lazySessionStorage.js.map +1 -1
  36. package/keycloak/keycloak-js/Keycloak.d.ts +40 -0
  37. package/keycloak/keycloak-js/Keycloak.js +2 -1
  38. package/keycloak/keycloak-js/Keycloak.js.map +1 -1
  39. package/package.json +5 -1
  40. package/react/react.js +24 -2
  41. package/react/react.js.map +1 -1
  42. package/react-spa/createOidcSpaApi.js +26 -4
  43. package/react-spa/createOidcSpaApi.js.map +1 -1
  44. package/react-spa/types.d.ts +26 -3
  45. package/src/angular.ts +72 -18
  46. package/src/core/createOidc.ts +73 -27
  47. package/src/core/desiredPostLoginRedirectUrl.ts +9 -0
  48. package/src/core/diagnostic.ts +4 -4
  49. package/src/keycloak/keycloak-js/Keycloak.ts +43 -1
  50. package/src/react/react.tsx +32 -3
  51. package/src/react-spa/createOidcSpaApi.tsx +34 -5
  52. package/src/react-spa/types.tsx +26 -3
  53. package/src/tanstack-start/react/createOidcSpaApi.tsx +33 -4
  54. package/src/tanstack-start/react/types.tsx +26 -3
  55. package/src/tools/lazySessionStorage.ts +11 -7
  56. package/src/vite-plugin/manageOptimizedDeps.ts +2 -1
  57. package/tools/lazySessionStorage.d.ts +3 -1
  58. package/tools/lazySessionStorage.js +8 -6
  59. package/tools/lazySessionStorage.js.map +1 -1
  60. package/vite-plugin/manageOptimizedDeps.js +2 -1
  61. package/vite-plugin/manageOptimizedDeps.js.map +1 -1
@@ -52,6 +52,7 @@ import {
52
52
  evtIsThereMoreThanOneInstanceThatCantUserIframes,
53
53
  notifyNewInstanceThatCantUseIframes
54
54
  } from "./instancesThatCantUseIframes";
55
+ import { getDesiredPostLoginRedirectUrl } from "./desiredPostLoginRedirectUrl";
55
56
 
56
57
  // NOTE: Replaced at build time
57
58
  const VERSION = "{{OIDC_SPA_VERSION}}";
@@ -106,21 +107,6 @@ export type ParamsOfCreateOidc<
106
107
  * extraTokenParams: { selectedCustomer: "xxx" }
107
108
  */
108
109
  extraTokenParams?: Record<string, string | undefined> | (() => Record<string, string | undefined>);
109
- /**
110
- * @deprecated: Use login({ redirectUrl: "..." }) instead.
111
- *
112
- * Usage discouraged, it's here because we don't want to assume too much on your
113
- * usecase but I can't think of a scenario where you would want anything
114
- * other than the current page.
115
- *
116
- * Where to redirect after successful login.
117
- * Default: window.location.href (here)
118
- *
119
- * It does not need to include the origin, eg: "/dashboard"
120
- *
121
- * This parameter can also be passed to login() directly as `redirectUrl`.
122
- */
123
- postLoginRedirectUrl?: string;
124
110
 
125
111
  decodedIdTokenSchema?: {
126
112
  parse: (decodedIdToken_original: Oidc.Tokens.DecodedIdToken_OidcCoreSpec) => DecodedIdToken;
@@ -150,9 +136,43 @@ export type ParamsOfCreateOidc<
150
136
  autoLogin?: AutoLogin;
151
137
 
152
138
  /**
139
+ * Determines how session restoration is handled.
140
+ * Session restoration allows users to stay logged in between visits
141
+ * without needing to explicitly sign in each time.
142
+ *
143
+ * Options:
144
+ *
145
+ * - **"auto" (default)**:
146
+ * Automatically selects the best method.
147
+ * If the app’s domain shares a common parent domain with the authorization endpoint,
148
+ * an iframe is used for silent session restoration.
149
+ * Otherwise, a full-page redirect is used.
150
+ *
151
+ * - **"full page redirect"**:
152
+ * Forces full-page reloads for session restoration.
153
+ * Use this if your application is served with a restrictive CSP
154
+ * (e.g., `Content-Security-Policy: frame-ancestors "none"`)
155
+ * or `X-Frame-Options: DENY`, and you cannot modify those headers.
156
+ * This mode provides a slightly less seamless UX and will lead oidc-spa to
157
+ * store tokens in `localStorage` if multiple OIDC clients are used
158
+ * (e.g., your app communicates with several APIs).
159
+ *
160
+ * - **"iframe"**:
161
+ * Forces iframe-based session restoration.
162
+ * In development, if you go in your browser setting and allow your auth server’s domain
163
+ * to set third-party cookies this value will let you test your app
164
+ * with the local dev server as it will behave in production.
165
+ *
166
+ * See: https://docs.oidc-spa.dev/v/v8/resources/third-party-cookies-and-session-restoration
167
+ */
168
+ sessionRestorationMethod?: "iframe" | "full page redirect" | "auto";
169
+
170
+ /**
171
+ * @deprecated Use `sessionRestorationMethod: "full page redirect"` instead.
172
+ *
153
173
  * Default: false
154
174
  *
155
- * See: https://docs.oidc-spa.dev/v/v8/resources/iframe-related-issues
175
+ * See: https://docs.oidc-spa.dev/v/v8/resources/third-party-cookies-and-session-restoration
156
176
  */
157
177
  noIframe?: boolean;
158
178
 
@@ -205,6 +225,21 @@ export type ParamsOfCreateOidc<
205
225
 
206
226
  /** @deprecated: Use BASE_URL (same thing, just renamed). */
207
227
  homeUrl?: string;
228
+
229
+ /**
230
+ * This parameter is irrelevant in most usecases.
231
+ * It tells where to redirect after a successful login or autoLogin.
232
+ *
233
+ * If you are not in autoLogin mode there is absolutely no reason to use
234
+ * this parameter since you can pass `login({ redirectUrl: "..." })`.
235
+ *
236
+ * It can only be useful in some edge case with `autoLogin: true`
237
+ * When you want to precisely redirect somewhere after login.
238
+ *
239
+ * This can make sense if you have multiple clients to talk with different
240
+ * API and no iframe capabilities.
241
+ */
242
+ postLoginRedirectUrl?: string;
208
243
  };
209
244
 
210
245
  const globalContext = {
@@ -338,8 +373,8 @@ export async function createOidc_nonMemoized<
338
373
  __unsafe_clientSecret,
339
374
  __unsafe_useIdTokenAsAccessToken = false,
340
375
  __metadata,
341
- noIframe = false,
342
- scopes = ["openid", "profile"]
376
+ scopes = ["openid", "profile"],
377
+ sessionRestorationMethod = params.autoLogin === true ? "full page redirect" : "auto"
343
378
  } = params;
344
379
 
345
380
  const BASE_URL_params = params.BASE_URL ?? params.homeUrl;
@@ -416,8 +451,15 @@ export async function createOidc_nonMemoized<
416
451
  const oidcMetadata = __metadata ?? (await fetchOidcMetadata({ issuerUri }));
417
452
 
418
453
  const canUseIframe = (() => {
419
- if (noIframe) {
420
- return false;
454
+ switch (sessionRestorationMethod) {
455
+ case "auto":
456
+ break;
457
+ case "full page redirect":
458
+ return false;
459
+ case "iframe":
460
+ return true;
461
+ default:
462
+ assert<Equals<typeof sessionRestorationMethod, never>>;
421
463
  }
422
464
 
423
465
  third_party_cookies: {
@@ -525,7 +567,7 @@ export async function createOidc_nonMemoized<
525
567
  ];
526
568
  })(),
527
569
  "\n\nMore info:",
528
- "https://docs.oidc-spa.dev/v/v8/resources/end-of-third-party-cookies#when-are-cookies-considered-third-party"
570
+ "https://docs.oidc-spa.dev/v/v8/resources/third-party-cookies-and-session-restoration"
529
571
  ].join(" ")
530
572
  );
531
573
  } else {
@@ -555,7 +597,7 @@ export async function createOidc_nonMemoized<
555
597
  ];
556
598
  })(),
557
599
  "\nMore info:",
558
- "https://docs.oidc-spa.dev/v/v8/resources/end-of-third-party-cookies#when-are-cookies-considered-third-party"
600
+ "https://docs.oidc-spa.dev/v/v8/resources/third-party-cookies-and-session-restoration"
559
601
  ].join(" ")
560
602
  );
561
603
  }
@@ -599,7 +641,7 @@ export async function createOidc_nonMemoized<
599
641
  return new InMemoryWebStorage();
600
642
  }
601
643
 
602
- const storage = createLazySessionStorage();
644
+ const storage = createLazySessionStorage({ storageId: configId });
603
645
 
604
646
  if (evtIsThereMoreThanOneInstanceThatCantUserIframes.current) {
605
647
  storage.persistCurrentStateAndSubsequentChanges();
@@ -896,7 +938,7 @@ export async function createOidc_nonMemoized<
896
938
  redirectUri: homeUrlAndRedirectUri,
897
939
  clientId,
898
940
  issuerUri,
899
- noIframe
941
+ canUseIframe
900
942
  });
901
943
  }
902
944
 
@@ -957,11 +999,15 @@ export async function createOidc_nonMemoized<
957
999
  action: "login",
958
1000
  doForceReloadOnBfCache: true,
959
1001
  redirectUrl: (() => {
960
- if (evtIsThereMoreThanOneInstanceThatCantUserIframes.current) {
961
- return window.location.href;
1002
+ if (postLoginRedirectUrl_default) {
1003
+ return postLoginRedirectUrl_default;
1004
+ }
1005
+
1006
+ if (!evtIsThereMoreThanOneInstanceThatCantUserIframes.current) {
1007
+ return getRootRelativeOriginalLocationHref();
962
1008
  }
963
1009
 
964
- return getRootRelativeOriginalLocationHref();
1010
+ return getDesiredPostLoginRedirectUrl() ?? window.location.href;
965
1011
  })(),
966
1012
  // NOTE: Wether or not it's the preferred behavior, pushing to history
967
1013
  // only works on user interaction so it have to be false
@@ -0,0 +1,9 @@
1
+ let postLoginRedirectUrl: string | undefined;
2
+
3
+ export function setDesiredPostLoginRedirectUrl(params: { postLoginRedirectUrl: string | undefined }) {
4
+ postLoginRedirectUrl = params.postLoginRedirectUrl;
5
+ }
6
+
7
+ export function getDesiredPostLoginRedirectUrl() {
8
+ return postLoginRedirectUrl;
9
+ }
@@ -90,9 +90,9 @@ export async function createIframeTimeoutInitializationError(params: {
90
90
  redirectUri: string;
91
91
  issuerUri: string;
92
92
  clientId: string;
93
- noIframe: boolean;
93
+ canUseIframe: boolean;
94
94
  }): Promise<OidcInitializationError> {
95
- const { redirectUri, issuerUri, clientId, noIframe } = params;
95
+ const { redirectUri, issuerUri, clientId, canUseIframe } = params;
96
96
 
97
97
  check_if_well_known_endpoint_is_reachable: {
98
98
  const isValid = await getIsValidRemoteJson(`${issuerUri}${WELL_KNOWN_PATH}`);
@@ -105,7 +105,7 @@ export async function createIframeTimeoutInitializationError(params: {
105
105
  }
106
106
 
107
107
  iframe_blocked: {
108
- if (noIframe) {
108
+ if (!canUseIframe) {
109
109
  break iframe_blocked;
110
110
  }
111
111
 
@@ -189,7 +189,7 @@ export async function createIframeTimeoutInitializationError(params: {
189
189
  messageOrCause: [
190
190
  `${redirectUri} is currently served by your web server with the HTTP header \`${key_problem}: ${headers[key_problem]}\`.\n`,
191
191
  "This header prevents the silent sign-in process from working.\n",
192
- "Refer to this documentation page to fix this issue: https://docs.oidc-spa.dev/v/v8/resources/iframe-related-issues"
192
+ "Refer to this documentation page to fix this issue: https://docs.oidc-spa.dev/v/v8/resources/third-party-cookies-and-session-restoration"
193
193
  ].join(" ")
194
194
  });
195
195
  }
@@ -23,7 +23,48 @@ import { type StatefulEvt, createStatefulEvt } from "../../tools/StatefulEvt";
23
23
  import { readExpirationTimeInJwt } from "../../tools/readExpirationTimeInJwt";
24
24
 
25
25
  type ConstructorParams = KeycloakServerConfig & {
26
+ /**
27
+ * NOTE: This parameter is optional if you use the Vite plugin.
28
+ *
29
+ * This parameter let's you overwrite the value provided in
30
+ * oidcEarlyInit({ BASE_URL: xxx });
31
+ *
32
+ * What should you put in this parameter?
33
+ * - Vite project: `BASE_URL: import.meta.env.BASE_URL`
34
+ * - Create React App project: `BASE_URL: process.env.PUBLIC_URL`
35
+ * - Other: `BASE_URL: "/"` (Usually, or `/dashboard` if your app is not at the root of the domain)
36
+ */
26
37
  BASE_URL?: string;
38
+
39
+ /**
40
+ * Determines how session restoration is handled.
41
+ * Session restoration allows users to stay logged in between visits
42
+ * without needing to explicitly sign in each time.
43
+ *
44
+ * Options:
45
+ *
46
+ * - **"auto" (default)**:
47
+ * Automatically selects the best method.
48
+ * If the app’s domain shares a common parent domain with the authorization endpoint,
49
+ * an iframe is used for silent session restoration.
50
+ * Otherwise, a full-page redirect is used.
51
+ *
52
+ * - **"full page redirect"**:
53
+ * Forces full-page reloads for session restoration.
54
+ * Use this if your application is served with a restrictive CSP
55
+ * (e.g., `Content-Security-Policy: frame-ancestors "none"`)
56
+ * or `X-Frame-Options: DENY`, and you cannot modify those headers.
57
+ * This mode provides a slightly less seamless UX and will lead oidc-spa to
58
+ * store tokens in `localStorage` if multiple OIDC clients are used
59
+ * (e.g., your app communicates with several APIs).
60
+ *
61
+ * - **"iframe"**:
62
+ * Forces iframe-based session restoration.
63
+ * In development, if you go in your browser setting and allow your auth server’s domain
64
+ * to set third-party cookies this value will let you test your app
65
+ * with the local dev server as it will behave in production.
66
+ */
67
+ sessionRestorationMethod?: "iframe" | "full page redirect" | "auto";
27
68
  };
28
69
 
29
70
  /**
@@ -99,7 +140,8 @@ export class Keycloak {
99
140
  let hasCreateResolved = false;
100
141
 
101
142
  const oidcOrError = await createOidc({
102
- homeUrl: constructorParams.BASE_URL,
143
+ BASE_URL: constructorParams.BASE_URL,
144
+ sessionRestorationMethod: constructorParams.sessionRestorationMethod,
103
145
  issuerUri,
104
146
  clientId: this.#state.constructorParams.clientId,
105
147
  autoLogin,
@@ -15,6 +15,7 @@ import { id } from "../tools/tsafe/id";
15
15
  import type { ValueOrAsyncGetter } from "../tools/ValueOrAsyncGetter";
16
16
  import { Deferred } from "../tools/Deferred";
17
17
  import { toFullyQualifiedUrl } from "../tools/toFullyQualifiedUrl";
18
+ import { setDesiredPostLoginRedirectUrl } from "../core/desiredPostLoginRedirectUrl";
18
19
 
19
20
  export type OidcReact<DecodedIdToken extends Record<string, unknown>> =
20
21
  | OidcReact.NotLoggedIn
@@ -409,20 +410,48 @@ export function createReactOidc_dependencyInjection<
409
410
 
410
411
  const oidc = await getOidc();
411
412
 
413
+ const isUrlAlreadyReplaced =
414
+ window.location.href.replace(/\/$/, "") === redirectUrl.replace(/\/$/, "");
415
+
412
416
  if (!oidc.isUserLoggedIn) {
413
417
  if (cause === "preload") {
414
418
  throw new Error(
415
419
  "oidc-spa: User is not yet logged in. This is an expected error, nothing to be addressed."
416
420
  );
417
421
  }
418
- const doesCurrentHrefRequiresAuth =
419
- location.href.replace(/\/$/, "") === redirectUrl.replace(/\/$/, "");
420
422
 
421
423
  await oidc.login({
422
424
  redirectUrl,
423
- doesCurrentHrefRequiresAuth
425
+ doesCurrentHrefRequiresAuth: isUrlAlreadyReplaced
424
426
  });
425
427
  }
428
+
429
+ define_temporary_postLoginRedirectUrl: {
430
+ if (isUrlAlreadyReplaced) {
431
+ break define_temporary_postLoginRedirectUrl;
432
+ }
433
+
434
+ setDesiredPostLoginRedirectUrl({ postLoginRedirectUrl: redirectUrl });
435
+
436
+ const history_pushState = history.pushState;
437
+ const history_replaceState = history.replaceState;
438
+
439
+ const onNavigated = () => {
440
+ history.pushState = history_pushState;
441
+ history.replaceState = history_replaceState;
442
+ setDesiredPostLoginRedirectUrl({ postLoginRedirectUrl: undefined });
443
+ };
444
+
445
+ history.pushState = function pushState(...args) {
446
+ onNavigated();
447
+ return history_pushState.call(history, ...args);
448
+ };
449
+
450
+ history.replaceState = function replaceState(...args) {
451
+ onNavigated();
452
+ return history_replaceState.call(history, ...args);
453
+ };
454
+ }
426
455
  }
427
456
 
428
457
  async function getOidc(): Promise<Oidc<DecodedIdToken>> {
@@ -10,6 +10,7 @@ import { createObjectThatThrowsIfAccessed } from "../tools/createObjectThatThrow
10
10
  import { createStatefulEvt } from "../tools/StatefulEvt";
11
11
  import { id } from "../tools/tsafe/id";
12
12
  import { toFullyQualifiedUrl } from "../tools/toFullyQualifiedUrl";
13
+ import { setDesiredPostLoginRedirectUrl } from "../core/desiredPostLoginRedirectUrl";
13
14
 
14
15
  export function createOidcSpaApi<
15
16
  AutoLogin extends boolean,
@@ -390,7 +391,7 @@ export function createOidcSpaApi<
390
391
  transformUrlBeforeRedirect: paramsOfBootstrap.transformUrlBeforeRedirect,
391
392
  extraQueryParams: paramsOfBootstrap.extraQueryParams,
392
393
  extraTokenParams: paramsOfBootstrap.extraTokenParams,
393
- noIframe: paramsOfBootstrap.noIframe,
394
+ sessionRestorationMethod: paramsOfBootstrap.sessionRestorationMethod,
394
395
  debugLogs: paramsOfBootstrap.debugLogs,
395
396
  __unsafe_clientSecret: paramsOfBootstrap.__unsafe_clientSecret,
396
397
  __metadata: paramsOfBootstrap.__metadata,
@@ -440,25 +441,53 @@ export function createOidcSpaApi<
440
441
  });
441
442
  }
442
443
 
443
- return location.href;
444
+ return window.location.href;
444
445
  })();
445
446
 
446
447
  const oidc = await getOidc();
447
448
 
449
+ const isUrlAlreadyReplaced =
450
+ window.location.href.replace(/\/$/, "") === redirectUrl.replace(/\/$/, "");
451
+
448
452
  if (!oidc.isUserLoggedIn) {
449
453
  if (cause === "preload") {
450
454
  throw new Error(
451
455
  "oidc-spa: User is not yet logged in. This is an expected error, nothing to be addressed."
452
456
  );
453
457
  }
454
- const doesCurrentHrefRequiresAuth =
455
- location.href.replace(/\/$/, "") === redirectUrl.replace(/\/$/, "");
456
458
 
457
459
  await oidc.login({
458
460
  redirectUrl,
459
- doesCurrentHrefRequiresAuth
461
+ doesCurrentHrefRequiresAuth: isUrlAlreadyReplaced
460
462
  });
461
463
  }
464
+
465
+ define_temporary_postLoginRedirectUrl: {
466
+ if (isUrlAlreadyReplaced) {
467
+ break define_temporary_postLoginRedirectUrl;
468
+ }
469
+
470
+ setDesiredPostLoginRedirectUrl({ postLoginRedirectUrl: redirectUrl });
471
+
472
+ const history_pushState = history.pushState;
473
+ const history_replaceState = history.replaceState;
474
+
475
+ const onNavigated = () => {
476
+ history.pushState = history_pushState;
477
+ history.replaceState = history_replaceState;
478
+ setDesiredPostLoginRedirectUrl({ postLoginRedirectUrl: undefined });
479
+ };
480
+
481
+ history.pushState = function pushState(...args) {
482
+ onNavigated();
483
+ return history_pushState.call(history, ...args);
484
+ };
485
+
486
+ history.replaceState = function replaceState(...args) {
487
+ onNavigated();
488
+ return history_replaceState.call(history, ...args);
489
+ };
490
+ }
462
491
  }
463
492
 
464
493
  function OidcInitializationErrorGate(props: {
@@ -213,11 +213,34 @@ export namespace ParamsOfBootstrap {
213
213
  | (() => Record<string, string | undefined>);
214
214
 
215
215
  /**
216
- * Default: false
216
+ * Determines how session restoration is handled.
217
+ * Session restoration allows users to stay logged in between visits
218
+ * without needing to explicitly sign in each time.
217
219
  *
218
- * See: https://docs.oidc-spa.dev/v/v8/resources/iframe-related-issues
220
+ * Options:
221
+ *
222
+ * - **"auto" (default)**:
223
+ * Automatically selects the best method.
224
+ * If the app’s domain shares a common parent domain with the authorization endpoint,
225
+ * an iframe is used for silent session restoration.
226
+ * Otherwise, a full-page redirect is used.
227
+ *
228
+ * - **"full page redirect"**:
229
+ * Forces full-page reloads for session restoration.
230
+ * Use this if your application is served with a restrictive CSP
231
+ * (e.g., `Content-Security-Policy: frame-ancestors "none"`)
232
+ * or `X-Frame-Options: DENY`, and you cannot modify those headers.
233
+ * This mode provides a slightly less seamless UX and will lead oidc-spa to
234
+ * store tokens in `localStorage` if multiple OIDC clients are used
235
+ * (e.g., your app communicates with several APIs).
236
+ *
237
+ * - **"iframe"**:
238
+ * Forces iframe-based session restoration.
239
+ * In development, if you go in your browser setting and allow your auth server’s domain
240
+ * to set third-party cookies this value will let you test your app
241
+ * with the local dev server as it will behave in production.
219
242
  */
220
- noIframe?: boolean;
243
+ sessionRestorationMethod?: "iframe" | "full page redirect" | "auto";
221
244
 
222
245
  debugLogs?: boolean;
223
246
 
@@ -30,6 +30,7 @@ import { createServerFn, createMiddleware } from "@tanstack/react-start";
30
30
  import { getRequest, setResponseHeader, setResponseStatus } from "@tanstack/react-start/server";
31
31
  import { toFullyQualifiedUrl } from "../../tools/toFullyQualifiedUrl";
32
32
  import { UnifiedClientRetryForSsrLoadersError } from "./rfcUnifiedClientRetryForSsrLoaders/UnifiedClientRetryForSsrLoadersError";
33
+ import { setDesiredPostLoginRedirectUrl } from "../../core/desiredPostLoginRedirectUrl";
33
34
 
34
35
  export function createOidcSpaApi<
35
36
  AutoLogin extends boolean,
@@ -662,7 +663,7 @@ export function createOidcSpaApi<
662
663
  transformUrlBeforeRedirect: paramsOfBootstrap.transformUrlBeforeRedirect,
663
664
  extraQueryParams: paramsOfBootstrap.extraQueryParams,
664
665
  extraTokenParams: paramsOfBootstrap.extraTokenParams,
665
- noIframe: paramsOfBootstrap.noIframe,
666
+ sessionRestorationMethod: paramsOfBootstrap.sessionRestorationMethod,
666
667
  debugLogs: paramsOfBootstrap.debugLogs,
667
668
  __unsafe_clientSecret: paramsOfBootstrap.__unsafe_clientSecret,
668
669
  __metadata: paramsOfBootstrap.__metadata,
@@ -714,6 +715,9 @@ export function createOidcSpaApi<
714
715
 
715
716
  const oidc = await getOidc();
716
717
 
718
+ const isUrlAlreadyReplaced =
719
+ window.location.href.replace(/\/$/, "") === redirectUrl.replace(/\/$/, "");
720
+
717
721
  if (!oidc.isUserLoggedIn) {
718
722
  if (cause === "preload") {
719
723
  throw new Error(
@@ -724,14 +728,39 @@ export function createOidcSpaApi<
724
728
  ].join(" ")
725
729
  );
726
730
  }
727
- const doesCurrentHrefRequiresAuth =
728
- location.href.replace(/\/$/, "") === redirectUrl.replace(/\/$/, "");
729
731
 
730
732
  await oidc.login({
731
733
  redirectUrl,
732
- doesCurrentHrefRequiresAuth
734
+ doesCurrentHrefRequiresAuth: isUrlAlreadyReplaced
733
735
  });
734
736
  }
737
+
738
+ define_temporary_postLoginRedirectUrl: {
739
+ if (isUrlAlreadyReplaced) {
740
+ break define_temporary_postLoginRedirectUrl;
741
+ }
742
+
743
+ setDesiredPostLoginRedirectUrl({ postLoginRedirectUrl: redirectUrl });
744
+
745
+ const history_pushState = history.pushState;
746
+ const history_replaceState = history.replaceState;
747
+
748
+ const onNavigated = () => {
749
+ history.pushState = history_pushState;
750
+ history.replaceState = history_replaceState;
751
+ setDesiredPostLoginRedirectUrl({ postLoginRedirectUrl: undefined });
752
+ };
753
+
754
+ history.pushState = function pushState(...args) {
755
+ onNavigated();
756
+ return history_pushState.call(history, ...args);
757
+ };
758
+
759
+ history.replaceState = function replaceState(...args) {
760
+ onNavigated();
761
+ return history_replaceState.call(history, ...args);
762
+ };
763
+ }
735
764
  }
736
765
 
737
766
  enforceLogin.__isOidcSpaEnforceLogin = true;
@@ -306,11 +306,34 @@ export namespace ParamsOfBootstrap {
306
306
  | (() => Record<string, string | undefined>);
307
307
 
308
308
  /**
309
- * Default: false
309
+ * Determines how session restoration is handled.
310
+ * Session restoration allows users to stay logged in between visits
311
+ * without needing to explicitly sign in each time.
310
312
  *
311
- * See: https://docs.oidc-spa.dev/v/v8/resources/iframe-related-issues
313
+ * Options:
314
+ *
315
+ * - **"auto" (default)**:
316
+ * Automatically selects the best method.
317
+ * If the app’s domain shares a common parent domain with the authorization endpoint,
318
+ * an iframe is used for silent session restoration.
319
+ * Otherwise, a full-page redirect is used.
320
+ *
321
+ * - **"full page redirect"**:
322
+ * Forces full-page reloads for session restoration.
323
+ * Use this if your application is served with a restrictive CSP
324
+ * (e.g., `Content-Security-Policy: frame-ancestors "none"`)
325
+ * or `X-Frame-Options: DENY`, and you cannot modify those headers.
326
+ * This mode provides a slightly less seamless UX and will lead oidc-spa to
327
+ * store tokens in `localStorage` if multiple OIDC clients are used
328
+ * (e.g., your app communicates with several APIs).
329
+ *
330
+ * - **"iframe"**:
331
+ * Forces iframe-based session restoration.
332
+ * In development, if you go in your browser setting and allow your auth server’s domain
333
+ * to set third-party cookies this value will let you test your app
334
+ * with the local dev server as it will behave in production.
312
335
  */
313
- noIframe?: boolean;
336
+ sessionRestorationMethod?: "iframe" | "full page redirect" | "auto";
314
337
 
315
338
  debugLogs?: boolean;
316
339
 
@@ -1,7 +1,5 @@
1
1
  import { assert } from "../tools/tsafe/assert";
2
2
 
3
- const SESSION_STORAGE_PREFIX = "lazy-session-storage:";
4
-
5
3
  export type LazySessionStorage = {
6
4
  // `Storage` methods, we don't use the type directly because it has [name: string]: any;
7
5
  readonly length: number;
@@ -15,14 +13,20 @@ export type LazySessionStorage = {
15
13
  persistCurrentStateAndSubsequentChanges: () => void;
16
14
  };
17
15
 
18
- export function createLazySessionStorage(): LazySessionStorage {
16
+ export function createLazySessionStorage(params: { storageId: string }): LazySessionStorage {
17
+ const { storageId } = params;
18
+
19
+ const sessionStoragePrefix = `lazy-session-storage:${storageId}:`;
20
+
21
+ const getSessionStorageKey = (key: string) => `${sessionStoragePrefix}${key}`;
22
+
19
23
  const entries: { key: string; value: string }[] = [];
20
24
 
21
25
  for (let i = 0; i < sessionStorage.length; i++) {
22
26
  const key = sessionStorage.key(i);
23
27
  assert(key !== null, "470498");
24
28
 
25
- if (!key.startsWith(SESSION_STORAGE_PREFIX)) {
29
+ if (!key.startsWith(sessionStoragePrefix)) {
26
30
  continue;
27
31
  }
28
32
 
@@ -33,7 +37,7 @@ export function createLazySessionStorage(): LazySessionStorage {
33
37
  sessionStorage.removeItem(key);
34
38
 
35
39
  entries.push({
36
- key: key.slice(SESSION_STORAGE_PREFIX.length),
40
+ key: key.slice(sessionStoragePrefix.length),
37
41
  value
38
42
  });
39
43
  }
@@ -74,7 +78,7 @@ export function createLazySessionStorage(): LazySessionStorage {
74
78
  return;
75
79
  }
76
80
 
77
- sessionStorage.removeItem(`${SESSION_STORAGE_PREFIX}${entry.key}`);
81
+ sessionStorage.removeItem(getSessionStorageKey(entry.key));
78
82
 
79
83
  const index = entries.indexOf(entry);
80
84
 
@@ -96,7 +100,7 @@ export function createLazySessionStorage(): LazySessionStorage {
96
100
  },
97
101
  setItem: (key, value) => {
98
102
  if (isPersistenceEnabled) {
99
- sessionStorage.setItem(`${SESSION_STORAGE_PREFIX}${key}`, value);
103
+ sessionStorage.setItem(getSessionStorageKey(key), value);
100
104
  }
101
105
 
102
106
  update: {
@@ -34,7 +34,8 @@ export function manageOptimizedDeps(params: {
34
34
  const moduleNames_include = [
35
35
  "oidc-spa/react-spa",
36
36
  "oidc-spa/entrypoint",
37
- "oidc-spa/keycloak"
37
+ "oidc-spa/keycloak",
38
+ "oidc-spa/core"
38
39
  ];
39
40
 
40
41
  for (const moduleName of moduleNames_include) {
@@ -7,4 +7,6 @@ export type LazySessionStorage = {
7
7
  setItem(key: string, value: string): void;
8
8
  persistCurrentStateAndSubsequentChanges: () => void;
9
9
  };
10
- export declare function createLazySessionStorage(): LazySessionStorage;
10
+ export declare function createLazySessionStorage(params: {
11
+ storageId: string;
12
+ }): LazySessionStorage;