oidc-spa 7.2.1 → 7.2.3
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/backend.js.map +1 -1
- package/core/AuthResponse.js.map +1 -1
- package/core/Oidc.js.map +1 -1
- package/core/OidcInitializationError.js.map +1 -1
- package/core/OidcMetadata.js.map +1 -1
- package/core/StateData.js.map +1 -1
- package/core/configId.js.map +1 -1
- package/core/createOidc.js +1 -1
- package/core/createOidc.js.map +1 -1
- package/core/diagnostic.js.map +1 -1
- package/core/evtIsUserActive.js.map +1 -1
- package/core/handleOidcCallback.js.map +1 -1
- package/core/iframeMessageProtection.js.map +1 -1
- package/core/index.js.map +1 -1
- package/core/initialLocationHref.js.map +1 -1
- package/core/isNewBrowserSession.js.map +1 -1
- package/core/loginOrGoToAuthServer.js.map +1 -1
- package/core/loginPropagationToOtherTabs.js.map +1 -1
- package/core/loginSilent.js.map +1 -1
- package/core/logoutPropagationToOtherTabs.js.map +1 -1
- package/core/oidcClientTsUserToTokens.js.map +1 -1
- package/core/ongoingLoginOrRefreshProcesses.js.map +1 -1
- package/core/persistedAuthState.js.map +1 -1
- package/entrypoint.js.map +1 -1
- package/esm/core/AuthResponse.js +2 -2
- package/esm/core/AuthResponse.js.map +1 -1
- package/esm/core/Oidc.d.ts +1 -1
- package/esm/core/Oidc.js.map +1 -1
- package/esm/core/OidcInitializationError.js.map +1 -1
- package/esm/core/OidcMetadata.js +2 -2
- package/esm/core/OidcMetadata.js.map +1 -1
- package/esm/core/StateData.js +3 -3
- package/esm/core/StateData.js.map +1 -1
- package/esm/core/configId.js.map +1 -1
- package/esm/core/createOidc.d.ts +2 -2
- package/esm/core/createOidc.js +33 -33
- package/esm/core/createOidc.js.map +1 -1
- package/esm/core/diagnostic.d.ts +1 -1
- package/esm/core/diagnostic.js +4 -4
- package/esm/core/diagnostic.js.map +1 -1
- package/esm/core/evtIsUserActive.d.ts +1 -1
- package/esm/core/evtIsUserActive.js +5 -5
- package/esm/core/evtIsUserActive.js.map +1 -1
- package/esm/core/handleOidcCallback.d.ts +2 -2
- package/esm/core/handleOidcCallback.js +5 -5
- package/esm/core/handleOidcCallback.js.map +1 -1
- package/esm/core/iframeMessageProtection.d.ts +1 -1
- package/esm/core/iframeMessageProtection.js +3 -3
- package/esm/core/iframeMessageProtection.js.map +1 -1
- package/esm/core/index.d.ts +4 -4
- package/esm/core/index.js +4 -4
- package/esm/core/index.js.map +1 -1
- package/esm/core/initialLocationHref.js.map +1 -1
- package/esm/core/isNewBrowserSession.d.ts +1 -1
- package/esm/core/isNewBrowserSession.js.map +1 -1
- package/esm/core/loginOrGoToAuthServer.d.ts +2 -2
- package/esm/core/loginOrGoToAuthServer.js +6 -6
- package/esm/core/loginOrGoToAuthServer.js.map +1 -1
- package/esm/core/loginPropagationToOtherTabs.js +3 -3
- package/esm/core/loginPropagationToOtherTabs.js.map +1 -1
- package/esm/core/loginSilent.d.ts +2 -2
- package/esm/core/loginSilent.js +8 -8
- package/esm/core/loginSilent.js.map +1 -1
- package/esm/core/logoutPropagationToOtherTabs.js +3 -3
- package/esm/core/logoutPropagationToOtherTabs.js.map +1 -1
- package/esm/core/oidcClientTsUserToTokens.d.ts +2 -2
- package/esm/core/oidcClientTsUserToTokens.js +4 -4
- package/esm/core/oidcClientTsUserToTokens.js.map +1 -1
- package/esm/core/ongoingLoginOrRefreshProcesses.js +3 -3
- package/esm/core/ongoingLoginOrRefreshProcesses.js.map +1 -1
- package/esm/core/persistedAuthState.js +2 -2
- package/esm/core/persistedAuthState.js.map +1 -1
- package/esm/entrypoint.js +3 -3
- package/esm/entrypoint.js.map +1 -1
- package/esm/index.d.ts +1 -1
- package/esm/index.js +2 -2
- package/esm/index.js.map +1 -1
- package/esm/keycloak/index.d.ts +3 -3
- package/esm/keycloak/index.js +3 -3
- package/esm/keycloak/index.js.map +1 -1
- package/esm/keycloak/isKeycloak.js.map +1 -1
- package/esm/keycloak/keycloak-js/Keycloak.d.ts +1 -1
- package/esm/keycloak/keycloak-js/Keycloak.js +9 -9
- package/esm/keycloak/keycloak-js/Keycloak.js.map +1 -1
- package/esm/keycloak/keycloak-js/index.d.ts +2 -2
- package/esm/keycloak/keycloak-js/index.js +2 -2
- package/esm/keycloak/keycloak-js/index.js.map +1 -1
- package/esm/keycloak/keycloak-js/types.js.map +1 -1
- package/esm/keycloak/keycloakIssuerUriParsed.js +3 -3
- package/esm/keycloak/keycloakIssuerUriParsed.js.map +1 -1
- package/esm/keycloak/keycloakUtils.d.ts +1 -1
- package/esm/keycloak/keycloakUtils.js +3 -3
- package/esm/keycloak/keycloakUtils.js.map +1 -1
- package/esm/keycloak-js.d.ts +1 -1
- package/esm/keycloak-js.js +2 -2
- package/esm/keycloak-js.js.map +1 -1
- package/esm/mock/index.d.ts +1 -1
- package/esm/mock/index.js +2 -2
- package/esm/mock/index.js.map +1 -1
- package/esm/mock/oidc.d.ts +1 -1
- package/esm/mock/oidc.js +6 -6
- package/esm/mock/oidc.js.map +1 -1
- package/esm/mock/react.d.ts +8 -8
- package/esm/mock/react.js +3 -3
- package/esm/mock/react.js.map +1 -1
- package/esm/react/index.d.ts +1 -1
- package/esm/react/index.js +2 -2
- package/esm/react/index.js.map +1 -1
- package/esm/react/react.d.ts +2 -2
- package/esm/react/react.js +6 -6
- package/esm/react/react.js.map +1 -1
- package/esm/tools/Deferred.js.map +1 -1
- package/esm/tools/EphemeralSessionStorage.js +2 -2
- package/esm/tools/EphemeralSessionStorage.js.map +1 -1
- package/esm/tools/Evt.js +3 -3
- package/esm/tools/Evt.js.map +1 -1
- package/esm/tools/StatefulEvt.js.map +1 -1
- package/esm/tools/ValueOrAsyncGetter.js.map +1 -1
- package/esm/tools/asymmetricEncryption.js.map +1 -1
- package/esm/tools/base64.js.map +1 -1
- package/esm/tools/createObjectThatThrowsIfAccessed.js.map +1 -1
- package/esm/tools/decodeJwt.js.map +1 -1
- package/esm/tools/generateUrlSafeRandom.js.map +1 -1
- package/esm/tools/getDownlinkAndRtt.js +2 -2
- package/esm/tools/getDownlinkAndRtt.js.map +1 -1
- package/esm/tools/getIsOnline.js +2 -2
- package/esm/tools/getIsOnline.js.map +1 -1
- package/esm/tools/getIsValidRemoteJson.js.map +1 -1
- package/esm/tools/getPrUserInteraction.js +2 -2
- package/esm/tools/getPrUserInteraction.js.map +1 -1
- package/esm/tools/getUserEnvironmentInfo.js.map +1 -1
- package/esm/tools/haveSharedParentDomain.js.map +1 -1
- package/esm/tools/isDev.js.map +1 -1
- package/esm/tools/parseKeycloakIssuerUri.js +2 -2
- package/esm/tools/parseKeycloakIssuerUri.js.map +1 -1
- package/esm/tools/readExpirationTimeInJwt.js +3 -3
- package/esm/tools/readExpirationTimeInJwt.js.map +1 -1
- package/esm/tools/startCountdown.js +2 -2
- package/esm/tools/startCountdown.js.map +1 -1
- package/esm/tools/subscribeToUserInteraction.js +2 -2
- package/esm/tools/subscribeToUserInteraction.js.map +1 -1
- package/esm/tools/toFullyQualifiedUrl.js.map +1 -1
- package/esm/tools/toHumanReadableDuration.js.map +1 -1
- package/esm/tools/urlSearchParams.js.map +1 -1
- package/esm/tools/workerTimers.js +2 -2
- package/esm/tools/workerTimers.js.map +1 -1
- package/index.js.map +1 -1
- package/keycloak/index.js.map +1 -1
- package/keycloak/isKeycloak.js.map +1 -1
- package/keycloak/keycloak-js/Keycloak.js.map +1 -1
- package/keycloak/keycloak-js/index.js.map +1 -1
- package/keycloak/keycloak-js/types.js.map +1 -1
- package/keycloak/keycloakIssuerUriParsed.js.map +1 -1
- package/keycloak/keycloakUtils.js.map +1 -1
- package/keycloak-js.js.map +1 -1
- package/mock/index.js.map +1 -1
- package/mock/oidc.js.map +1 -1
- package/mock/react.js.map +1 -1
- package/package.json +11 -33
- package/react/index.js.map +1 -1
- package/react/react.js.map +1 -1
- package/src/backend.ts +391 -0
- package/src/core/AuthResponse.ts +26 -0
- package/src/core/Oidc.ts +140 -0
- package/src/core/OidcInitializationError.ts +19 -0
- package/src/core/OidcMetadata.ts +271 -0
- package/src/core/StateData.ts +118 -0
- package/src/core/configId.ts +3 -0
- package/src/core/createOidc.ts +1576 -0
- package/src/core/diagnostic.ts +267 -0
- package/src/core/evtIsUserActive.ts +108 -0
- package/src/core/handleOidcCallback.ts +321 -0
- package/src/core/iframeMessageProtection.ts +100 -0
- package/src/core/index.ts +4 -0
- package/src/core/initialLocationHref.ts +5 -0
- package/src/core/isNewBrowserSession.ts +37 -0
- package/src/core/loginOrGoToAuthServer.ts +324 -0
- package/src/core/loginPropagationToOtherTabs.ts +51 -0
- package/src/core/loginSilent.ts +242 -0
- package/src/core/logoutPropagationToOtherTabs.ts +53 -0
- package/src/core/oidcClientTsUserToTokens.ts +229 -0
- package/src/core/ongoingLoginOrRefreshProcesses.ts +47 -0
- package/src/core/persistedAuthState.ts +122 -0
- package/src/entrypoint.ts +69 -0
- package/src/index.ts +1 -0
- package/src/keycloak/index.ts +8 -0
- package/src/keycloak/isKeycloak.ts +23 -0
- package/src/keycloak/keycloak-js/Keycloak.ts +1097 -0
- package/src/keycloak/keycloak-js/index.ts +2 -0
- package/src/keycloak/keycloak-js/types.ts +442 -0
- package/src/keycloak/keycloakIssuerUriParsed.ts +29 -0
- package/src/keycloak/keycloakUtils.ts +90 -0
- package/src/keycloak-js.ts +1 -0
- package/src/mock/index.ts +1 -0
- package/src/mock/oidc.ts +211 -0
- package/src/mock/react.tsx +11 -0
- package/src/react/index.ts +1 -0
- package/src/react/react.tsx +476 -0
- package/src/tools/Deferred.ts +33 -0
- package/src/tools/EphemeralSessionStorage.ts +223 -0
- package/src/tools/Evt.ts +56 -0
- package/src/tools/StatefulEvt.ts +38 -0
- package/src/tools/ValueOrAsyncGetter.ts +1 -0
- package/src/tools/asymmetricEncryption.ts +184 -0
- package/src/tools/base64.ts +7 -0
- package/src/tools/createObjectThatThrowsIfAccessed.ts +40 -0
- package/src/tools/decodeJwt.ts +95 -0
- package/src/tools/generateUrlSafeRandom.ts +26 -0
- package/src/tools/getDownlinkAndRtt.ts +22 -0
- package/src/tools/getIsOnline.ts +20 -0
- package/src/tools/getIsValidRemoteJson.ts +18 -0
- package/src/tools/getPrUserInteraction.ts +27 -0
- package/src/tools/getUserEnvironmentInfo.ts +42 -0
- package/src/tools/haveSharedParentDomain.ts +13 -0
- package/src/tools/isDev.ts +30 -0
- package/src/tools/parseKeycloakIssuerUri.ts +49 -0
- package/src/tools/readExpirationTimeInJwt.ts +16 -0
- package/src/tools/startCountdown.ts +36 -0
- package/src/tools/subscribeToUserInteraction.ts +33 -0
- package/src/tools/toFullyQualifiedUrl.ts +58 -0
- package/src/tools/toHumanReadableDuration.ts +21 -0
- package/src/tools/urlSearchParams.ts +130 -0
- package/src/tools/workerTimers.ts +57 -0
- package/src/vendor/backend/evt.ts +2 -0
- package/src/vendor/backend/jsonwebtoken.ts +1 -0
- package/src/vendor/backend/node-fetch.ts +2 -0
- package/src/vendor/backend/node-jose.ts +1 -0
- package/src/vendor/backend/tsafe.ts +5 -0
- package/src/vendor/backend/zod.ts +1 -0
- package/src/vendor/frontend/oidc-client-ts.ts +1 -0
- package/src/vendor/frontend/tsafe.ts +6 -0
- package/src/vendor/frontend/worker-timers.ts +2 -0
- package/tools/Deferred.js.map +1 -1
- package/tools/EphemeralSessionStorage.js.map +1 -1
- package/tools/Evt.js.map +1 -1
- package/tools/StatefulEvt.js.map +1 -1
- package/tools/ValueOrAsyncGetter.js.map +1 -1
- package/tools/asymmetricEncryption.js.map +1 -1
- package/tools/base64.js.map +1 -1
- package/tools/createObjectThatThrowsIfAccessed.js.map +1 -1
- package/tools/decodeJwt.js.map +1 -1
- package/tools/generateUrlSafeRandom.js.map +1 -1
- package/tools/getDownlinkAndRtt.js.map +1 -1
- package/tools/getIsOnline.js.map +1 -1
- package/tools/getIsValidRemoteJson.js.map +1 -1
- package/tools/getPrUserInteraction.js.map +1 -1
- package/tools/getUserEnvironmentInfo.js.map +1 -1
- package/tools/haveSharedParentDomain.js.map +1 -1
- package/tools/isDev.js.map +1 -1
- package/tools/parseKeycloakIssuerUri.js.map +1 -1
- package/tools/readExpirationTimeInJwt.js.map +1 -1
- package/tools/startCountdown.js.map +1 -1
- package/tools/subscribeToUserInteraction.js.map +1 -1
- package/tools/toFullyQualifiedUrl.js.map +1 -1
- package/tools/toHumanReadableDuration.js.map +1 -1
- package/tools/urlSearchParams.js.map +1 -1
- 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
|
+
}
|