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.
- package/core/OidcInitializationError.d.ts +1 -0
- package/core/OidcInitializationError.js +44 -31
- package/core/OidcInitializationError.js.map +1 -1
- package/core/createOidc.d.ts +6 -0
- package/core/createOidc.js +64 -52
- package/core/createOidc.js.map +1 -1
- package/core/handleOidcCallback.js +9 -1
- package/core/handleOidcCallback.js.map +1 -1
- package/core/loginOrGoToAuthServer.js +238 -179
- package/core/loginOrGoToAuthServer.js.map +1 -1
- package/core/logoutPropagationToOtherTabs.d.ts +1 -5
- package/core/logoutPropagationToOtherTabs.js +3 -10
- package/core/logoutPropagationToOtherTabs.js.map +1 -1
- package/package.json +1 -1
- package/src/core/OidcInitializationError.ts +59 -34
- package/src/core/createOidc.ts +73 -42
- package/src/core/handleOidcCallback.ts +11 -2
- package/src/core/loginOrGoToAuthServer.ts +16 -1
- package/src/core/logoutPropagationToOtherTabs.ts +6 -24
|
@@ -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
|
-
|
|
117
|
-
if (
|
|
118
|
-
break
|
|
117
|
+
iframe_blocked: {
|
|
118
|
+
if (noIframe) {
|
|
119
|
+
break iframe_blocked;
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
const
|
|
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
|
|
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 (
|
|
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
|
|
147
|
+
const headers = headersOrError;
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
.replace(/\s+/g, " ")
|
|
152
|
-
.toLowerCase()
|
|
153
|
-
.includes("frame-ancestors none");
|
|
190
|
+
return undefined;
|
|
191
|
+
})();
|
|
154
192
|
|
|
155
|
-
if (
|
|
156
|
-
break
|
|
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
|
|
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
|
-
"
|
|
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
|
}
|
package/src/core/createOidc.ts
CHANGED
|
@@ -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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
386
|
-
"
|
|
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
|
-
|
|
390
|
-
|
|
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 (
|
|
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 (
|
|
633
|
-
|
|
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 (
|
|
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 (
|
|
1255
|
-
log?.(`Other tab has logged out,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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<
|
|
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
|
-
|
|
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;
|