oidc-spa 8.2.2 → 8.2.4

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 (99) hide show
  1. package/core/Oidc.d.ts +1 -0
  2. package/core/createOidc.d.ts +48 -16
  3. package/core/createOidc.js +27 -36
  4. package/core/createOidc.js.map +1 -1
  5. package/core/desiredPostLoginRedirectUrl.d.ts +4 -0
  6. package/core/desiredPostLoginRedirectUrl.js +12 -0
  7. package/core/desiredPostLoginRedirectUrl.js.map +1 -0
  8. package/core/diagnostic.d.ts +1 -1
  9. package/core/diagnostic.js +3 -3
  10. package/core/diagnostic.js.map +1 -1
  11. package/core/homeAndRedirectUri.d.ts +5 -0
  12. package/core/homeAndRedirectUri.js +32 -0
  13. package/core/homeAndRedirectUri.js.map +1 -0
  14. package/esm/angular.d.ts +28 -4
  15. package/esm/angular.js +31 -6
  16. package/esm/angular.js.map +1 -1
  17. package/esm/core/Oidc.d.ts +1 -0
  18. package/esm/core/createOidc.d.ts +48 -16
  19. package/esm/core/createOidc.js +27 -36
  20. package/esm/core/createOidc.js.map +1 -1
  21. package/esm/core/desiredPostLoginRedirectUrl.d.ts +4 -0
  22. package/esm/core/desiredPostLoginRedirectUrl.js +8 -0
  23. package/esm/core/desiredPostLoginRedirectUrl.js.map +1 -0
  24. package/esm/core/diagnostic.d.ts +1 -1
  25. package/esm/core/diagnostic.js +3 -3
  26. package/esm/core/diagnostic.js.map +1 -1
  27. package/esm/core/homeAndRedirectUri.d.ts +5 -0
  28. package/esm/core/homeAndRedirectUri.js +29 -0
  29. package/esm/core/homeAndRedirectUri.js.map +1 -0
  30. package/esm/keycloak/keycloak-js/Keycloak.d.ts +40 -0
  31. package/esm/keycloak/keycloak-js/Keycloak.js +13 -3
  32. package/esm/keycloak/keycloak-js/Keycloak.js.map +1 -1
  33. package/esm/keycloak/keycloakUtils.d.ts +3 -1
  34. package/esm/keycloak/keycloakUtils.js +26 -5
  35. package/esm/keycloak/keycloakUtils.js.map +1 -1
  36. package/esm/mock/oidc.js +2 -1
  37. package/esm/mock/oidc.js.map +1 -1
  38. package/esm/react/react.js +24 -2
  39. package/esm/react/react.js.map +1 -1
  40. package/esm/react-spa/apiBuilder.d.ts +2 -2
  41. package/esm/react-spa/apiBuilder.js +1 -1
  42. package/esm/react-spa/apiBuilder.js.map +1 -1
  43. package/esm/react-spa/createOidcSpaApi.js +29 -5
  44. package/esm/react-spa/createOidcSpaApi.js.map +1 -1
  45. package/esm/react-spa/types.d.ts +27 -3
  46. package/esm/tanstack-start/react/apiBuilder.d.ts +2 -2
  47. package/esm/tanstack-start/react/apiBuilder.js +1 -1
  48. package/esm/tanstack-start/react/apiBuilder.js.map +1 -1
  49. package/esm/tanstack-start/react/createOidcSpaApi.js +28 -4
  50. package/esm/tanstack-start/react/createOidcSpaApi.js.map +1 -1
  51. package/esm/tanstack-start/react/types.d.ts +27 -3
  52. package/esm/tools/lazySessionStorage.d.ts +3 -1
  53. package/esm/tools/lazySessionStorage.js +8 -6
  54. package/esm/tools/lazySessionStorage.js.map +1 -1
  55. package/esm/tools/parseKeycloakIssuerUri.js +5 -1
  56. package/esm/tools/parseKeycloakIssuerUri.js.map +1 -1
  57. package/keycloak/keycloak-js/Keycloak.d.ts +40 -0
  58. package/keycloak/keycloak-js/Keycloak.js +13 -3
  59. package/keycloak/keycloak-js/Keycloak.js.map +1 -1
  60. package/keycloak/keycloakUtils.d.ts +3 -1
  61. package/keycloak/keycloakUtils.js +26 -5
  62. package/keycloak/keycloakUtils.js.map +1 -1
  63. package/mock/oidc.js +2 -1
  64. package/mock/oidc.js.map +1 -1
  65. package/package.json +5 -1
  66. package/react/react.js +24 -2
  67. package/react/react.js.map +1 -1
  68. package/react-spa/apiBuilder.d.ts +2 -2
  69. package/react-spa/apiBuilder.js +1 -1
  70. package/react-spa/apiBuilder.js.map +1 -1
  71. package/react-spa/createOidcSpaApi.js +29 -5
  72. package/react-spa/createOidcSpaApi.js.map +1 -1
  73. package/react-spa/types.d.ts +27 -3
  74. package/src/angular.ts +76 -18
  75. package/src/core/Oidc.ts +1 -0
  76. package/src/core/createOidc.ts +78 -57
  77. package/src/core/desiredPostLoginRedirectUrl.ts +9 -0
  78. package/src/core/diagnostic.ts +4 -4
  79. package/src/core/homeAndRedirectUri.ts +36 -0
  80. package/src/keycloak/keycloak-js/Keycloak.ts +56 -3
  81. package/src/keycloak/keycloakUtils.ts +38 -6
  82. package/src/mock/oidc.ts +2 -1
  83. package/src/react/react.tsx +32 -3
  84. package/src/react-spa/apiBuilder.ts +3 -3
  85. package/src/react-spa/createOidcSpaApi.tsx +37 -6
  86. package/src/react-spa/types.tsx +27 -3
  87. package/src/tanstack-start/react/apiBuilder.ts +3 -3
  88. package/src/tanstack-start/react/createOidcSpaApi.tsx +36 -5
  89. package/src/tanstack-start/react/types.tsx +27 -3
  90. package/src/tools/lazySessionStorage.ts +11 -7
  91. package/src/tools/parseKeycloakIssuerUri.ts +6 -1
  92. package/src/vite-plugin/manageOptimizedDeps.ts +2 -1
  93. package/tools/lazySessionStorage.d.ts +3 -1
  94. package/tools/lazySessionStorage.js +8 -6
  95. package/tools/lazySessionStorage.js.map +1 -1
  96. package/tools/parseKeycloakIssuerUri.js +5 -1
  97. package/tools/parseKeycloakIssuerUri.js.map +1 -1
  98. package/vite-plugin/manageOptimizedDeps.js +2 -1
  99. package/vite-plugin/manageOptimizedDeps.js.map +1 -1
