oidc-spa 6.5.1 → 6.6.0
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/mock/oidc.js +6 -5
- package/mock/oidc.js.map +1 -1
- package/oidc/AuthResponse.d.ts +6 -0
- package/oidc/AuthResponse.js +59 -0
- package/oidc/AuthResponse.js.map +1 -0
- package/oidc/Oidc.d.ts +19 -8
- package/oidc/createOidc.d.ts +4 -4
- package/oidc/createOidc.js +359 -295
- package/oidc/createOidc.js.map +1 -1
- package/oidc/evtIsUserActive.d.ts +15 -0
- package/oidc/{isUserActive.js → evtIsUserActive.js} +29 -12
- package/oidc/evtIsUserActive.js.map +1 -0
- package/oidc/handleOidcCallback.d.ts +8 -1
- package/oidc/handleOidcCallback.js +68 -13
- package/oidc/handleOidcCallback.js.map +1 -1
- package/oidc/loginOrGoToAuthServer.d.ts +5 -4
- package/oidc/loginOrGoToAuthServer.js +190 -227
- package/oidc/loginOrGoToAuthServer.js.map +1 -1
- package/oidc/loginPropagationToOtherTabs.d.ts +17 -0
- package/oidc/loginPropagationToOtherTabs.js +41 -0
- package/oidc/loginPropagationToOtherTabs.js.map +1 -0
- package/oidc/loginSilent.d.ts +1 -5
- package/oidc/loginSilent.js +3 -51
- package/oidc/loginSilent.js.map +1 -1
- package/oidc/logoutPropagationToOtherTabs.js +1 -1
- package/oidc/logoutPropagationToOtherTabs.js.map +1 -1
- package/oidc/oidcClientTsUserToTokens.d.ts +1 -1
- package/oidc/oidcClientTsUserToTokens.js +45 -23
- package/oidc/oidcClientTsUserToTokens.js.map +1 -1
- package/oidc/ongoingLoginOrRefreshProcesses.d.ts +16 -0
- package/oidc/ongoingLoginOrRefreshProcesses.js +102 -0
- package/oidc/ongoingLoginOrRefreshProcesses.js.map +1 -0
- package/oidc/persistedAuthState.d.ts +16 -3
- package/oidc/persistedAuthState.js +35 -4
- package/oidc/persistedAuthState.js.map +1 -1
- package/package.json +36 -21
- package/react/react.js +8 -14
- package/react/react.js.map +1 -1
- package/src/mock/oidc.ts +14 -3
- package/src/oidc/AuthResponse.ts +26 -0
- package/src/oidc/Oidc.ts +19 -4
- package/src/oidc/createOidc.ts +233 -206
- package/src/oidc/{isUserActive.ts → evtIsUserActive.ts} +36 -10
- package/src/oidc/handleOidcCallback.ts +73 -12
- package/src/oidc/loginOrGoToAuthServer.ts +94 -87
- package/src/oidc/loginPropagationToOtherTabs.ts +63 -0
- package/src/oidc/loginSilent.ts +2 -20
- package/src/oidc/logoutPropagationToOtherTabs.ts +2 -2
- package/src/oidc/oidcClientTsUserToTokens.ts +74 -35
- package/src/oidc/ongoingLoginOrRefreshProcesses.ts +60 -0
- package/src/oidc/persistedAuthState.ts +66 -8
- package/src/react/react.tsx +8 -16
- package/src/tools/{ephemeralSessionStorage.ts → EphemeralSessionStorage.ts} +59 -27
- package/src/tools/Evt.ts +56 -0
- package/src/tools/StatefulEvt.ts +38 -0
- package/src/tools/parseKeycloakIssuerUri.ts +9 -1
- package/src/tools/subscribeToUserInteraction.ts +0 -1
- package/src/tools/workerTimers.ts +10 -12
- package/tools/EphemeralSessionStorage.d.ts +12 -0
- package/tools/{ephemeralSessionStorage.js → EphemeralSessionStorage.js} +29 -16
- package/tools/EphemeralSessionStorage.js.map +1 -0
- package/tools/Evt.d.ts +11 -0
- package/tools/{AwaitableEventEmitter.js → Evt.js} +24 -8
- package/tools/Evt.js.map +1 -0
- package/tools/StatefulEvt.d.ts +12 -0
- package/tools/StatefulEvt.js +24 -0
- package/tools/StatefulEvt.js.map +1 -0
- package/tools/parseKeycloakIssuerUri.js +5 -1
- package/tools/parseKeycloakIssuerUri.js.map +1 -1
- package/tools/subscribeToUserInteraction.js +2 -3
- package/tools/subscribeToUserInteraction.js.map +1 -1
- package/tools/workerTimers.js +11 -13
- package/tools/workerTimers.js.map +1 -1
- package/oidc/isUserActive.d.ts +0 -13
- package/oidc/isUserActive.js.map +0 -1
- package/src/tools/AwaitableEventEmitter.ts +0 -33
- package/src/tools/StatefulObservable.ts +0 -52
- package/tools/AwaitableEventEmitter.d.ts +0 -5
- package/tools/AwaitableEventEmitter.js.map +0 -1
- package/tools/StatefulObservable.d.ts +0 -12
- package/tools/StatefulObservable.js +0 -33
- package/tools/StatefulObservable.js.map +0 -1
- package/tools/ephemeralSessionStorage.d.ts +0 -3
- package/tools/ephemeralSessionStorage.js.map +0 -1
|
@@ -1,27 +1,46 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createEvt, type NonPostableEvt } from "../tools/Evt";
|
|
2
2
|
import { subscribeToUserInteraction } from "../tools/subscribeToUserInteraction";
|
|
3
3
|
import { assert, is, id } from "../vendor/frontend/tsafe";
|
|
4
4
|
import { setTimeout, clearTimeout } from "../tools/workerTimers";
|
|
5
5
|
|
|
6
|
-
const GLOBAL_CONTEXT_KEY = "__oidc-spa.
|
|
6
|
+
const GLOBAL_CONTEXT_KEY = "__oidc-spa.evtIsUserActive.globalContext";
|
|
7
7
|
|
|
8
8
|
declare global {
|
|
9
9
|
interface Window {
|
|
10
10
|
[GLOBAL_CONTEXT_KEY]: {
|
|
11
11
|
appInstanceId: string;
|
|
12
|
+
evtIsUserActiveBySessionId: Map<string, NonPostableEvt<boolean>>;
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
window[GLOBAL_CONTEXT_KEY] ??= {
|
|
17
|
-
appInstanceId: Math.random().toString(36).slice(2)
|
|
18
|
+
appInstanceId: Math.random().toString(36).slice(2),
|
|
19
|
+
evtIsUserActiveBySessionId: new Map()
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
const globalContext = window[GLOBAL_CONTEXT_KEY];
|
|
21
23
|
|
|
22
|
-
export function
|
|
24
|
+
export function createEvtIsUserActive(params: {
|
|
25
|
+
configId: string;
|
|
26
|
+
sessionId: string | undefined;
|
|
27
|
+
}): NonPostableEvt<boolean> {
|
|
23
28
|
const { configId, sessionId } = params;
|
|
24
29
|
|
|
30
|
+
use_existing_instance: {
|
|
31
|
+
if (sessionId === undefined) {
|
|
32
|
+
break use_existing_instance;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const evtIsUserActive = globalContext.evtIsUserActiveBySessionId.get(sessionId);
|
|
36
|
+
|
|
37
|
+
if (evtIsUserActive === undefined) {
|
|
38
|
+
break use_existing_instance;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return evtIsUserActive;
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
const { notifyOtherTabsOfUserInteraction, subscribeToUserInteractionOnOtherTabs } = (() => {
|
|
26
45
|
type Message = {
|
|
27
46
|
appInstanceId: string;
|
|
@@ -54,12 +73,14 @@ export function create$isUserActive(params: { configId: string; sessionId: strin
|
|
|
54
73
|
return { notifyOtherTabsOfUserInteraction, subscribeToUserInteractionOnOtherTabs };
|
|
55
74
|
})();
|
|
56
75
|
|
|
57
|
-
const
|
|
76
|
+
const evtIsUserActive = createEvt<boolean>();
|
|
77
|
+
let isUserActive = true;
|
|
58
78
|
|
|
59
79
|
const scheduleSetInactive = () => {
|
|
60
80
|
const timer = setTimeout(() => {
|
|
61
|
-
assert(
|
|
62
|
-
|
|
81
|
+
assert(isUserActive);
|
|
82
|
+
isUserActive = false;
|
|
83
|
+
evtIsUserActive.post(isUserActive);
|
|
63
84
|
}, 5_000);
|
|
64
85
|
return () => {
|
|
65
86
|
clearTimeout(timer);
|
|
@@ -79,8 +100,9 @@ export function create$isUserActive(params: { configId: string; sessionId: strin
|
|
|
79
100
|
notifyOtherTabsOfUserInteraction();
|
|
80
101
|
}
|
|
81
102
|
|
|
82
|
-
if (
|
|
83
|
-
|
|
103
|
+
if (!isUserActive) {
|
|
104
|
+
isUserActive = true;
|
|
105
|
+
evtIsUserActive.post(isUserActive);
|
|
84
106
|
}
|
|
85
107
|
};
|
|
86
108
|
|
|
@@ -91,5 +113,9 @@ export function create$isUserActive(params: { configId: string; sessionId: strin
|
|
|
91
113
|
|
|
92
114
|
subscribeToUserInteractionOnOtherTabs(() => onUserActivity({ isInteractionOnCurrentTab: false }));
|
|
93
115
|
|
|
94
|
-
|
|
116
|
+
if (sessionId !== undefined) {
|
|
117
|
+
globalContext.evtIsUserActiveBySessionId.set(sessionId, evtIsUserActive);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return evtIsUserActive;
|
|
95
121
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getStateData,
|
|
3
|
+
markStateDataAsProcessedByCallback,
|
|
4
|
+
getIsStatQueryParamValue,
|
|
5
|
+
type StateData
|
|
6
|
+
} from "./StateData";
|
|
7
|
+
import { assert } from "../vendor/frontend/tsafe";
|
|
8
|
+
import type { AuthResponse } from "./AuthResponse";
|
|
2
9
|
|
|
3
10
|
const GLOBAL_CONTEXT_KEY = "__oidc-spa.handleOidcCallback.globalContext";
|
|
4
11
|
|
|
@@ -24,8 +31,6 @@ export function handleOidcCallback(): { isHandled: boolean } {
|
|
|
24
31
|
return (globalContext.previousCall = handleOidcCallback_nonMemoized());
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
export const AUTH_RESPONSE_KEY = "oidc-spa.authResponse";
|
|
28
|
-
|
|
29
34
|
function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
|
|
30
35
|
const locationUrl = new URL(window.location.href);
|
|
31
36
|
|
|
@@ -80,7 +85,7 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
|
|
|
80
85
|
stateData === undefined ||
|
|
81
86
|
(stateData.context === "redirect" && stateData.hasBeenProcessedByCallback)
|
|
82
87
|
) {
|
|
83
|
-
|
|
88
|
+
reloadOnBfCacheNavigation();
|
|
84
89
|
|
|
85
90
|
const historyMethod: "back" | "forward" = (() => {
|
|
86
91
|
const backForwardTracker = readBackForwardTracker();
|
|
@@ -113,21 +118,25 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
|
|
|
113
118
|
return { isHandled };
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
const authResponse:
|
|
121
|
+
const authResponse: AuthResponse = { state: "" };
|
|
117
122
|
|
|
118
123
|
for (const [key, value] of locationUrl.searchParams) {
|
|
119
124
|
authResponse[key] = value;
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
assert(authResponse.state !== "");
|
|
128
|
+
|
|
122
129
|
switch (stateData.context) {
|
|
123
130
|
case "iframe":
|
|
124
131
|
parent.postMessage(authResponse, location.origin);
|
|
125
132
|
break;
|
|
126
133
|
case "redirect":
|
|
127
|
-
|
|
134
|
+
reloadOnBfCacheNavigation();
|
|
128
135
|
markStateDataAsProcessedByCallback({ stateQueryParamValue });
|
|
129
136
|
clearBackForwardTracker();
|
|
130
|
-
|
|
137
|
+
writeRedirectAuthResponses({
|
|
138
|
+
authResponses: [...readRedirectAuthResponses(), authResponse]
|
|
139
|
+
});
|
|
131
140
|
location.href = (() => {
|
|
132
141
|
if (stateData.action === "login" && authResponse.error === "consent_required") {
|
|
133
142
|
return stateData.redirectUrl_consentRequiredCase;
|
|
@@ -141,16 +150,68 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
|
|
|
141
150
|
return { isHandled };
|
|
142
151
|
}
|
|
143
152
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
const { readRedirectAuthResponses, writeRedirectAuthResponses } = (() => {
|
|
154
|
+
const AUTH_RESPONSES_KEY = "oidc-spa:authResponses";
|
|
155
|
+
|
|
156
|
+
function writeRedirectAuthResponses(params: { authResponses: AuthResponse[] }): void {
|
|
157
|
+
const { authResponses } = params;
|
|
158
|
+
sessionStorage.setItem(AUTH_RESPONSES_KEY, JSON.stringify(authResponses));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readRedirectAuthResponses(): AuthResponse[] {
|
|
162
|
+
const raw = sessionStorage.getItem(AUTH_RESPONSES_KEY);
|
|
163
|
+
|
|
164
|
+
if (raw === null) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return JSON.parse(raw);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { writeRedirectAuthResponses, readRedirectAuthResponses };
|
|
172
|
+
})();
|
|
173
|
+
|
|
174
|
+
export function retrieveRedirectAuthResponseAndStateData(params: {
|
|
175
|
+
configId: string;
|
|
176
|
+
}): { authResponse: AuthResponse; stateData: StateData.Redirect } | undefined {
|
|
177
|
+
const { configId } = params;
|
|
178
|
+
|
|
179
|
+
const authResponses = readRedirectAuthResponses();
|
|
180
|
+
|
|
181
|
+
let authResponseAndStateData:
|
|
182
|
+
| { authResponse: AuthResponse; stateData: StateData.Redirect }
|
|
183
|
+
| undefined = undefined;
|
|
184
|
+
|
|
185
|
+
for (const authResponse of [...authResponses]) {
|
|
186
|
+
const stateData = getStateData({ stateQueryParamValue: authResponse.state });
|
|
187
|
+
|
|
188
|
+
assert(stateData !== undefined);
|
|
189
|
+
assert(stateData.context === "redirect");
|
|
190
|
+
|
|
191
|
+
if (stateData.configId !== configId) {
|
|
192
|
+
continue;
|
|
148
193
|
}
|
|
194
|
+
|
|
195
|
+
authResponses.splice(authResponses.indexOf(authResponse), 1);
|
|
196
|
+
|
|
197
|
+
authResponseAndStateData = { authResponse, stateData };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (authResponseAndStateData !== undefined) {
|
|
201
|
+
writeRedirectAuthResponses({ authResponses });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return authResponseAndStateData;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function reloadOnBfCacheNavigation() {
|
|
208
|
+
window.addEventListener("pageshow", () => {
|
|
209
|
+
location.reload();
|
|
149
210
|
});
|
|
150
211
|
}
|
|
151
212
|
|
|
152
213
|
const { writeBackForwardTracker, readBackForwardTracker, clearBackForwardTracker } = (() => {
|
|
153
|
-
const BACK_NAVIGATION_TRACKER_KEY = "oidc-spa
|
|
214
|
+
const BACK_NAVIGATION_TRACKER_KEY = "oidc-spa:callback-back-forward-tracker";
|
|
154
215
|
|
|
155
216
|
type BackForwardTracker = {
|
|
156
217
|
previousHistoryMethod: "back" | "forward";
|
|
@@ -2,21 +2,22 @@ import type { UserManager as OidcClientTsUserManager } from "../vendor/frontend/
|
|
|
2
2
|
import { toFullyQualifiedUrl } from "../tools/toFullyQualifiedUrl";
|
|
3
3
|
import { id, assert, type Equals } from "../vendor/frontend/tsafe";
|
|
4
4
|
import type { StateData } from "./StateData";
|
|
5
|
+
import type { NonPostableEvt } from "../tools/Evt";
|
|
6
|
+
import { type StatefulEvt, createStatefulEvt } from "../tools/StatefulEvt";
|
|
7
|
+
import { Deferred } from "../tools/Deferred";
|
|
5
8
|
|
|
6
9
|
const GLOBAL_CONTEXT_KEY = "__oidc-spa.loginOrGoToAuthSever.globalContext";
|
|
7
10
|
|
|
8
11
|
declare global {
|
|
9
12
|
interface Window {
|
|
10
13
|
[GLOBAL_CONTEXT_KEY]: {
|
|
11
|
-
|
|
12
|
-
URL_real: typeof URL;
|
|
14
|
+
evtHasLoginBeenCalled: StatefulEvt<boolean>;
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
window[GLOBAL_CONTEXT_KEY] ??= {
|
|
18
|
-
|
|
19
|
-
URL_real: window.URL
|
|
20
|
+
evtHasLoginBeenCalled: createStatefulEvt(() => false)
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
const globalContext = window[GLOBAL_CONTEXT_KEY];
|
|
@@ -42,12 +43,26 @@ namespace Params {
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export function getPrSafelyRestoredFromBfCacheAfterLoginBackNavigation() {
|
|
47
|
+
const dOut = new Deferred<void>();
|
|
48
|
+
|
|
49
|
+
const { unsubscribe } = globalContext.evtHasLoginBeenCalled.subscribe(hasLoginBeenCalled => {
|
|
50
|
+
if (!hasLoginBeenCalled) {
|
|
51
|
+
unsubscribe();
|
|
52
|
+
dOut.resolve();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return dOut.pr;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
export function createLoginOrGoToAuthServer(params: {
|
|
46
60
|
configId: string;
|
|
47
61
|
oidcClientTsUserManager: OidcClientTsUserManager;
|
|
48
62
|
getExtraQueryParams: (() => Record<string, string>) | undefined;
|
|
49
63
|
transformUrlBeforeRedirect: ((url: string) => string) | undefined;
|
|
50
64
|
homeAndCallbackUrl: string;
|
|
65
|
+
evtIsUserLoggedIn: NonPostableEvt<boolean>;
|
|
51
66
|
log: typeof console.log | undefined;
|
|
52
67
|
}) {
|
|
53
68
|
const {
|
|
@@ -56,14 +71,15 @@ export function createLoginOrGoToAuthServer(params: {
|
|
|
56
71
|
getExtraQueryParams,
|
|
57
72
|
transformUrlBeforeRedirect,
|
|
58
73
|
homeAndCallbackUrl,
|
|
74
|
+
evtIsUserLoggedIn,
|
|
59
75
|
log
|
|
60
76
|
} = params;
|
|
61
77
|
|
|
62
|
-
const
|
|
78
|
+
const LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_USER_LOGGED_IN = `oidc-spa.login-redirect-initiated:${configId}`;
|
|
63
79
|
|
|
64
80
|
let lastPublicUrl: string | undefined = undefined;
|
|
65
81
|
|
|
66
|
-
|
|
82
|
+
function loginOrGoToAuthServer(params: Params): Promise<never> {
|
|
67
83
|
const {
|
|
68
84
|
redirectUrl: redirectUrl_params,
|
|
69
85
|
extraQueryParams_local,
|
|
@@ -78,64 +94,54 @@ export function createLoginOrGoToAuthServer(params: {
|
|
|
78
94
|
break login_specific_handling;
|
|
79
95
|
}
|
|
80
96
|
|
|
81
|
-
if (globalContext.
|
|
97
|
+
if (globalContext.evtHasLoginBeenCalled.current) {
|
|
82
98
|
log?.("login() has already been called, ignoring the call");
|
|
83
99
|
return new Promise<never>(() => {});
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
globalContext.
|
|
102
|
+
globalContext.evtHasLoginBeenCalled.current = true;
|
|
87
103
|
|
|
88
104
|
bf_cache_handling: {
|
|
89
105
|
if (rest.doForceReloadOnBfCache) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
location.reload();
|
|
93
|
-
}
|
|
106
|
+
window.removeEventListener("pageshow", () => {
|
|
107
|
+
location.reload();
|
|
94
108
|
});
|
|
95
109
|
break bf_cache_handling;
|
|
96
110
|
}
|
|
97
111
|
|
|
98
|
-
localStorage.setItem(
|
|
112
|
+
localStorage.setItem(LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_USER_LOGGED_IN, "true");
|
|
99
113
|
|
|
100
114
|
const callback = () => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
window.removeEventListener("pageshow", callback);
|
|
116
|
+
|
|
117
|
+
log?.(
|
|
118
|
+
"We came back from the login pages and the state of the app has been restored"
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (rest.doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack) {
|
|
122
|
+
if (lastPublicUrl !== undefined) {
|
|
123
|
+
log?.(`Loading last public route: ${lastPublicUrl}`);
|
|
124
|
+
window.location.href = lastPublicUrl;
|
|
125
|
+
} else {
|
|
126
|
+
log?.("We don't know the last public route, navigating back in history");
|
|
127
|
+
window.history.back();
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
log?.("The current page doesn't require auth...");
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
localStorage.getItem(LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_USER_LOGGED_IN) === null
|
|
134
|
+
) {
|
|
135
|
+
log?.("but the user is now authenticated, reloading the page");
|
|
136
|
+
location.reload();
|
|
116
137
|
} else {
|
|
117
|
-
log?.("
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
localStorage.getItem(
|
|
121
|
-
LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN
|
|
122
|
-
) === null
|
|
123
|
-
) {
|
|
124
|
-
log?.("but the user is now authenticated, reloading the page");
|
|
125
|
-
location.reload();
|
|
126
|
-
} else {
|
|
127
|
-
log?.(
|
|
128
|
-
"and the user doesn't seem to be authenticated, avoiding a reload"
|
|
129
|
-
);
|
|
130
|
-
globalContext.hasLoginBeenCalled = false;
|
|
131
|
-
}
|
|
138
|
+
log?.("and the user doesn't seem to be authenticated, avoiding a reload");
|
|
139
|
+
globalContext.evtHasLoginBeenCalled.current = false;
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
};
|
|
135
143
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
document.addEventListener("visibilitychange", callback);
|
|
144
|
+
window.addEventListener("pageshow", callback);
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -215,53 +221,54 @@ export function createLoginOrGoToAuthServer(params: {
|
|
|
215
221
|
return { extraQueryParams };
|
|
216
222
|
})();
|
|
217
223
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
return oidcClientTsUserManager
|
|
225
|
+
.signinRedirect({
|
|
226
|
+
state: id<StateData>({
|
|
227
|
+
context: "redirect",
|
|
228
|
+
redirectUrl,
|
|
229
|
+
extraQueryParams,
|
|
230
|
+
hasBeenProcessedByCallback: false,
|
|
231
|
+
configId,
|
|
232
|
+
action: "login",
|
|
233
|
+
redirectUrl_consentRequiredCase: (() => {
|
|
234
|
+
switch (rest.action) {
|
|
235
|
+
case "login":
|
|
236
|
+
return lastPublicUrl ?? homeAndCallbackUrl;
|
|
237
|
+
case "go to auth server":
|
|
238
|
+
return redirectUrl;
|
|
239
|
+
}
|
|
240
|
+
})()
|
|
241
|
+
}),
|
|
242
|
+
redirectMethod,
|
|
243
|
+
prompt: (() => {
|
|
227
244
|
switch (rest.action) {
|
|
228
|
-
case "login":
|
|
229
|
-
return lastPublicUrl ?? homeAndCallbackUrl;
|
|
230
245
|
case "go to auth server":
|
|
231
|
-
return
|
|
246
|
+
return undefined;
|
|
247
|
+
case "login":
|
|
248
|
+
return rest.doForceInteraction ? "consent" : undefined;
|
|
232
249
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
case "go to auth server":
|
|
239
|
-
return undefined;
|
|
240
|
-
case "login":
|
|
241
|
-
return rest.doForceInteraction ? "consent" : undefined;
|
|
242
|
-
}
|
|
243
|
-
assert<Equals<typeof rest, never>>;
|
|
244
|
-
})(),
|
|
245
|
-
transformUrl: transformUrl_oidcClientTs
|
|
246
|
-
});
|
|
247
|
-
return new Promise<never>(() => {});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function toCallBeforeReturningOidcLoggedIn() {
|
|
251
|
-
localStorage.removeItem(LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN);
|
|
250
|
+
assert<Equals<typeof rest, never>>;
|
|
251
|
+
})(),
|
|
252
|
+
transformUrl: transformUrl_oidcClientTs
|
|
253
|
+
})
|
|
254
|
+
.then(() => new Promise<never>(() => {}));
|
|
252
255
|
}
|
|
253
256
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
257
|
+
const { unsubscribe } = evtIsUserLoggedIn.subscribe(isLoggedIn => {
|
|
258
|
+
unsubscribe();
|
|
259
|
+
|
|
260
|
+
if (isLoggedIn) {
|
|
261
|
+
localStorage.removeItem(LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_USER_LOGGED_IN);
|
|
262
|
+
} else {
|
|
263
|
+
const realPushState = history.pushState.bind(history);
|
|
264
|
+
history.pushState = function pushState(...args) {
|
|
265
|
+
lastPublicUrl = window.location.href;
|
|
266
|
+
return realPushState(...args);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
});
|
|
261
270
|
|
|
262
271
|
return {
|
|
263
|
-
loginOrGoToAuthServer
|
|
264
|
-
toCallBeforeReturningOidcLoggedIn,
|
|
265
|
-
toCallBeforeReturningOidcNotLoggedIn
|
|
272
|
+
loginOrGoToAuthServer
|
|
266
273
|
};
|
|
267
274
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { assert, is } from "../vendor/frontend/tsafe";
|
|
2
|
+
import { Deferred } from "../tools/Deferred";
|
|
3
|
+
|
|
4
|
+
const GLOBAL_CONTEXT_KEY = "__oidc-spa.loginPropagationToOtherTabs.globalContext";
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
[GLOBAL_CONTEXT_KEY]: {
|
|
9
|
+
appInstanceId: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
window[GLOBAL_CONTEXT_KEY] ??= {
|
|
15
|
+
appInstanceId: Math.random().toString(36).slice(2)
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const globalContext = window[GLOBAL_CONTEXT_KEY];
|
|
19
|
+
|
|
20
|
+
type Message = {
|
|
21
|
+
appInstanceId: string;
|
|
22
|
+
configId: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function getChannelName(params: { configId: string }) {
|
|
26
|
+
const { configId } = params;
|
|
27
|
+
return `oidc-spa:login-propagation:${configId}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function notifyOtherTabsOfLogin(params: { configId: string }) {
|
|
31
|
+
const { configId } = params;
|
|
32
|
+
|
|
33
|
+
const message: Message = {
|
|
34
|
+
configId,
|
|
35
|
+
appInstanceId: globalContext.appInstanceId
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
new BroadcastChannel(getChannelName({ configId })).postMessage(message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getPrOtherTabLogin(params: { configId: string }) {
|
|
42
|
+
const { configId } = params;
|
|
43
|
+
|
|
44
|
+
const dOtherTabLogin = new Deferred<void>();
|
|
45
|
+
|
|
46
|
+
const channel = new BroadcastChannel(getChannelName({ configId }));
|
|
47
|
+
|
|
48
|
+
channel.onmessage = ({ data: message }) => {
|
|
49
|
+
assert(is<Message>(message));
|
|
50
|
+
|
|
51
|
+
if (message.appInstanceId === globalContext.appInstanceId) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
channel.close();
|
|
56
|
+
|
|
57
|
+
dOtherTabLogin.resolve();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const prOtherTabLogin = dOtherTabLogin.pr;
|
|
61
|
+
|
|
62
|
+
return { prOtherTabLogin };
|
|
63
|
+
}
|
package/src/oidc/loginSilent.ts
CHANGED
|
@@ -5,25 +5,7 @@ import { getStateData, clearStateStore, type StateData } from "./StateData";
|
|
|
5
5
|
import { getDownlinkAndRtt } from "../tools/getDownlinkAndRtt";
|
|
6
6
|
import { getIsDev } from "../tools/isDev";
|
|
7
7
|
import type { User as OidcClientTsUser } from "../vendor/frontend/oidc-client-ts-and-jwt-decode";
|
|
8
|
-
|
|
9
|
-
export type AuthResponse = {
|
|
10
|
-
state: string;
|
|
11
|
-
[key: string]: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function getIsAuthResponse(data: any): data is AuthResponse {
|
|
15
|
-
return data instanceof Object && "state" in data && typeof data.state === "string";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function authResponseToUrl(authResponse: AuthResponse): string {
|
|
19
|
-
const authResponseUrl = new URL("https://dummy.com");
|
|
20
|
-
|
|
21
|
-
for (const [name, value] of Object.entries(authResponse)) {
|
|
22
|
-
authResponseUrl.searchParams.set(name, value);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return authResponseUrl.href;
|
|
26
|
-
}
|
|
8
|
+
import { type AuthResponse, getIsAuthResponse } from "./AuthResponse";
|
|
27
9
|
|
|
28
10
|
type ResultOfLoginSilent =
|
|
29
11
|
| {
|
|
@@ -117,7 +99,7 @@ export async function loginSilent(params: {
|
|
|
117
99
|
})
|
|
118
100
|
.then(
|
|
119
101
|
oidcClientTsUser => {
|
|
120
|
-
assert(oidcClientTsUser !== null);
|
|
102
|
+
assert(oidcClientTsUser !== null, "oidcClientTsUser is not supposed to be null here");
|
|
121
103
|
|
|
122
104
|
clearTimeout(timeout);
|
|
123
105
|
|
|
@@ -60,12 +60,12 @@ export function getPrOtherTabLogout(params: {
|
|
|
60
60
|
channel.onmessage = ({ data: message }) => {
|
|
61
61
|
assert(is<Message>(message));
|
|
62
62
|
|
|
63
|
-
channel.close();
|
|
64
|
-
|
|
65
63
|
if (message.appInstanceId === globalContext.appInstanceId) {
|
|
66
64
|
return;
|
|
67
65
|
}
|
|
68
66
|
|
|
67
|
+
channel.close();
|
|
68
|
+
|
|
69
69
|
const redirectUrl = (() => {
|
|
70
70
|
if (configId === message.configId) {
|
|
71
71
|
return message.redirectUrl_initiator;
|