oidc-spa 7.2.1 → 7.2.2

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 (197) hide show
  1. package/backend.js.map +1 -1
  2. package/core/AuthResponse.js.map +1 -1
  3. package/core/Oidc.js.map +1 -1
  4. package/core/OidcInitializationError.js.map +1 -1
  5. package/core/OidcMetadata.js.map +1 -1
  6. package/core/StateData.js.map +1 -1
  7. package/core/configId.js.map +1 -1
  8. package/core/createOidc.js +1 -1
  9. package/core/createOidc.js.map +1 -1
  10. package/core/diagnostic.js.map +1 -1
  11. package/core/evtIsUserActive.js.map +1 -1
  12. package/core/handleOidcCallback.js.map +1 -1
  13. package/core/iframeMessageProtection.js.map +1 -1
  14. package/core/index.js.map +1 -1
  15. package/core/initialLocationHref.js.map +1 -1
  16. package/core/isNewBrowserSession.js.map +1 -1
  17. package/core/loginOrGoToAuthServer.js.map +1 -1
  18. package/core/loginPropagationToOtherTabs.js.map +1 -1
  19. package/core/loginSilent.js.map +1 -1
  20. package/core/logoutPropagationToOtherTabs.js.map +1 -1
  21. package/core/oidcClientTsUserToTokens.js.map +1 -1
  22. package/core/ongoingLoginOrRefreshProcesses.js.map +1 -1
  23. package/core/persistedAuthState.js.map +1 -1
  24. package/entrypoint.js.map +1 -1
  25. package/esm/core/AuthResponse.js.map +1 -1
  26. package/esm/core/Oidc.js.map +1 -1
  27. package/esm/core/OidcInitializationError.js.map +1 -1
  28. package/esm/core/OidcMetadata.js.map +1 -1
  29. package/esm/core/StateData.js.map +1 -1
  30. package/esm/core/configId.js.map +1 -1
  31. package/esm/core/createOidc.js +1 -1
  32. package/esm/core/createOidc.js.map +1 -1
  33. package/esm/core/diagnostic.js.map +1 -1
  34. package/esm/core/evtIsUserActive.js.map +1 -1
  35. package/esm/core/handleOidcCallback.js.map +1 -1
  36. package/esm/core/iframeMessageProtection.js.map +1 -1
  37. package/esm/core/index.js.map +1 -1
  38. package/esm/core/initialLocationHref.js.map +1 -1
  39. package/esm/core/isNewBrowserSession.js.map +1 -1
  40. package/esm/core/loginOrGoToAuthServer.js.map +1 -1
  41. package/esm/core/loginPropagationToOtherTabs.js.map +1 -1
  42. package/esm/core/loginSilent.js.map +1 -1
  43. package/esm/core/logoutPropagationToOtherTabs.js.map +1 -1
  44. package/esm/core/oidcClientTsUserToTokens.js.map +1 -1
  45. package/esm/core/ongoingLoginOrRefreshProcesses.js.map +1 -1
  46. package/esm/core/persistedAuthState.js.map +1 -1
  47. package/esm/entrypoint.js.map +1 -1
  48. package/esm/index.js.map +1 -1
  49. package/esm/keycloak/index.js.map +1 -1
  50. package/esm/keycloak/isKeycloak.js.map +1 -1
  51. package/esm/keycloak/keycloak-js/Keycloak.js.map +1 -1
  52. package/esm/keycloak/keycloak-js/index.js.map +1 -1
  53. package/esm/keycloak/keycloak-js/types.js.map +1 -1
  54. package/esm/keycloak/keycloakIssuerUriParsed.js.map +1 -1
  55. package/esm/keycloak/keycloakUtils.js.map +1 -1
  56. package/esm/keycloak-js.js.map +1 -1
  57. package/esm/mock/index.js.map +1 -1
  58. package/esm/mock/oidc.js.map +1 -1
  59. package/esm/mock/react.js.map +1 -1
  60. package/esm/react/index.js.map +1 -1
  61. package/esm/react/react.js.map +1 -1
  62. package/esm/tools/Deferred.js.map +1 -1
  63. package/esm/tools/EphemeralSessionStorage.js.map +1 -1
  64. package/esm/tools/Evt.js.map +1 -1
  65. package/esm/tools/StatefulEvt.js.map +1 -1
  66. package/esm/tools/ValueOrAsyncGetter.js.map +1 -1
  67. package/esm/tools/asymmetricEncryption.js.map +1 -1
  68. package/esm/tools/base64.js.map +1 -1
  69. package/esm/tools/createObjectThatThrowsIfAccessed.js.map +1 -1
  70. package/esm/tools/decodeJwt.js.map +1 -1
  71. package/esm/tools/generateUrlSafeRandom.js.map +1 -1
  72. package/esm/tools/getDownlinkAndRtt.js.map +1 -1
  73. package/esm/tools/getIsOnline.js.map +1 -1
  74. package/esm/tools/getIsValidRemoteJson.js.map +1 -1
  75. package/esm/tools/getPrUserInteraction.js.map +1 -1
  76. package/esm/tools/getUserEnvironmentInfo.js.map +1 -1
  77. package/esm/tools/haveSharedParentDomain.js.map +1 -1
  78. package/esm/tools/isDev.js.map +1 -1
  79. package/esm/tools/parseKeycloakIssuerUri.js.map +1 -1
  80. package/esm/tools/readExpirationTimeInJwt.js.map +1 -1
  81. package/esm/tools/startCountdown.js.map +1 -1
  82. package/esm/tools/subscribeToUserInteraction.js.map +1 -1
  83. package/esm/tools/toFullyQualifiedUrl.js.map +1 -1
  84. package/esm/tools/toHumanReadableDuration.js.map +1 -1
  85. package/esm/tools/urlSearchParams.js.map +1 -1
  86. package/esm/tools/workerTimers.js.map +1 -1
  87. package/index.js.map +1 -1
  88. package/keycloak/index.js.map +1 -1
  89. package/keycloak/isKeycloak.js.map +1 -1
  90. package/keycloak/keycloak-js/Keycloak.js.map +1 -1
  91. package/keycloak/keycloak-js/index.js.map +1 -1
  92. package/keycloak/keycloak-js/types.js.map +1 -1
  93. package/keycloak/keycloakIssuerUriParsed.js.map +1 -1
  94. package/keycloak/keycloakUtils.js.map +1 -1
  95. package/keycloak-js.js.map +1 -1
  96. package/mock/index.js.map +1 -1
  97. package/mock/oidc.js.map +1 -1
  98. package/mock/react.js.map +1 -1
  99. package/package.json +1 -1
  100. package/react/index.js.map +1 -1
  101. package/react/react.js.map +1 -1
  102. package/src/backend.ts +391 -0
  103. package/src/core/AuthResponse.ts +26 -0
  104. package/src/core/Oidc.ts +140 -0
  105. package/src/core/OidcInitializationError.ts +19 -0
  106. package/src/core/OidcMetadata.ts +271 -0
  107. package/src/core/StateData.ts +118 -0
  108. package/src/core/configId.ts +3 -0
  109. package/src/core/createOidc.ts +1576 -0
  110. package/src/core/diagnostic.ts +267 -0
  111. package/src/core/evtIsUserActive.ts +108 -0
  112. package/src/core/handleOidcCallback.ts +321 -0
  113. package/src/core/iframeMessageProtection.ts +100 -0
  114. package/src/core/index.ts +4 -0
  115. package/src/core/initialLocationHref.ts +5 -0
  116. package/src/core/isNewBrowserSession.ts +37 -0
  117. package/src/core/loginOrGoToAuthServer.ts +324 -0
  118. package/src/core/loginPropagationToOtherTabs.ts +51 -0
  119. package/src/core/loginSilent.ts +242 -0
  120. package/src/core/logoutPropagationToOtherTabs.ts +53 -0
  121. package/src/core/oidcClientTsUserToTokens.ts +229 -0
  122. package/src/core/ongoingLoginOrRefreshProcesses.ts +47 -0
  123. package/src/core/persistedAuthState.ts +122 -0
  124. package/src/entrypoint.ts +69 -0
  125. package/src/index.ts +1 -0
  126. package/src/keycloak/index.ts +8 -0
  127. package/src/keycloak/isKeycloak.ts +23 -0
  128. package/src/keycloak/keycloak-js/Keycloak.ts +1097 -0
  129. package/src/keycloak/keycloak-js/index.ts +2 -0
  130. package/src/keycloak/keycloak-js/types.ts +442 -0
  131. package/src/keycloak/keycloakIssuerUriParsed.ts +29 -0
  132. package/src/keycloak/keycloakUtils.ts +90 -0
  133. package/src/keycloak-js.ts +1 -0
  134. package/src/mock/index.ts +1 -0
  135. package/src/mock/oidc.ts +211 -0
  136. package/src/mock/react.tsx +11 -0
  137. package/src/react/index.ts +1 -0
  138. package/src/react/react.tsx +476 -0
  139. package/src/tools/Deferred.ts +33 -0
  140. package/src/tools/EphemeralSessionStorage.ts +223 -0
  141. package/src/tools/Evt.ts +56 -0
  142. package/src/tools/StatefulEvt.ts +38 -0
  143. package/src/tools/ValueOrAsyncGetter.ts +1 -0
  144. package/src/tools/asymmetricEncryption.ts +184 -0
  145. package/src/tools/base64.ts +7 -0
  146. package/src/tools/createObjectThatThrowsIfAccessed.ts +40 -0
  147. package/src/tools/decodeJwt.ts +95 -0
  148. package/src/tools/generateUrlSafeRandom.ts +26 -0
  149. package/src/tools/getDownlinkAndRtt.ts +22 -0
  150. package/src/tools/getIsOnline.ts +20 -0
  151. package/src/tools/getIsValidRemoteJson.ts +18 -0
  152. package/src/tools/getPrUserInteraction.ts +27 -0
  153. package/src/tools/getUserEnvironmentInfo.ts +42 -0
  154. package/src/tools/haveSharedParentDomain.ts +13 -0
  155. package/src/tools/isDev.ts +30 -0
  156. package/src/tools/parseKeycloakIssuerUri.ts +49 -0
  157. package/src/tools/readExpirationTimeInJwt.ts +16 -0
  158. package/src/tools/startCountdown.ts +36 -0
  159. package/src/tools/subscribeToUserInteraction.ts +33 -0
  160. package/src/tools/toFullyQualifiedUrl.ts +58 -0
  161. package/src/tools/toHumanReadableDuration.ts +21 -0
  162. package/src/tools/urlSearchParams.ts +130 -0
  163. package/src/tools/workerTimers.ts +57 -0
  164. package/src/vendor/backend/evt.ts +2 -0
  165. package/src/vendor/backend/jsonwebtoken.ts +1 -0
  166. package/src/vendor/backend/node-fetch.ts +2 -0
  167. package/src/vendor/backend/node-jose.ts +1 -0
  168. package/src/vendor/backend/tsafe.ts +5 -0
  169. package/src/vendor/backend/zod.ts +1 -0
  170. package/src/vendor/frontend/oidc-client-ts.ts +1 -0
  171. package/src/vendor/frontend/tsafe.ts +6 -0
  172. package/src/vendor/frontend/worker-timers.ts +2 -0
  173. package/tools/Deferred.js.map +1 -1
  174. package/tools/EphemeralSessionStorage.js.map +1 -1
  175. package/tools/Evt.js.map +1 -1
  176. package/tools/StatefulEvt.js.map +1 -1
  177. package/tools/ValueOrAsyncGetter.js.map +1 -1
  178. package/tools/asymmetricEncryption.js.map +1 -1
  179. package/tools/base64.js.map +1 -1
  180. package/tools/createObjectThatThrowsIfAccessed.js.map +1 -1
  181. package/tools/decodeJwt.js.map +1 -1
  182. package/tools/generateUrlSafeRandom.js.map +1 -1
  183. package/tools/getDownlinkAndRtt.js.map +1 -1
  184. package/tools/getIsOnline.js.map +1 -1
  185. package/tools/getIsValidRemoteJson.js.map +1 -1
  186. package/tools/getPrUserInteraction.js.map +1 -1
  187. package/tools/getUserEnvironmentInfo.js.map +1 -1
  188. package/tools/haveSharedParentDomain.js.map +1 -1
  189. package/tools/isDev.js.map +1 -1
  190. package/tools/parseKeycloakIssuerUri.js.map +1 -1
  191. package/tools/readExpirationTimeInJwt.js.map +1 -1
  192. package/tools/startCountdown.js.map +1 -1
  193. package/tools/subscribeToUserInteraction.js.map +1 -1
  194. package/tools/toFullyQualifiedUrl.js.map +1 -1
  195. package/tools/toHumanReadableDuration.js.map +1 -1
  196. package/tools/urlSearchParams.js.map +1 -1
  197. package/tools/workerTimers.js.map +1 -1
