oidc-spa 6.5.2 → 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.
Files changed (81) hide show
  1. package/mock/oidc.js +6 -5
  2. package/mock/oidc.js.map +1 -1
  3. package/oidc/AuthResponse.d.ts +6 -0
  4. package/oidc/AuthResponse.js +59 -0
  5. package/oidc/AuthResponse.js.map +1 -0
  6. package/oidc/Oidc.d.ts +19 -8
  7. package/oidc/createOidc.d.ts +4 -4
  8. package/oidc/createOidc.js +359 -295
  9. package/oidc/createOidc.js.map +1 -1
  10. package/oidc/evtIsUserActive.d.ts +15 -0
  11. package/oidc/{isUserActive.js → evtIsUserActive.js} +29 -12
  12. package/oidc/evtIsUserActive.js.map +1 -0
  13. package/oidc/handleOidcCallback.d.ts +8 -1
  14. package/oidc/handleOidcCallback.js +68 -13
  15. package/oidc/handleOidcCallback.js.map +1 -1
  16. package/oidc/loginOrGoToAuthServer.d.ts +5 -4
  17. package/oidc/loginOrGoToAuthServer.js +190 -227
  18. package/oidc/loginOrGoToAuthServer.js.map +1 -1
  19. package/oidc/loginPropagationToOtherTabs.d.ts +17 -0
  20. package/oidc/loginPropagationToOtherTabs.js +41 -0
  21. package/oidc/loginPropagationToOtherTabs.js.map +1 -0
  22. package/oidc/loginSilent.d.ts +1 -5
  23. package/oidc/loginSilent.js +3 -51
  24. package/oidc/loginSilent.js.map +1 -1
  25. package/oidc/logoutPropagationToOtherTabs.js +1 -1
  26. package/oidc/logoutPropagationToOtherTabs.js.map +1 -1
  27. package/oidc/oidcClientTsUserToTokens.d.ts +1 -1
  28. package/oidc/oidcClientTsUserToTokens.js +45 -23
  29. package/oidc/oidcClientTsUserToTokens.js.map +1 -1
  30. package/oidc/ongoingLoginOrRefreshProcesses.d.ts +16 -0
  31. package/oidc/ongoingLoginOrRefreshProcesses.js +102 -0
  32. package/oidc/ongoingLoginOrRefreshProcesses.js.map +1 -0
  33. package/oidc/persistedAuthState.d.ts +16 -3
  34. package/oidc/persistedAuthState.js +35 -4
  35. package/oidc/persistedAuthState.js.map +1 -1
  36. package/package.json +36 -21
  37. package/react/react.js +8 -14
  38. package/react/react.js.map +1 -1
  39. package/src/mock/oidc.ts +14 -3
  40. package/src/oidc/AuthResponse.ts +26 -0
  41. package/src/oidc/Oidc.ts +19 -4
  42. package/src/oidc/createOidc.ts +233 -206
  43. package/src/oidc/{isUserActive.ts → evtIsUserActive.ts} +36 -10
  44. package/src/oidc/handleOidcCallback.ts +73 -12
  45. package/src/oidc/loginOrGoToAuthServer.ts +94 -87
  46. package/src/oidc/loginPropagationToOtherTabs.ts +63 -0
  47. package/src/oidc/loginSilent.ts +2 -20
  48. package/src/oidc/logoutPropagationToOtherTabs.ts +2 -2
  49. package/src/oidc/oidcClientTsUserToTokens.ts +74 -35
  50. package/src/oidc/ongoingLoginOrRefreshProcesses.ts +60 -0
  51. package/src/oidc/persistedAuthState.ts +66 -8
  52. package/src/react/react.tsx +8 -16
  53. package/src/tools/{ephemeralSessionStorage.ts → EphemeralSessionStorage.ts} +59 -27
  54. package/src/tools/Evt.ts +56 -0
  55. package/src/tools/StatefulEvt.ts +38 -0
  56. package/src/tools/subscribeToUserInteraction.ts +0 -1
  57. package/src/tools/workerTimers.ts +10 -12
  58. package/tools/EphemeralSessionStorage.d.ts +12 -0
  59. package/tools/{ephemeralSessionStorage.js → EphemeralSessionStorage.js} +29 -16
  60. package/tools/EphemeralSessionStorage.js.map +1 -0
  61. package/tools/Evt.d.ts +11 -0
  62. package/tools/{AwaitableEventEmitter.js → Evt.js} +24 -8
  63. package/tools/Evt.js.map +1 -0
  64. package/tools/StatefulEvt.d.ts +12 -0
  65. package/tools/StatefulEvt.js +24 -0
  66. package/tools/StatefulEvt.js.map +1 -0
  67. package/tools/subscribeToUserInteraction.js +2 -3
  68. package/tools/subscribeToUserInteraction.js.map +1 -1
  69. package/tools/workerTimers.js +11 -13
  70. package/tools/workerTimers.js.map +1 -1
  71. package/oidc/isUserActive.d.ts +0 -13
  72. package/oidc/isUserActive.js.map +0 -1
  73. package/src/tools/AwaitableEventEmitter.ts +0 -33
  74. package/src/tools/StatefulObservable.ts +0 -52
  75. package/tools/AwaitableEventEmitter.d.ts +0 -5
  76. package/tools/AwaitableEventEmitter.js.map +0 -1
  77. package/tools/StatefulObservable.d.ts +0 -12
  78. package/tools/StatefulObservable.js +0 -33
  79. package/tools/StatefulObservable.js.map +0 -1
  80. package/tools/ephemeralSessionStorage.d.ts +0 -3
  81. package/tools/ephemeralSessionStorage.js.map +0 -1
