oidc-spa 6.14.2 → 6.15.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.
@@ -110,26 +110,30 @@ export async function createIframeTimeoutInitializationError(params: {
110
110
  callbackUri: string;
111
111
  issuerUri: string;
112
112
  clientId: string;
113
+ noIframe: boolean;
113
114
  }): Promise<OidcInitializationError> {
114
- const { callbackUri, issuerUri, clientId } = params;
115
+ const { callbackUri, issuerUri, clientId, noIframe } = params;
115
116
 
116
- frame_ancestors_none: {
117
- if (!/^https?:\/\//.test(callbackUri)) {
118
- break frame_ancestors_none;
117
+ iframe_blocked: {
118
+ if (noIframe) {
119
+ break iframe_blocked;
119
120
  }
120
121
 
121
- const cspOrError = await fetch(callbackUri).then(
122
+ const headersOrError = await fetch(callbackUri).then(
122
123
  response => {
123
124
  if (!response.ok) {
124
125
  return new Error(`${callbackUri} responded with a ${response.status} status code.`);
125
126
  }
126
127
 
127
- return response.headers.get("Content-Security-Policy");
128
+ return {
129
+ "Content-Security-Policy": response.headers.get("Content-Security-Policy"),
130
+ "X-Frame-Options": response.headers.get("X-Frame-Options")
131
+ };
128
132
  },
129
- error => error
133
+ (error: Error) => error
130
134
  );
131
135
 
132
- if (cspOrError instanceof Error) {
136
+ if (headersOrError instanceof Error) {
133
137
  return new OidcInitializationError({
134
138
  isAuthServerLikelyDown: false,
135
139
  messageOrCause: new Error(
@@ -140,41 +144,62 @@ export async function createIframeTimeoutInitializationError(params: {
140
144
  });
141
145
  }
142
146
 
143
- const csp = cspOrError;
147
+ const headers = headersOrError;
144
148
 
145
- if (csp === null) {
146
- break frame_ancestors_none;
147
- }
149
+ let key_problem = (() => {
150
+ block: {
151
+ const key = "Content-Security-Policy" as const;
152
+
153
+ const header = headers[key];
154
+
155
+ if (header === null) {
156
+ break block;
157
+ }
158
+
159
+ const hasFrameAncestorsNone = header
160
+ .replace(/["']/g, "")
161
+ .replace(/\s+/g, " ")
162
+ .toLowerCase()
163
+ .includes("frame-ancestors none");
164
+
165
+ if (!hasFrameAncestorsNone) {
166
+ break block;
167
+ }
168
+
169
+ return key;
170
+ }
171
+
172
+ block: {
173
+ const key = "X-Frame-Options" as const;
174
+
175
+ const header = headers[key];
176
+
177
+ if (header === null) {
178
+ break block;
179
+ }
180
+
181
+ const hasFrameAncestorsNone = header.toLowerCase().includes("deny");
182
+
183
+ if (!hasFrameAncestorsNone) {
184
+ break block;
185
+ }
186
+
187
+ return key;
188
+ }
148
189
 
149
- const hasFrameAncestorsNone = csp
150
- .replace(/["']/g, "")
151
- .replace(/\s+/g, " ")
152
- .toLowerCase()
153
- .includes("frame-ancestors none");
190
+ return undefined;
191
+ })();
154
192
 
155
- if (!hasFrameAncestorsNone) {
156
- break frame_ancestors_none;
193
+ if (key_problem === undefined) {
194
+ break iframe_blocked;
157
195
  }
158
196
 
159
197
  return new OidcInitializationError({
160
198
  isAuthServerLikelyDown: false,
161
199
  messageOrCause: [
162
- `${callbackUri} is currently served by your web server with the HTTP header \`Content-Security-Policy: frame-ancestors none\`.\n`,
200
+ `${callbackUri} is currently served by your web server with the HTTP header \`${key_problem}: ${headers[key_problem]}\`.\n`,
163
201
  "This header prevents the silent sign-in process from working.\n",
164
- "To fix this issue, you need to allow your application's homepage to be iframed during the silent login flow. ",
165
- "For example, replacing `frame-ancestors 'none'` with `frame-ancestors 'self'` ensures your app can be embedded in an iframe on the same domain.\n",
166
- "However, if you are concerned about allowing the entire SPA to be iframed, you can selectively loosen the `frame-ancestors` policy only when the `state` parameter is present on the URL.\n",
167
- "If you're using Nginx, a possible configuration might look like:\n",
168
- "ngnix.conf:\n",
169
- "```\n",
170
- "map $query_string $add_content_security_policy {\n",
171
- ' "~*state=" "frame-ancestors \'self\'";\n',
172
- " default \"frame-ancestors 'none'\";\n",
173
- "}\n",
174
- "add_header Content-Security-Policy $add_content_security_policy;\n",
175
- "```\n",
176
- "This way, the homepage is only iframed when the `state` parameter is present, and remains protected in all other scenarios.\n",
177
- `The URL in question is: ${callbackUri}`
202
+ "Refer to this documentation page to fix this issue: https://docs.oidc-spa.dev/v/v6/resources/iframe-related-issues"
178
203
  ].join(" ")
179
204
  });
180
205
  }
@@ -156,6 +156,14 @@ export type ParamsOfCreateOidc<
156
156
 
157
157
  autoLogoutParams?: Parameters<Oidc.LoggedIn<any>["logout"]>[0];
158
158
  autoLogin?: AutoLogin;
159
+
160
+ /**
161
+ * Default: false
162
+ *
163
+ * See: https://docs.oidc-spa.dev/v/v6/resources/iframe-related-issues
164
+ */
165
+ noIframe?: boolean;
166
+
159
167
  debugLogs?: boolean;
160
168
 
161
169
  __unsafe_clientSecret?: string;
@@ -302,7 +310,8 @@ export async function createOidc_nonMemoized<
302
310
  postLoginRedirectUrl: postLoginRedirectUrl_default,
303
311
  __unsafe_clientSecret,
304
312
  __unsafe_useIdTokenAsAccessToken = false,
305
- __metadata
313
+ __metadata,
314
+ noIframe = false
306
315
  } = params;
307
316
 
308
317
  const { issuerUri, clientId, scopes, configId, log } = preProcessedParams;
@@ -368,28 +377,54 @@ export async function createOidc_nonMemoized<
368
377
 
369
378
  const stateQueryParamValue_instance = generateStateQueryParamValue();
370
379
 
371
- let isOidcServerThirdPartyRelativeToApp: boolean;
372
- {
373
- const url1 = window.location.origin;
374
- const url2 = issuerUri;
380
+ const canUseIframe = (() => {
381
+ if (noIframe) {
382
+ return false;
383
+ }
384
+
385
+ // NOTE: Electron
386
+ if (!/https?:\/\//.test(callbackUri)) {
387
+ log?.("We won't use iframe, callbackUri uses a custom protocol.");
388
+ return false;
389
+ }
375
390
 
376
- isOidcServerThirdPartyRelativeToApp =
377
- getHaveSharedParentDomain({
378
- url1,
379
- url2
380
- }) === false;
391
+ third_party_cookies: {
392
+ const isOidcServerThirdPartyRelativeToApp =
393
+ getHaveSharedParentDomain({
394
+ url1: window.location.origin,
395
+ url2: issuerUri
396
+ }) === false;
397
+
398
+ if (!isOidcServerThirdPartyRelativeToApp) {
399
+ break third_party_cookies;
400
+ }
401
+
402
+ const isGoogleChrome = (() => {
403
+ const ua = navigator.userAgent;
404
+ const vendor = navigator.vendor;
405
+
406
+ return (
407
+ /Chrome/.test(ua) && /Google Inc/.test(vendor) && !/Edg/.test(ua) && !/OPR/.test(ua)
408
+ );
409
+ })();
410
+
411
+ if (window.location.origin.startsWith("http://localhost") && isGoogleChrome) {
412
+ break third_party_cookies;
413
+ }
381
414
 
382
- if (isOidcServerThirdPartyRelativeToApp) {
383
415
  log?.(
384
416
  [
385
- `${url1} and ${url2} don't have shared parent domain, silent signin in iframe (unless in chrome in dev mode) is not possible.`,
386
- "oidc-spa will have to fully reload the app to restore the auth state."
417
+ "Can't use iframe because your auth server is on a third party domain relative",
418
+ "to the domain of your app and third party cookies are blocked by navigators."
387
419
  ].join(" ")
388
420
  );
389
- } else {
390
- log?.(`${url1} and ${url2} have shared parent domain silent signin in iframe is possible`);
421
+
422
+ return false;
391
423
  }
392
- }
424
+
425
+ // NOTE: Maybe not, it depend if the app can iframe itself.
426
+ return true;
427
+ })();
393
428
 
394
429
  let isUserStoreInMemoryOnly: boolean;
395
430
 
@@ -405,7 +440,7 @@ export async function createOidc_nonMemoized<
405
440
  automaticSilentRenew: false,
406
441
  userStore: new WebStorageStateStore({
407
442
  store: (() => {
408
- if (!isOidcServerThirdPartyRelativeToApp) {
443
+ if (canUseIframe) {
409
444
  isUserStoreInMemoryOnly = true;
410
445
  return new InMemoryWebStorage();
411
446
  }
@@ -568,7 +603,6 @@ export async function createOidc_nonMemoized<
568
603
 
569
604
  notifyOtherTabsOfLogout({
570
605
  configId,
571
- redirectUrl: stateData.redirectUrl,
572
606
  sessionId: stateData.sessionId
573
607
  });
574
608
 
@@ -629,19 +663,8 @@ export async function createOidc_nonMemoized<
629
663
  break actual_silent_signin;
630
664
  }
631
665
 
632
- if (isOidcServerThirdPartyRelativeToApp) {
633
- // NOTE: Electron
634
- if (!/https?:\/\//.test(callbackUri)) {
635
- log?.("Skipping silent signin with iframe, custom protocol");
636
- break actual_silent_signin;
637
- }
638
-
639
- if (!homeUrl.startsWith("http://localhost")) {
640
- log?.(
641
- "Skipping silent signin with iframe, third party cookies are are not allowed so we know this won't work"
642
- );
643
- break actual_silent_signin;
644
- }
666
+ if (!canUseIframe) {
667
+ break actual_silent_signin;
645
668
  }
646
669
 
647
670
  log?.(
@@ -672,7 +695,8 @@ export async function createOidc_nonMemoized<
672
695
  return createIframeTimeoutInitializationError({
673
696
  callbackUri,
674
697
  clientId,
675
- issuerUri
698
+ issuerUri,
699
+ noIframe
676
700
  });
677
701
  }
678
702
 
@@ -924,7 +948,7 @@ export async function createOidc_nonMemoized<
924
948
  persistAuthState({ configId, state: undefined });
925
949
  }
926
950
 
927
- if (isOidcServerThirdPartyRelativeToApp) {
951
+ if (!canUseIframe) {
928
952
  persistAuthState({
929
953
  configId,
930
954
  state: {
@@ -1018,7 +1042,6 @@ export async function createOidc_nonMemoized<
1018
1042
 
1019
1043
  notifyOtherTabsOfLogout({
1020
1044
  configId,
1021
- redirectUrl: postLogoutRedirectUrl,
1022
1045
  sessionId
1023
1046
  });
1024
1047
 
@@ -1036,6 +1059,19 @@ export async function createOidc_nonMemoized<
1036
1059
  }) {
1037
1060
  const { extraTokenParams } = params;
1038
1061
 
1062
+ if (!currentTokens.hasRefreshToken && !canUseIframe) {
1063
+ const message = [
1064
+ "Unable to refresh tokens without a full app reload,",
1065
+ "because no refresh token is available",
1066
+ "and your app setup prevents silent sign-in via iframe.",
1067
+ "Your only option to refresh tokens is to call `window.location.reload()`"
1068
+ ].join(" ");
1069
+
1070
+ log?.(message);
1071
+
1072
+ throw new Error(message);
1073
+ }
1074
+
1039
1075
  log?.("Renewing tokens");
1040
1076
 
1041
1077
  const { completeLoginOrRefreshProcess } = await startLoginOrRefreshProcess();
@@ -1247,22 +1283,17 @@ export async function createOidc_nonMemoized<
1247
1283
  {
1248
1284
  const { prOtherTabLogout } = getPrOtherTabLogout({
1249
1285
  configId,
1250
- homeUrl,
1251
1286
  sessionId
1252
1287
  });
1253
1288
 
1254
- prOtherTabLogout.then(async ({ redirectUrl }) => {
1255
- log?.(`Other tab has logged out, redirecting to ${redirectUrl}`);
1289
+ prOtherTabLogout.then(async () => {
1290
+ log?.(`Other tab has logged out, refreshing current tab`);
1256
1291
 
1257
1292
  await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
1258
1293
  prUnlock: new Promise<never>(() => {})
1259
1294
  });
1260
1295
 
1261
- window.addEventListener("pageshow", () => {
1262
- location.reload();
1263
- });
1264
-
1265
- window.location.href = redirectUrl;
1296
+ location.reload();
1266
1297
  });
1267
1298
  }
1268
1299
 
@@ -132,12 +132,21 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
132
132
  }
133
133
  });
134
134
 
135
- reloadOnBfCacheNavigation();
136
-
137
135
  setTimeout(() => {
138
136
  debug966975.log(`(callback 0) Calling window.history.${historyMethod}()`);
139
137
 
138
+ reloadOnBfCacheNavigation();
139
+
140
140
  window.history[historyMethod]();
141
+
142
+ // NOTE: This is a "better than nothing" approach.
143
+ // Under some circumstances it's possible to get stuck on this url
144
+ // if there is no "next" page in the history for example, navigating
145
+ // forward is a NoOp. So in that case it's better to navigate to the home.
146
+ setTimeout(() => {
147
+ const { protocol, host, pathname, hash } = window.location;
148
+ window.location.href = `${protocol}//${host}${pathname}${hash}`;
149
+ }, 350);
141
150
  }, 0);
142
151
 
143
152
  debug966975.log(`returning isHandled: ${isHandled ? "true" : "false"}`);
@@ -85,7 +85,7 @@ export function createLoginOrGoToAuthServer(params: {
85
85
 
86
86
  let lastPublicUrl: string | undefined = undefined;
87
87
 
88
- function loginOrGoToAuthServer(params: Params): Promise<never> {
88
+ async function loginOrGoToAuthServer(params: Params): Promise<never> {
89
89
  const {
90
90
  redirectUrl: redirectUrl_params,
91
91
  extraQueryParams_local,
@@ -107,6 +107,21 @@ export function createLoginOrGoToAuthServer(params: {
107
107
 
108
108
  globalContext.evtHasLoginBeenCalled.current = true;
109
109
 
110
+ if (document.visibilityState !== "visible") {
111
+ const dVisible = new Deferred<void>();
112
+
113
+ const onVisible = () => {
114
+ if (document.visibilityState !== "visible") {
115
+ return;
116
+ }
117
+ document.removeEventListener("visibilitychange", onVisible);
118
+ dVisible.resolve();
119
+ };
120
+ document.addEventListener("visibilitychange", onVisible);
121
+
122
+ await dVisible.pr;
123
+ }
124
+
110
125
  bf_cache_handling: {
111
126
  if (rest.doForceReloadOnBfCache) {
112
127
  window.removeEventListener("pageshow", () => {
@@ -7,7 +7,6 @@ const globalContext = {
7
7
 
8
8
  type Message = {
9
9
  appInstanceId: string;
10
- redirectUrl_initiator: string;
11
10
  configId: string;
12
11
  };
13
12
 
@@ -16,15 +15,10 @@ function getChannelName(params: { sessionIdOrConfigId: string }) {
16
15
  return `oidc-spa:logout-propagation:${sessionIdOrConfigId}`;
17
16
  }
18
17
 
19
- export function notifyOtherTabsOfLogout(params: {
20
- redirectUrl: string;
21
- configId: string;
22
- sessionId: string | undefined;
23
- }) {
24
- const { redirectUrl, configId, sessionId } = params;
18
+ export function notifyOtherTabsOfLogout(params: { configId: string; sessionId: string | undefined }) {
19
+ const { configId, sessionId } = params;
25
20
 
26
21
  const message: Message = {
27
- redirectUrl_initiator: redirectUrl,
28
22
  configId,
29
23
  appInstanceId: globalContext.appInstanceId
30
24
  };
@@ -34,14 +28,10 @@ export function notifyOtherTabsOfLogout(params: {
34
28
  );
35
29
  }
36
30
 
37
- export function getPrOtherTabLogout(params: {
38
- sessionId: string | undefined;
39
- configId: string;
40
- homeUrl: string;
41
- }) {
42
- const { sessionId, configId, homeUrl } = params;
31
+ export function getPrOtherTabLogout(params: { sessionId: string | undefined; configId: string }) {
32
+ const { sessionId, configId } = params;
43
33
 
44
- const dOtherTabLogout = new Deferred<{ redirectUrl: string }>();
34
+ const dOtherTabLogout = new Deferred<void>();
45
35
 
46
36
  const channel = new BroadcastChannel(getChannelName({ sessionIdOrConfigId: sessionId ?? configId }));
47
37
 
@@ -54,15 +44,7 @@ export function getPrOtherTabLogout(params: {
54
44
 
55
45
  channel.close();
56
46
 
57
- const redirectUrl = (() => {
58
- if (configId === message.configId) {
59
- return message.redirectUrl_initiator;
60
- }
61
-
62
- return homeUrl;
63
- })();
64
-
65
- dOtherTabLogout.resolve({ redirectUrl });
47
+ dOtherTabLogout.resolve();
66
48
  };
67
49
 
68
50
  const prOtherTabLogout = dOtherTabLogout.pr;