@@ -0,0 +1,242 @@
1
+ import type {
2
+ UserManager as OidcClientTsUserManager,
3
+ User as OidcClientTsUser
4
+ } from "../vendor/frontend/oidc-client-ts";
5
+ import { Deferred } from "../tools/Deferred";
6
+ import { id, assert, noUndefined } from "../vendor/frontend/tsafe";
7
+ import { getStateData, clearStateStore, type StateData } from "./StateData";
8
+ import { getDownlinkAndRtt } from "../tools/getDownlinkAndRtt";
9
+ import { getIsDev } from "../tools/isDev";
10
+ import { type AuthResponse } from "./AuthResponse";
11
+ import { addOrUpdateSearchParam } from "../tools/urlSearchParams";
12
+ import { initIframeMessageProtection } from "./iframeMessageProtection";
13
+
14
+ type ResultOfLoginSilent =
15
+ | {
16
+ outcome: "got auth response from iframe";
17
+ authResponse: AuthResponse;
18
+ }
19
+ | {
20
+ outcome: "failure";
21
+ cause: "timeout" | "can't reach well-known oidc endpoint";
22
+ }
23
+ | {
24
+ outcome: "token refreshed using refresh token";
25
+ oidcClientTsUser: OidcClientTsUser;
26
+ };
27
+
28
+ export async function loginSilent(params: {
29
+ oidcClientTsUserManager: OidcClientTsUserManager;
30
+ stateUrlParamValue_instance: string;
31
+ configId: string;
32
+
33
+ transformUrlBeforeRedirect:
34
+ | ((params: { authorizationUrl: string; isSilent: true }) => string)
35
+ | undefined;
36
+
37
+ getExtraQueryParams:
38
+ | ((params: { isSilent: true; url: string }) => Record<string, string | undefined>)
39
+ | undefined;
40
+
41
+ getExtraTokenParams: (() => Record<string, string | undefined>) | undefined;
42
+ autoLogin: boolean;
43
+ }): Promise<ResultOfLoginSilent> {
44
+ const {
45
+ oidcClientTsUserManager,
46
+ stateUrlParamValue_instance,
47
+ configId,
48
+ transformUrlBeforeRedirect,
49
+ getExtraQueryParams,
50
+ getExtraTokenParams,
51
+ autoLogin
52
+ } = params;
53
+
54
+ const dResult = new Deferred<ResultOfLoginSilent>();
55
+
56
+ const timeoutDelayMs: number = (() => {
57
+ const isDev = getIsDev();
58
+
59
+ const downlinkAndRtt = getDownlinkAndRtt();
60
+
61
+ // Base delay is the minimum delay we should wait in any case
62
+ const BASE_DELAY_MS = isDev ? 9_000 : autoLogin ? 25_000 : 7_000;
63
+
64
+ if (downlinkAndRtt === undefined) {
65
+ return BASE_DELAY_MS;
66
+ }
67
+
68
+ const { downlink, rtt } = downlinkAndRtt;
69
+
70
+ // Calculate dynamic delay based on RTT and downlink
71
+ // Add 1 to downlink to avoid division by zero
72
+ const dynamicDelay = rtt * 2.5 + BASE_DELAY_MS / (downlink + 1);
73
+
74
+ return Math.max(BASE_DELAY_MS, dynamicDelay);
75
+ })();
76
+
77
+ const { decodeEncryptedAuth, getIsEncryptedAuthResponse, clearSessionStoragePublicKey } =
78
+ await initIframeMessageProtection({
79
+ stateUrlParamValue: stateUrlParamValue_instance
80
+ });
81
+
82
+ let clearTimeouts: (params: { wasSuccess: boolean }) => void;
83
+ {
84
+ let hasLoggedWarningMessage = false;
85
+
86
+ const timeouts = [
87
+ setTimeout(() => {
88
+ dResult.resolve({
89
+ outcome: "failure",
90
+ cause: "timeout"
91
+ });
92
+ }, timeoutDelayMs),
93
+ setTimeout(() => {
94
+ console.warn(
95
+ [
96
+ "oidc-spa: Session restoration is taking longer than expected.",
97
+ "This likely indicates a misconfiguration.",
98
+ `Waiting ${Math.floor(
99
+ timeoutDelayMs / 1_000
100
+ )} seconds before running diagnostics.`,
101
+ "Once the timeout expires, helpful debugging information will be printed to the console."
102
+ ].join(" ")
103
+ );
104
+ hasLoggedWarningMessage = true;
105
+ }, 2_000)
106
+ ];
107
+
108
+ clearTimeouts = ({ wasSuccess }) => {
109
+ timeouts.forEach(clearTimeout);
110
+ if (wasSuccess && hasLoggedWarningMessage) {
111
+ console.log(
112
+ [
113
+ "oidc-spa: Never mind, the auth server was just slow to respond.",
114
+ "You can safely ignore the previous warning."
115
+ ].join(" ")
116
+ );
117
+ }
118
+ };
119
+ }
120
+
121
+ const listener = async (event: MessageEvent) => {
122
+ if (event.origin !== window.location.origin) {
123
+ return;
124
+ }
125
+
126
+ if (
127
+ !getIsEncryptedAuthResponse({
128
+ message: event.data
129
+ })
130
+ ) {
131
+ return;
132
+ }
133
+
134
+ const { authResponse } = await decodeEncryptedAuth({ encryptedAuthResponse: event.data });
135
+
136
+ const stateData = getStateData({ stateUrlParamValue: authResponse.state });
137
+
138
+ assert(stateData !== undefined, "765645");
139
+ assert(stateData.context === "iframe", "250711");
140
+
141
+ if (stateData.configId !== configId) {
142
+ return;
143
+ }
144
+
145
+ clearTimeouts({ wasSuccess: true });
146
+
147
+ window.removeEventListener("message", listener);
148
+
149
+ dResult.resolve({
150
+ outcome: "got auth response from iframe",
151
+ authResponse
152
+ });
153
+ };
154
+
155
+ window.addEventListener("message", listener, false);
156
+
157
+ const transformUrl_oidcClientTs = (url: string) => {
158
+ add_extra_query_params: {
159
+ if (getExtraQueryParams === undefined) {
160
+ break add_extra_query_params;
161
+ }
162
+
163
+ const extraQueryParams = getExtraQueryParams({ isSilent: true, url });
164
+
165
+ for (const [name, value] of Object.entries(extraQueryParams)) {
166
+ if (value === undefined) {
167
+ continue;
168
+ }
169
+ url = addOrUpdateSearchParam({ url, name, value, encodeMethod: "www-form" });
170
+ }
171
+ }
172
+
173
+ apply_transform_url: {
174
+ if (transformUrlBeforeRedirect === undefined) {
175
+ break apply_transform_url;
176
+ }
177
+ url = transformUrlBeforeRedirect({ authorizationUrl: url, isSilent: true });
178
+ }
179
+
180
+ return url;
181
+ };
182
+
183
+ oidcClientTsUserManager
184
+ .signinSilent({
185
+ state: id<StateData.IFrame>({
186
+ context: "iframe",
187
+ configId
188
+ }),
189
+ silentRequestTimeoutInSeconds: timeoutDelayMs / 1000,
190
+ extraTokenParams:
191
+ getExtraTokenParams === undefined ? undefined : noUndefined(getExtraTokenParams()),
192
+ transformUrl: transformUrl_oidcClientTs
193
+ })
194
+ .then(
195
+ oidcClientTsUser => {
196
+ assert(oidcClientTsUser !== null, "oidcClientTsUser is not supposed to be null here");
197
+
198
+ clearTimeouts({ wasSuccess: true });
199
+ window.removeEventListener("message", listener);
200
+
201
+ dResult.resolve({
202
+ outcome: "token refreshed using refresh token",
203
+ oidcClientTsUser
204
+ });
205
+ },
206
+ (error: Error) => {
207
+ if (error.message === "Failed to fetch") {
208
+ // NOTE: If we got an error here it means that the fetch to the
209
+ // well-known oidc endpoint failed.
210
+ // This usually means that the server is down or that the issuerUri
211
+ // is not pointing to a valid oidc server.
212
+ // It could be a CORS error on the well-known endpoint but it's unlikely.
213
+
214
+ // NOTE: This error should happen well before we displayed
215
+ // the warning notifying that something is probably misconfigured.
216
+ // wasSuccess shouldn't really be a required parameter but we do it
217
+ // for peace of mind.
218
+ clearTimeouts({ wasSuccess: false });
219
+
220
+ dResult.resolve({
221
+ outcome: "failure",
222
+ cause: "can't reach well-known oidc endpoint"
223
+ });
224
+
225
+ return;
226
+ }
227
+
228
+ // NOTE: Here, except error on our understanding there can't be any other
229
+ // error than timeout so we fail silently and let the timeout expire.
230
+ }
231
+ );
232
+
233
+ dResult.pr.then(result => {
234
+ clearSessionStoragePublicKey();
235
+
236
+ if (result.outcome === "failure") {
237
+ clearStateStore({ stateUrlParamValue: stateUrlParamValue_instance });
238
+ }
239
+ });
240
+
241
+ return dResult.pr;
242
+ }
@@ -0,0 +1,53 @@
1
+ import { assert, is } from "../vendor/frontend/tsafe";
2
+ import { Deferred } from "../tools/Deferred";
3
+
4
+ const globalContext = {
5
+ appInstanceId: Math.random().toString(36).slice(2)
6
+ };
7
+
8
+ type Message = {
9
+ appInstanceId: string;
10
+ configId: string;
11
+ };
12
+
13
+ function getChannelName(params: { sessionIdOrConfigId: string }) {
14
+ const { sessionIdOrConfigId } = params;
15
+ return `oidc-spa:logout-propagation:${sessionIdOrConfigId}`;
16
+ }
17
+
18
+ export function notifyOtherTabsOfLogout(params: { configId: string; sessionId: string | undefined }) {
19
+ const { configId, sessionId } = params;
20
+
21
+ const message: Message = {
22
+ configId,
23
+ appInstanceId: globalContext.appInstanceId
24
+ };
25
+
26
+ new BroadcastChannel(getChannelName({ sessionIdOrConfigId: sessionId ?? configId })).postMessage(
27
+ message
28
+ );
29
+ }
30
+
31
+ export function getPrOtherTabLogout(params: { sessionId: string | undefined; configId: string }) {
32
+ const { sessionId, configId } = params;
33
+
34
+ const dOtherTabLogout = new Deferred<void>();
35
+
36
+ const channel = new BroadcastChannel(getChannelName({ sessionIdOrConfigId: sessionId ?? configId }));
37
+
38
+ channel.onmessage = ({ data: message }) => {
39
+ assert(is<Message>(message));
40
+
41
+ if (message.appInstanceId === globalContext.appInstanceId) {
42
+ return;
43
+ }
44
+
45
+ channel.close();
46
+
47
+ dOtherTabLogout.resolve();
48
+ };
49
+
50
+ const prOtherTabLogout = dOtherTabLogout.pr;
51
+
52
+ return { prOtherTabLogout };
53
+ }
@@ -0,0 +1,229 @@
1
+ import type { User as OidcClientTsUser } from "../vendor/frontend/oidc-client-ts";
2
+ import { assert, id } from "../vendor/frontend/tsafe";
3
+ import { readExpirationTimeInJwt } from "../tools/readExpirationTimeInJwt";
4
+ import { decodeJwt } from "../tools/decodeJwt";
5
+ import type { Oidc } from "./Oidc";
6
+
7
+ export function oidcClientTsUserToTokens<DecodedIdToken extends Record<string, unknown>>(params: {
8
+ oidcClientTsUser: OidcClientTsUser;
9
+ decodedIdTokenSchema?: {
10
+ parse: (decodedIdToken_original: Oidc.Tokens.DecodedIdToken_base) => DecodedIdToken;
11
+ };
12
+ __unsafe_useIdTokenAsAccessToken: boolean;
13
+ decodedIdToken_previous: DecodedIdToken | undefined;
14
+ log: typeof console.log | undefined;
15
+ }): Oidc.Tokens<DecodedIdToken> {
16
+ const {
17
+ oidcClientTsUser,
18
+ decodedIdTokenSchema,
19
+ __unsafe_useIdTokenAsAccessToken,
20
+ decodedIdToken_previous,
21
+ log
22
+ } = params;
23
+
24
+ const isFirstInit = decodedIdToken_previous === undefined;
25
+
26
+ const accessToken = oidcClientTsUser.access_token;
27
+
28
+ const refreshToken = oidcClientTsUser.refresh_token;
29
+
30
+ const idToken = oidcClientTsUser.id_token;
31
+
32
+ assert(idToken !== undefined, "No id token provided by the oidc server");
33
+
34
+ const decodedIdToken_original = decodeJwt<Oidc.Tokens.DecodedIdToken_base>(idToken);
35
+
36
+ if (isFirstInit) {
37
+ log?.(
38
+ [
39
+ `Decoded ID token`,
40
+ decodedIdTokenSchema === undefined ? "" : " before `decodedIdTokenSchema.parse()`\n",
41
+ JSON.stringify(decodedIdToken_original, null, 2)
42
+ ].join("")
43
+ );
44
+ }
45
+
46
+ const decodedIdToken = (() => {
47
+ let decodedIdToken: DecodedIdToken;
48
+
49
+ if (decodedIdTokenSchema !== undefined) {
50
+ decodedIdToken = decodedIdTokenSchema.parse(decodedIdToken_original);
51
+
52
+ if (isFirstInit) {
53
+ log?.(
54
+ [
55
+ "Decoded ID token after `decodedIdTokenSchema.parse()`\n",
56
+ JSON.stringify(decodedIdToken, null, 2)
57
+ ].join("")
58
+ );
59
+ }
60
+ } else {
61
+ // @ts-expect-error
62
+ decodedIdToken = decodedIdToken_original;
63
+ }
64
+
65
+ if (
66
+ decodedIdToken_previous !== undefined &&
67
+ JSON.stringify(decodedIdToken) === JSON.stringify(decodedIdToken_previous)
68
+ ) {
69
+ // NOTE: For stable ref, prevent re-render for component that would memoize
70
+ return decodedIdToken_previous;
71
+ }
72
+
73
+ return decodedIdToken;
74
+ })();
75
+
76
+ const issuedAtTime = (() => {
77
+ // NOTE: The id_token is always a JWT as per the protocol.
78
+ // We don't use Date.now() due to network latency.
79
+ const id_token_iat = (() => {
80
+ let iat: number | undefined;
81
+
82
+ try {
83
+ const iat_claimValue = decodedIdToken_original.iat;
84
+ assert(iat_claimValue === undefined || typeof iat_claimValue === "number");
85
+ iat = iat_claimValue;
86
+ } catch {
87
+ iat = undefined;
88
+ }
89
+
90
+ if (iat === undefined) {
91
+ return undefined;
92
+ }
93
+
94
+ return iat;
95
+ })();
96
+
97
+ if (id_token_iat === undefined) {
98
+ return Date.now();
99
+ }
100
+
101
+ return id_token_iat * 1000;
102
+ })();
103
+
104
+ const tokens_common: Oidc.Tokens.Common<DecodedIdToken> = {
105
+ ...(__unsafe_useIdTokenAsAccessToken
106
+ ? {
107
+ accessToken: idToken,
108
+ accessTokenExpirationTime: (() => {
109
+ const expirationTime = readExpirationTimeInJwt(idToken);
110
+
111
+ assert(
112
+ expirationTime !== undefined,
113
+ "Failed to get id token expiration time while trying to substitute the access token by the id token"
114
+ );
115
+
116
+ return expirationTime;
117
+ })()
118
+ }
119
+ : {
120
+ accessToken,
121
+ accessTokenExpirationTime: (() => {
122
+ read_from_jwt: {
123
+ const expirationTime = readExpirationTimeInJwt(accessToken);
124
+
125
+ if (expirationTime === undefined) {
126
+ break read_from_jwt;
127
+ }
128
+
129
+ return expirationTime;
130
+ }
131
+
132
+ read_from_token_response_expires_at: {
133
+ const { expires_at } = oidcClientTsUser.__oidc_spa_tokenResponse;
134
+
135
+ if (expires_at === undefined) {
136
+ break read_from_token_response_expires_at;
137
+ }
138
+
139
+ assert(typeof expires_at === "number", "2033392");
140
+
141
+ return expires_at * 1000;
142
+ }
143
+
144
+ read_from_token_response_expires_in: {
145
+ const { expires_in } = oidcClientTsUser.__oidc_spa_tokenResponse;
146
+
147
+ if (expires_in === undefined) {
148
+ break read_from_token_response_expires_in;
149
+ }
150
+
151
+ assert(typeof expires_in === "number", "203333425");
152
+
153
+ return issuedAtTime + expires_in * 1_000;
154
+ }
155
+
156
+ assert(false, "Failed to get access token expiration time");
157
+ })()
158
+ }),
159
+ idToken,
160
+ decodedIdToken,
161
+ decodedIdToken_original,
162
+ issuedAtTime
163
+ };
164
+
165
+ const tokens: Oidc.Tokens<DecodedIdToken> =
166
+ refreshToken === undefined
167
+ ? id<Oidc.Tokens.WithoutRefreshToken<DecodedIdToken>>({
168
+ ...tokens_common,
169
+ hasRefreshToken: false
170
+ })
171
+ : id<Oidc.Tokens.WithRefreshToken<DecodedIdToken>>({
172
+ ...tokens_common,
173
+ hasRefreshToken: true,
174
+ refreshToken,
175
+ refreshTokenExpirationTime: (() => {
176
+ read_from_token_response_expires_at: {
177
+ const { refresh_expires_at } = oidcClientTsUser.__oidc_spa_tokenResponse;
178
+
179
+ if (refresh_expires_at === undefined) {
180
+ break read_from_token_response_expires_at;
181
+ }
182
+
183
+ assert(typeof refresh_expires_at === "number", "2033392");
184
+
185
+ return refresh_expires_at * 1000;
186
+ }
187
+
188
+ read_from_token_response_expires_in: {
189
+ const { refresh_expires_in } = oidcClientTsUser.__oidc_spa_tokenResponse;
190
+
191
+ if (refresh_expires_in === undefined) {
192
+ break read_from_token_response_expires_in;
193
+ }
194
+
195
+ assert(typeof refresh_expires_in === "number", "2033425330");
196
+
197
+ return issuedAtTime + refresh_expires_in * 1000;
198
+ }
199
+
200
+ read_from_jwt: {
201
+ const expirationTime = readExpirationTimeInJwt(refreshToken);
202
+
203
+ if (expirationTime === undefined) {
204
+ break read_from_jwt;
205
+ }
206
+
207
+ return expirationTime;
208
+ }
209
+
210
+ return undefined;
211
+ })()
212
+ });
213
+
214
+ if (
215
+ isFirstInit &&
216
+ tokens.hasRefreshToken &&
217
+ tokens.refreshTokenExpirationTime !== undefined &&
218
+ tokens.refreshTokenExpirationTime < tokens.accessTokenExpirationTime
219
+ ) {
220
+ console.warn(
221
+ [
222
+ "The OIDC refresh token expirationTime is shorter than the one of the access token.",
223
+ "This is very unusual and probably a misconfiguration."
224
+ ].join(" ")
225
+ );
226
+ }
227
+
228
+ return tokens;
229
+ }
@@ -0,0 +1,47 @@
1
+ import { Deferred } from "../tools/Deferred";
2
+ import { assert, id } from "../vendor/frontend/tsafe";
3
+
4
+ const globalContext = {
5
+ prDone_arr: id<Promise<void>[]>([]),
6
+ prUnlock: id<Promise<void>>(Promise.resolve())
7
+ };
8
+
9
+ export async function startLoginOrRefreshProcess(): Promise<{
10
+ completeLoginOrRefreshProcess: () => void;
11
+ }> {
12
+ await globalContext.prUnlock;
13
+
14
+ const dDone = new Deferred<void>();
15
+
16
+ const { prDone_arr } = globalContext;
17
+
18
+ prDone_arr.push(dDone.pr);
19
+
20
+ function completeLoginOrRefreshProcess() {
21
+ const index = prDone_arr.indexOf(dDone.pr);
22
+
23
+ assert(index !== -1, "104044");
24
+
25
+ prDone_arr.splice(index, 1);
26
+
27
+ dDone.resolve();
28
+ }
29
+
30
+ return { completeLoginOrRefreshProcess };
31
+ }
32
+
33
+ export async function waitForAllOtherOngoingLoginOrRefreshProcessesToComplete(params: {
34
+ prUnlock: Promise<void>;
35
+ }): Promise<void> {
36
+ const { prUnlock } = params;
37
+
38
+ const prUnlock_current = globalContext.prUnlock;
39
+
40
+ globalContext.prUnlock = (async () => {
41
+ await prUnlock_current;
42
+
43
+ await prUnlock;
44
+ })();
45
+
46
+ await Promise.all(globalContext.prDone_arr);
47
+ }
@@ -0,0 +1,122 @@
1
+ import { typeGuard, id } from "../vendor/frontend/tsafe";
2
+
3
+ function getKey(params: { configId: string }) {
4
+ const { configId } = params;
5
+
6
+ return `oidc-spa:auth-state:${configId}`;
7
+ }
8
+
9
+ type PersistedAuthState = PersistedAuthState.LoggedIn | PersistedAuthState.ExplicitlyLoggedOut;
10
+ namespace PersistedAuthState {
11
+ type Common = {
12
+ __brand: "PersistedAuthState-v1";
13
+ };
14
+
15
+ export type LoggedIn = Common & {
16
+ stateDescription: "logged in";
17
+ untilTime: number | undefined;
18
+ };
19
+
20
+ export type ExplicitlyLoggedOut = Common & {
21
+ stateDescription: "explicitly logged out";
22
+ };
23
+ }
24
+
25
+ export function persistAuthState(params: {
26
+ configId: string;
27
+ state:
28
+ | {
29
+ stateDescription: "logged in";
30
+ idleSessionLifetimeInSeconds: number | undefined;
31
+ refreshTokenExpirationTime: number | undefined;
32
+ }
33
+ | {
34
+ stateDescription: "explicitly logged out";
35
+ }
36
+ | undefined;
37
+ }) {
38
+ const { configId, state } = params;
39
+
40
+ const key = getKey({ configId });
41
+
42
+ if (state === undefined) {
43
+ localStorage.removeItem(key);
44
+ return;
45
+ }
46
+
47
+ localStorage.setItem(
48
+ key,
49
+ JSON.stringify(
50
+ id<PersistedAuthState>(
51
+ (() => {
52
+ switch (state.stateDescription) {
53
+ case "logged in":
54
+ return id<PersistedAuthState.LoggedIn>({
55
+ __brand: "PersistedAuthState-v1",
56
+ stateDescription: "logged in",
57
+ untilTime: (() => {
58
+ const { idleSessionLifetimeInSeconds, refreshTokenExpirationTime } =
59
+ state;
60
+
61
+ if (idleSessionLifetimeInSeconds !== undefined) {
62
+ return Date.now() + idleSessionLifetimeInSeconds * 1000;
63
+ }
64
+
65
+ return refreshTokenExpirationTime;
66
+ })()
67
+ });
68
+ case "explicitly logged out":
69
+ return id<PersistedAuthState.ExplicitlyLoggedOut>({
70
+ __brand: "PersistedAuthState-v1",
71
+ stateDescription: "explicitly logged out"
72
+ });
73
+ }
74
+ })()
75
+ )
76
+ )
77
+ );
78
+ }
79
+
80
+ export function getPersistedAuthState(params: {
81
+ configId: string;
82
+ }): PersistedAuthState["stateDescription"] | undefined {
83
+ const { configId } = params;
84
+
85
+ const key = getKey({ configId });
86
+
87
+ const value = localStorage.getItem(key);
88
+
89
+ if (value === null) {
90
+ return undefined;
91
+ }
92
+
93
+ let state: unknown;
94
+
95
+ try {
96
+ state = JSON.parse(value);
97
+ } catch {
98
+ localStorage.removeItem(key);
99
+ return undefined;
100
+ }
101
+
102
+ if (
103
+ !typeGuard<PersistedAuthState>(
104
+ state,
105
+ state instanceof Object &&
106
+ "__brand" in state &&
107
+ state.__brand === id<PersistedAuthState["__brand"]>("PersistedAuthState-v1")
108
+ )
109
+ ) {
110
+ localStorage.removeItem(key);
111
+ return undefined;
112
+ }
113
+
114
+ if (state.stateDescription === "logged in") {
115
+ if (state.untilTime !== undefined && state.untilTime <= Date.now()) {
116
+ localStorage.removeItem(key);
117
+ return undefined;
118
+ }
119
+ }
120
+
121
+ return state.stateDescription;
122
+ }