package/src/angular.ts CHANGED
@@ -19,6 +19,7 @@ import { Router, type CanActivateFn } from "@angular/router";
19
19
  import type { ValueOrAsyncGetter } from "./tools/ValueOrAsyncGetter";
20
20
  import { getBaseHref } from "./tools/getBaseHref";
21
21
  import type { ConcreteClass } from "./tools/ConcreteClass";
22
+ import { setDesiredPostLoginRedirectUrl } from "./core/desiredPostLoginRedirectUrl";
22
23
 
23
24
  export type ParamsOfProvide = {
24
25
  issuerUri: string;
@@ -96,11 +97,34 @@ export type ParamsOfProvide = {
96
97
  autoLogin?: boolean;
97
98
 
98
99
  /**
99
- * Default: false
100
+ * Determines how session restoration is handled.
101
+ * Session restoration allows users to stay logged in between visits
102
+ * without needing to explicitly sign in each time.
100
103
  *
101
- * See: https://docs.oidc-spa.dev/v/v8/resources/iframe-related-issues
104
+ * Options:
105
+ *
106
+ * - **"auto" (default)**:
107
+ * Automatically selects the best method.
108
+ * If the app’s domain shares a common parent domain with the authorization endpoint,
109
+ * an iframe is used for silent session restoration.
110
+ * Otherwise, a full-page redirect is used.
111
+ *
112
+ * - **"full page redirect"**:
113
+ * Forces full-page reloads for session restoration.
114
+ * Use this if your application is served with a restrictive CSP
115
+ * (e.g., `Content-Security-Policy: frame-ancestors "none"`)
116
+ * or `X-Frame-Options: DENY`, and you cannot modify those headers.
117
+ * This mode provides a slightly less seamless UX and will lead oidc-spa to
118
+ * store tokens in `localStorage` if multiple OIDC clients are used
119
+ * (e.g., your app communicates with several APIs).
120
+ *
121
+ * - **"iframe"**:
122
+ * Forces iframe-based session restoration.
123
+ * In development, if you go in your browser setting and allow your auth server’s domain
124
+ * to set third-party cookies this value will let you test your app
125
+ * with the local dev server as it will behave in production.
102
126
  */
103
- noIframe?: boolean;
127
+ sessionRestorationMethod?: "iframe" | "full page redirect" | "auto";
104
128
 
105
129
  debugLogs?: boolean;
106
130
 
@@ -150,7 +174,10 @@ export type ParamsOfProvide = {
150
174
  assert<
151
175
  Equals<
152
176
  Omit<ParamsOfProvide, "autoLogoutWarningDurationSeconds">,
153
- Omit<ParamsOfCreateOidc<any, boolean>, "homeUrl" | "BASE_URL" | "decodedIdTokenSchema">
177
+ Omit<
178
+ ParamsOfCreateOidc<any, boolean>,
179
+ "homeUrl" | "BASE_URL" | "noIframe" | "decodedIdTokenSchema"
180
+ >
154
181
  >
155
182
  >;
156
183
 
@@ -454,28 +481,55 @@ export abstract class AbstractOidcService<
454
481
 
455
482
  await instance.prInitialized;
456
483
 
457
- const oidc = instance.#getOidc({ callerName: "enforceLoginGuard" });
484
+ const redirectUrl = router.serializeUrl(
485
+ router.createUrlTree(
486
+ route.url.map(u => u.path),
487
+ {
488
+ queryParams: route.queryParams,
489
+ fragment: route.fragment ?? undefined
490
+ }
491
+ )
492
+ );
458
493
 
459
- if (!oidc.isUserLoggedIn) {
460
- const redirectUrl = router.serializeUrl(
461
- router.createUrlTree(
462
- route.url.map(u => u.path),
463
- {
464
- queryParams: route.queryParams,
465
- fragment: route.fragment ?? undefined
466
- }
467
- )
468
- );
494
+ const oidc = instance.#getOidc({ callerName: "enforceLoginGuard" });
469
495
 
470
- const doesCurrentHrefRequiresAuth =
471
- location.href.replace(/\/$/, "") === redirectUrl.replace(/\/$/, "");
496
+ const isUrlAlreadyReplaced =
497
+ window.location.href.replace(/\/$/, "") === redirectUrl.replace(/\/$/, "");
472
498
 
499
+ if (!oidc.isUserLoggedIn) {
473
500
  await oidc.login({
474
- doesCurrentHrefRequiresAuth,
501
+ doesCurrentHrefRequiresAuth: isUrlAlreadyReplaced,
475
502
  redirectUrl
476
503
  });
477
504
  }
478
505
 
506
+ define_temporary_postLoginRedirectUrl: {
507
+ if (isUrlAlreadyReplaced) {
508
+ break define_temporary_postLoginRedirectUrl;
509
+ }
510
+
511
+ setDesiredPostLoginRedirectUrl({ postLoginRedirectUrl: redirectUrl });
512
+
513
+ const history_pushState = history.pushState;
514
+ const history_replaceState = history.replaceState;
515
+
516
+ const onNavigated = () => {
517
+ history.pushState = history_pushState;
518
+ history.replaceState = history_replaceState;
519
+ setDesiredPostLoginRedirectUrl({ postLoginRedirectUrl: undefined });
520
+ };
521
+
522
+ history.pushState = function pushState(...args) {
523
+ onNavigated();
524
+ return history_pushState.call(history, ...args);
525
+ };
526
+
527
+ history.replaceState = function replaceState(...args) {
528
+ onNavigated();
529
+ return history_replaceState.call(history, ...args);
530
+ };
531
+ }
532
+
479
533
  return true;
480
534
  }) satisfies CanActivateFn;
481
535
  return canActivateFn;
@@ -569,6 +623,10 @@ export abstract class AbstractOidcService<
569
623
  return this.#getOidc({ callerName: "clientId" }).params.clientId;
570
624
  }
571
625
 
626
+ get validRedirectUri() {
627
+ return this.#getOidc({ callerName: "validRedirectUri" }).params.validRedirectUri;
628
+ }
629
+
572
630
  #isUserLoggedIn_override: boolean | undefined = undefined;
573
631
 
574
632
  get isUserLoggedIn() {
package/src/core/Oidc.ts CHANGED
@@ -9,6 +9,7 @@ export declare namespace Oidc {
9
9
  params: {
10
10
  issuerUri: string;
11
11
  clientId: string;
12
+ validRedirectUri: string;
12
13
  };
13
14
  };
14
15
 
@@ -45,13 +45,14 @@ import { getIsOnline } from "../tools/getIsOnline";
45
45
  import { isKeycloak } from "../keycloak/isKeycloak";
46
46
  import { INFINITY_TIME } from "../tools/INFINITY_TIME";
47
47
  import { prShouldLoadApp } from "./prShouldLoadApp";
48
- import { getBASE_URL } from "./BASE_URL";
49
48
  import { getIsLikelyDevServer } from "../tools/isLikelyDevServer";
50
49
  import { createObjectThatThrowsIfAccessed } from "../tools/createObjectThatThrowsIfAccessed";
51
50
  import {
52
51
  evtIsThereMoreThanOneInstanceThatCantUserIframes,
53
52
  notifyNewInstanceThatCantUseIframes
54
53
  } from "./instancesThatCantUseIframes";
54
+ import { getDesiredPostLoginRedirectUrl } from "./desiredPostLoginRedirectUrl";
55
+ import { getHomeAndRedirectUri } from "./homeAndRedirectUri";
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;
@@ -370,33 +405,7 @@ export async function createOidc_nonMemoized<
370
405
  return extraTokenParamsOrGetter;
371
406
  })();