@@ -1,27 +1,46 @@
1
- import { createStatefulObservable } from "../tools/StatefulObservable";
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.createIsUserActive.globalContext";
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 create$isUserActive(params: { configId: string; sessionId: string | undefined }) {
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 $isUserActive = createStatefulObservable(() => true);
76
+ const evtIsUserActive = createEvt<boolean>();
77
+ let isUserActive = true;
58
78
 
59
79
  const scheduleSetInactive = () => {
60
80
  const timer = setTimeout(() => {
61
- assert($isUserActive.current);
62
- $isUserActive.current = false;
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 (!$isUserActive.current) {
83
- $isUserActive.current = true;
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
- return $isUserActive;
116
+ if (sessionId !== undefined) {
117
+ globalContext.evtIsUserActiveBySessionId.set(sessionId, evtIsUserActive);
118
+ }
119
+
120
+ return evtIsUserActive;
95
121
  }
@@ -1,4 +1,11 @@
1
- import { getStateData, markStateDataAsProcessedByCallback, getIsStatQueryParamValue } from "./StateData";
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
- reloadOnRestore();
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: Record<string, string> = {};
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
- reloadOnRestore();
134
+ reloadOnBfCacheNavigation();
128
135
  markStateDataAsProcessedByCallback({ stateQueryParamValue });
129
136
  clearBackForwardTracker();
130
- sessionStorage.setItem(AUTH_RESPONSE_KEY, JSON.stringify(authResponse));
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
- function reloadOnRestore() {
145
- document.addEventListener("visibilitychange", () => {
146
- if (document.visibilityState === "visible") {
147
- location.reload();
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.callback-back-forward-tracker";
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
- hasLoginBeenCalled: boolean;
12
- URL_real: typeof URL;
14
+ evtHasLoginBeenCalled: StatefulEvt<boolean>;
13
15
  };
14
16
  }
15
17
  }
16
18
 
17
19
  window[GLOBAL_CONTEXT_KEY] ??= {
18
- hasLoginBeenCalled: false,
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 LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN = `oidc-spa.login-redirect-initiated:${configId}`;
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
- async function loginOrGoToAuthServer(params: Params): Promise<never> {
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.hasLoginBeenCalled) {
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.hasLoginBeenCalled = true;
102
+ globalContext.evtHasLoginBeenCalled.current = true;
87
103
 
88
104
  bf_cache_handling: {
89
105
  if (rest.doForceReloadOnBfCache) {
90
- document.removeEventListener("visibilitychange", () => {
91
- if (document.visibilityState === "visible") {
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(LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN, "true");
112
+ localStorage.setItem(LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_USER_LOGGED_IN, "true");
99
113
 
100
114
  const callback = () => {
101
- if (document.visibilityState === "visible") {
102
- document.removeEventListener("visibilitychange", callback);
103
-
104
- log?.(
105
- "We came back from the login pages and the state of the app has been restored"
106
- );
107
-
108
- if (rest.doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack) {
109
- if (lastPublicUrl !== undefined) {
110
- log?.(`Loading last public route: ${lastPublicUrl}`);
111
- window.location.href = lastPublicUrl;
112
- } else {
113
- log?.("We don't know the last public route, navigating back in history");
114
- window.history.back();
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?.("The current page doesn't require auth...");
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
- log?.("Start listening to visibility change event");
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
- await oidcClientTsUserManager.signinRedirect({
219
- state: id<StateData>({
220
- context: "redirect",
221
- redirectUrl,
222
- extraQueryParams,
223
- hasBeenProcessedByCallback: false,
224
- configId,
225
- action: "login",
226
- redirectUrl_consentRequiredCase: (() => {
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 redirectUrl;
246
+ return undefined;
247
+ case "login":
248
+ return rest.doForceInteraction ? "consent" : undefined;
232
249
  }
233
- })()
234
- }),
235
- redirectMethod,
236
- prompt: (() => {
237
- switch (rest.action) {
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
- function toCallBeforeReturningOidcNotLoggedIn() {
255
- const realPushState = history.pushState.bind(history);
256
- history.pushState = function pushState(...args) {
257
- lastPublicUrl = window.location.href;
258
- return realPushState(...args);
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
+ }
@@ -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;