372
407
 
373
- const homeUrlAndRedirectUri = toFullyQualifiedUrl({
374
- urlish: (() => {
375
- if (BASE_URL_params !== undefined) {
376
- return BASE_URL_params;
377
- }
378
-
379
- const BASE_URL = getBASE_URL();
380
-
381
- if (BASE_URL === undefined) {
382
- throw new Error(
383
- [
384
- "oidc-spa: If you do not use the oidc-spa Vite plugin",
385
- "you must provide the BASE_URL to the earlyInit() examples:",
386
- "oidcSpaEarlyInit({ BASE_URL: import.meta.env.BASE_URL })",
387
- "oidcSpaEarlyInit({ BASE_URL: '/' })",
388
- "",
389
- "You can also pass this parameter to createOidc({ BASE_URL: '...' })",
390
- "or bootstrapOidc({ BASE_URL: '...' })"
391
- ].join("\n")
392
- );
393
- }
394
-
395
- return BASE_URL;
396
- })(),
397
- doAssertNoQueryParams: true,
398
- doOutputWithTrailingSlash: true
399
- });
408
+ const { homeUrlAndRedirectUri } = getHomeAndRedirectUri({ BASE_URL_params });
400
409
 
401
410
  log?.(
402
411
  `Calling createOidc v${VERSION} ${JSON.stringify(
@@ -404,7 +413,7 @@ export async function createOidc_nonMemoized<
404
413
  issuerUri,
405
414
  clientId,
406
415
  scopes,
407
- oidcRedirectUri: homeUrlAndRedirectUri
416
+ validRedirectUri: homeUrlAndRedirectUri
408
417
  },
409
418
  null,
410
419
  2
@@ -416,8 +425,15 @@ export async function createOidc_nonMemoized<
416
425
  const oidcMetadata = __metadata ?? (await fetchOidcMetadata({ issuerUri }));
417
426
 
418
427
  const canUseIframe = (() => {
419
- if (noIframe) {
420
- return false;
428
+ switch (sessionRestorationMethod) {
429
+ case "auto":
430
+ break;
431
+ case "full page redirect":
432
+ return false;
433
+ case "iframe":
434
+ return true;
435
+ default:
436
+ assert<Equals<typeof sessionRestorationMethod, never>>;
421
437
  }
422
438
 
423
439
  third_party_cookies: {
@@ -525,7 +541,7 @@ export async function createOidc_nonMemoized<
525
541
  ];
526
542
  })(),
527
543
  "\n\nMore info:",
528
- "https://docs.oidc-spa.dev/v/v8/resources/end-of-third-party-cookies#when-are-cookies-considered-third-party"
544
+ "https://docs.oidc-spa.dev/v/v8/resources/third-party-cookies-and-session-restoration"
529
545
  ].join(" ")
530
546
  );
531
547
  } else {
@@ -555,7 +571,7 @@ export async function createOidc_nonMemoized<
555
571
  ];
556
572
  })(),
557
573
  "\nMore info:",
558
- "https://docs.oidc-spa.dev/v/v8/resources/end-of-third-party-cookies#when-are-cookies-considered-third-party"
574
+ "https://docs.oidc-spa.dev/v/v8/resources/third-party-cookies-and-session-restoration"
559
575
  ].join(" ")
560
576
  );
561
577
  }
@@ -599,7 +615,7 @@ export async function createOidc_nonMemoized<
599
615
  return new InMemoryWebStorage();
600
616
  }
601
617
 
602
- const storage = createLazySessionStorage();
618
+ const storage = createLazySessionStorage({ storageId: configId });
603
619
 
604
620
  if (evtIsThereMoreThanOneInstanceThatCantUserIframes.current) {
605
621
  storage.persistCurrentStateAndSubsequentChanges();
@@ -896,7 +912,7 @@ export async function createOidc_nonMemoized<
896
912
  redirectUri: homeUrlAndRedirectUri,
897
913
  clientId,
898
914
  issuerUri,
899
- noIframe
915
+ canUseIframe
900
916
  });
901
917
  }
902
918
 
@@ -957,11 +973,15 @@ export async function createOidc_nonMemoized<
957
973
  action: "login",
958
974
  doForceReloadOnBfCache: true,
959
975
  redirectUrl: (() => {
960
- if (evtIsThereMoreThanOneInstanceThatCantUserIframes.current) {
961
- return window.location.href;
976
+ if (postLoginRedirectUrl_default) {
977
+ return postLoginRedirectUrl_default;
978
+ }
979
+
980
+ if (!evtIsThereMoreThanOneInstanceThatCantUserIframes.current) {
981
+ return getRootRelativeOriginalLocationHref();
962
982
  }
963
983
 
964
- return getRootRelativeOriginalLocationHref();
984
+ return getDesiredPostLoginRedirectUrl() ?? window.location.href;
965
985
  })(),
966
986
  // NOTE: Wether or not it's the preferred behavior, pushing to history
967
987
  // only works on user interaction so it have to be false
@@ -1020,7 +1040,8 @@ export async function createOidc_nonMemoized<
1020
1040
  const oidc_common: Oidc.Common = {
1021
1041
  params: {
1022
1042
  issuerUri,
1023
- clientId
1043
+ clientId,
1044
+ validRedirectUri: homeUrlAndRedirectUri
1024
1045
  }
1025
1046
  };
1026
1047
 
@@ -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
  }
@@ -0,0 +1,36 @@
1
+ import { toFullyQualifiedUrl } from "../tools/toFullyQualifiedUrl";
2
+ import { getBASE_URL } from "./BASE_URL";
3
+
4
+ export function getHomeAndRedirectUri(params: { BASE_URL_params: string | undefined }) {
5
+ const { BASE_URL_params } = params;
6
+
7
+ const homeUrlAndRedirectUri = toFullyQualifiedUrl({
8
+ urlish: (() => {
9
+ if (BASE_URL_params !== undefined) {
10
+ return BASE_URL_params;
11
+ }
12
+
13
+ const BASE_URL = getBASE_URL();
14
+
15
+ if (BASE_URL === undefined) {
16
+ throw new Error(
17
+ [
18
+ "oidc-spa: If you do not use the oidc-spa Vite plugin",
19
+ "you must provide the BASE_URL to the earlyInit() examples:",
20
+ "oidcSpaEarlyInit({ BASE_URL: import.meta.env.BASE_URL })",
21
+ "oidcSpaEarlyInit({ BASE_URL: '/' })",
22
+ "",
23
+ "You can also pass this parameter to createOidc({ BASE_URL: '...' })",
24
+ "or bootstrapOidc({ BASE_URL: '...' })"
25
+ ].join("\n")
26
+ );
27
+ }
28
+
29
+ return BASE_URL;
30
+ })(),
31
+ doAssertNoQueryParams: true,
32
+ doOutputWithTrailingSlash: true
33
+ });
34
+
35
+ return { homeUrlAndRedirectUri };
36
+ }
@@ -21,9 +21,51 @@ import { type KeycloakUtils, createKeycloakUtils } from "../keycloakUtils";
21
21
  import { workerTimers } from "../../vendor/frontend/worker-timers";
22
22
  import { type StatefulEvt, createStatefulEvt } from "../../tools/StatefulEvt";
23
23
  import { readExpirationTimeInJwt } from "../../tools/readExpirationTimeInJwt";
24
+ import { getHomeAndRedirectUri } from "../../core/homeAndRedirectUri";
24
25
 
25
26
  type ConstructorParams = KeycloakServerConfig & {
27
+ /**
28
+ * NOTE: This parameter is optional if you use the Vite plugin.
29
+ *
30
+ * This parameter let's you overwrite the value provided in
31
+ * oidcEarlyInit({ BASE_URL: xxx });
32
+ *
33
+ * What should you put in this parameter?
34
+ * - Vite project: `BASE_URL: import.meta.env.BASE_URL`
35
+ * - Create React App project: `BASE_URL: process.env.PUBLIC_URL`
36
+ * - Other: `BASE_URL: "/"` (Usually, or `/dashboard` if your app is not at the root of the domain)
37
+ */
26
38
  BASE_URL?: string;
39
+
40
+ /**
41
+ * Determines how session restoration is handled.
42
+ * Session restoration allows users to stay logged in between visits
43
+ * without needing to explicitly sign in each time.
44
+ *
45
+ * Options:
46
+ *
47
+ * - **"auto" (default)**:
48
+ * Automatically selects the best method.
49
+ * If the app’s domain shares a common parent domain with the authorization endpoint,
50
+ * an iframe is used for silent session restoration.
51
+ * Otherwise, a full-page redirect is used.
52
+ *
53
+ * - **"full page redirect"**:
54
+ * Forces full-page reloads for session restoration.
55
+ * Use this if your application is served with a restrictive CSP
56
+ * (e.g., `Content-Security-Policy: frame-ancestors "none"`)
57
+ * or `X-Frame-Options: DENY`, and you cannot modify those headers.
58
+ * This mode provides a slightly less seamless UX and will lead oidc-spa to
59
+ * store tokens in `localStorage` if multiple OIDC clients are used
60
+ * (e.g., your app communicates with several APIs).
61
+ *
62
+ * - **"iframe"**:
63
+ * Forces iframe-based session restoration.
64
+ * In development, if you go in your browser setting and allow your auth server’s domain
65
+ * to set third-party cookies this value will let you test your app
66
+ * with the local dev server as it will behave in production.
67
+ */
68
+ sessionRestorationMethod?: "iframe" | "full page redirect" | "auto";
27
69
  };
28
70
 
29
71
  /**
@@ -99,7 +141,8 @@ export class Keycloak {
99
141
  let hasCreateResolved = false;
100
142
 
101
143
  const oidcOrError = await createOidc({
102
- homeUrl: constructorParams.BASE_URL,
144
+ BASE_URL: constructorParams.BASE_URL,
145
+ sessionRestorationMethod: constructorParams.sessionRestorationMethod,
103
146
  issuerUri,
104
147
  clientId: this.#state.constructorParams.clientId,
105
148
  autoLogin,
@@ -892,11 +935,21 @@ export class Keycloak {
892
935
  createAccountUrl(options?: KeycloakAccountOptions & { locale?: string }): string {
893
936
  const { locale, redirectUri } = options ?? {};
894
937
 
895
- const { keycloakUtils } = this.#state;
938
+ const { keycloakUtils, constructorParams } = this.#state;
896
939
 
897
940
  return keycloakUtils.getAccountUrl({
898
941
  clientId: this.clientId,
899
- backToAppFromAccountUrl: redirectUri ?? location.href,
942
+ validRedirectUri: (() => {
943
+ if (redirectUri !== undefined) {
944
+ return redirectUri;
945
+ }
946
+
947
+ const { homeUrlAndRedirectUri } = getHomeAndRedirectUri({
948
+ BASE_URL_params: constructorParams.BASE_URL
949
+ });
950
+
951
+ return homeUrlAndRedirectUri;
952
+ })(),
900
953
  locale
901
954
  });
902
955
  }
@@ -1,5 +1,6 @@
1
1
  import { toFullyQualifiedUrl } from "../tools/toFullyQualifiedUrl";
2
2
  import { type KeycloakIssuerUriParsed, parseKeycloakIssuerUri } from "./keycloakIssuerUriParsed";
3
+ import { assert } from "../tools/tsafe/assert";
3
4
 
4
5
  export type KeycloakUtils = {
5
6
  issuerUriParsed: KeycloakIssuerUriParsed;
@@ -7,7 +8,9 @@ export type KeycloakUtils = {
7
8
  adminConsoleUrl_master: string;
8
9
  getAccountUrl: (params: {
9
10
  clientId: string;
10
- backToAppFromAccountUrl: string;
11
+ validRedirectUri?: string;
12
+ /** @deprecated: Use validRedirectUri */
13
+ backToAppFromAccountUrl?: string;
11
14
  locale?: string;
12
15
  }) => string;
13
16
  fetchUserProfile: (params: { accessToken: string }) => Promise<KeycloakProfile>;
@@ -49,17 +52,46 @@ export function createKeycloakUtils(params: { issuerUri: string }): KeycloakUtil
49
52
  issuerUriParsed,
50
53
  adminConsoleUrl: getAdminConsoleUrl(issuerUriParsed.realm),
51
54
  adminConsoleUrl_master: getAdminConsoleUrl("master"),
52
- getAccountUrl: ({ clientId, backToAppFromAccountUrl, locale }) => {
55
+ getAccountUrl: ({ clientId, locale, ...rest }) => {
56
+ const validRedirectUri = (() => {
57
+ const { validRedirectUri, backToAppFromAccountUrl } = rest;
58
+
59
+ if (validRedirectUri !== undefined) {
60
+ assert(
61
+ backToAppFromAccountUrl === undefined,
62
+ "getAccountUrl: backToAppFromAccountUrl is deprecated"
63
+ );
64
+ return validRedirectUri;
65
+ }
66
+
67
+ assert(
68
+ backToAppFromAccountUrl !== undefined,
69
+ "getAccountUrl: Must provide validRedirectUri"
70
+ );
71
+
72
+ return backToAppFromAccountUrl;
73
+ })();
74
+
53
75
  const accountUrlObj = new URL(
54
76
  `${keycloakServerUrl}/realms/${issuerUriParsed.realm}/account`
55
77
  );
56
78
  accountUrlObj.searchParams.set("referrer", clientId);
57
79
  accountUrlObj.searchParams.set(
58
80
  "referrer_uri",
59
- toFullyQualifiedUrl({
60
- urlish: backToAppFromAccountUrl,
61
- doAssertNoQueryParams: false
62
- })
81
+ (() => {
82
+ try {
83
+ return toFullyQualifiedUrl({
84
+ urlish: validRedirectUri,
85
+ doAssertNoQueryParams: true,
86
+ doOutputWithTrailingSlash: true
87
+ });
88
+ } catch {
89
+ return toFullyQualifiedUrl({
90
+ urlish: validRedirectUri,
91
+ doAssertNoQueryParams: false
92
+ });
93
+ }
94
+ })()
63
95
  );
64
96
  if (locale !== undefined) {
65
97
  accountUrlObj.searchParams.set("kc_locale", locale);
package/src/mock/oidc.ts CHANGED
@@ -96,7 +96,8 @@ export async function createMockOidc<
96
96
  const common: Oidc.Common = {
97
97
  params: {
98
98
  clientId: mockedParams.clientId ?? "mymockclient",
99
- issuerUri: mockedParams.issuerUri ?? "https://my-mock-oidc-server.net/realms/mymockrealm"
99
+ issuerUri: mockedParams.issuerUri ?? "https://my-mock-oidc-server.net/realms/mymockrealm",
100
+ validRedirectUri: homeUrl
100
101
  }
101
102
  };
102
103