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.
- 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.map +1 -1
- package/esm/core/Oidc.js.map +1 -1
- package/esm/core/OidcInitializationError.js.map +1 -1
- package/esm/core/OidcMetadata.js.map +1 -1
- package/esm/core/StateData.js.map +1 -1
- package/esm/core/configId.js.map +1 -1
- package/esm/core/createOidc.js +1 -1
- package/esm/core/createOidc.js.map +1 -1
- package/esm/core/diagnostic.js.map +1 -1
- package/esm/core/evtIsUserActive.js.map +1 -1
- package/esm/core/handleOidcCallback.js.map +1 -1
- package/esm/core/iframeMessageProtection.js.map +1 -1
- package/esm/core/index.js.map +1 -1
- package/esm/core/initialLocationHref.js.map +1 -1
- package/esm/core/isNewBrowserSession.js.map +1 -1
- package/esm/core/loginOrGoToAuthServer.js.map +1 -1
- package/esm/core/loginPropagationToOtherTabs.js.map +1 -1
- package/esm/core/loginSilent.js.map +1 -1
- package/esm/core/logoutPropagationToOtherTabs.js.map +1 -1
- package/esm/core/oidcClientTsUserToTokens.js.map +1 -1
- package/esm/core/ongoingLoginOrRefreshProcesses.js.map +1 -1
- package/esm/core/persistedAuthState.js.map +1 -1
- package/esm/entrypoint.js.map +1 -1
- package/esm/index.js.map +1 -1
- package/esm/keycloak/index.js.map +1 -1
- package/esm/keycloak/isKeycloak.js.map +1 -1
- package/esm/keycloak/keycloak-js/Keycloak.js.map +1 -1
- 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.map +1 -1
- package/esm/keycloak/keycloakUtils.js.map +1 -1
- package/esm/keycloak-js.js.map +1 -1
- package/esm/mock/index.js.map +1 -1
- package/esm/mock/oidc.js.map +1 -1
- package/esm/mock/react.js.map +1 -1
- package/esm/react/index.js.map +1 -1
- package/esm/react/react.js.map +1 -1
- package/esm/tools/Deferred.js.map +1 -1
- package/esm/tools/EphemeralSessionStorage.js.map +1 -1
- 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.map +1 -1
- package/esm/tools/getIsOnline.js.map +1 -1
- package/esm/tools/getIsValidRemoteJson.js.map +1 -1
- 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.map +1 -1
- package/esm/tools/readExpirationTimeInJwt.js.map +1 -1
- package/esm/tools/startCountdown.js.map +1 -1
- 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.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 +1 -1
- 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,1576 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UserManager as OidcClientTsUserManager,
|
|
3
|
+
WebStorageStateStore,
|
|
4
|
+
type User as OidcClientTsUser,
|
|
5
|
+
InMemoryWebStorage
|
|
6
|
+
} from "../vendor/frontend/oidc-client-ts";
|
|
7
|
+
import type { OidcMetadata } from "./OidcMetadata";
|
|
8
|
+
import { id, assert, is, type Equals } from "../vendor/frontend/tsafe";
|
|
9
|
+
import { setTimeout, clearTimeout } from "../tools/workerTimers";
|
|
10
|
+
import { Deferred } from "../tools/Deferred";
|
|
11
|
+
import { createEvtIsUserActive } from "./evtIsUserActive";
|
|
12
|
+
import { createStartCountdown } from "../tools/startCountdown";
|
|
13
|
+
import { toHumanReadableDuration } from "../tools/toHumanReadableDuration";
|
|
14
|
+
import { toFullyQualifiedUrl } from "../tools/toFullyQualifiedUrl";
|
|
15
|
+
import { OidcInitializationError } from "./OidcInitializationError";
|
|
16
|
+
import { type StateData, generateStateUrlParamValue, STATE_STORE_KEY_PREFIX } from "./StateData";
|
|
17
|
+
import { notifyOtherTabsOfLogout, getPrOtherTabLogout } from "./logoutPropagationToOtherTabs";
|
|
18
|
+
import { notifyOtherTabsOfLogin, getPrOtherTabLogin } from "./loginPropagationToOtherTabs";
|
|
19
|
+
import { getConfigId } from "./configId";
|
|
20
|
+
import { oidcClientTsUserToTokens } from "./oidcClientTsUserToTokens";
|
|
21
|
+
import { loginSilent } from "./loginSilent";
|
|
22
|
+
import { authResponseToUrl } from "./AuthResponse";
|
|
23
|
+
import { handleOidcCallback, retrieveRedirectAuthResponseAndStateData } from "./handleOidcCallback";
|
|
24
|
+
import { getPersistedAuthState, persistAuthState } from "./persistedAuthState";
|
|
25
|
+
import type { Oidc } from "./Oidc";
|
|
26
|
+
import { createEvt } from "../tools/Evt";
|
|
27
|
+
import { getHaveSharedParentDomain } from "../tools/haveSharedParentDomain";
|
|
28
|
+
import {
|
|
29
|
+
createLoginOrGoToAuthServer,
|
|
30
|
+
getPrSafelyRestoredFromBfCacheAfterLoginBackNavigation
|
|
31
|
+
} from "./loginOrGoToAuthServer";
|
|
32
|
+
import { createEphemeralSessionStorage } from "../tools/EphemeralSessionStorage";
|
|
33
|
+
import {
|
|
34
|
+
startLoginOrRefreshProcess,
|
|
35
|
+
waitForAllOtherOngoingLoginOrRefreshProcessesToComplete
|
|
36
|
+
} from "./ongoingLoginOrRefreshProcesses";
|
|
37
|
+
import { initialLocationHref } from "./initialLocationHref";
|
|
38
|
+
import { createGetIsNewBrowserSession } from "./isNewBrowserSession";
|
|
39
|
+
import { getIsOnline } from "../tools/getIsOnline";
|
|
40
|
+
import { isKeycloak } from "../keycloak/isKeycloak";
|
|
41
|
+
|
|
42
|
+
// NOTE: Replaced at build time
|
|
43
|
+
const VERSION = "{{OIDC_SPA_VERSION}}";
|
|
44
|
+
|
|
45
|
+
export type ParamsOfCreateOidc<
|
|
46
|
+
DecodedIdToken extends Record<string, unknown> = Record<string, unknown>,
|
|
47
|
+
AutoLogin extends boolean = false
|
|
48
|
+
> = {
|
|
49
|
+
issuerUri: string;
|
|
50
|
+
clientId: string;
|
|
51
|
+
/**
|
|
52
|
+
* The scopes being requested from the OIDC/OAuth2 provider (default: `["profile"]`
|
|
53
|
+
* (the scope "openid" is added automatically as it's mandatory)
|
|
54
|
+
**/
|
|
55
|
+
scopes?: string[];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Transform the url (authorization endpoint) before redirecting to the login pages.
|
|
59
|
+
*
|
|
60
|
+
* The isSilent parameter is true when the redirect is initiated in the background iframe for silent signin.
|
|
61
|
+
* This can be used to omit ui related query parameters (like `ui_locales`).
|
|
62
|
+
*/
|
|
63
|
+
transformUrlBeforeRedirect?: (params: { authorizationUrl: string; isSilent: boolean }) => string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extra query params to be added to the authorization endpoint url before redirecting or silent signing in.
|
|
67
|
+
* You can provide a function that returns those extra query params, it will be called
|
|
68
|
+
* when login() is called.
|
|
69
|
+
*
|
|
70
|
+
* Example: extraQueryParams: ()=> ({ ui_locales: "fr" })
|
|
71
|
+
*
|
|
72
|
+
* This parameter can also be passed to login() directly.
|
|
73
|
+
*/
|
|
74
|
+
extraQueryParams?:
|
|
75
|
+
| Record<string, string | undefined>
|
|
76
|
+
| ((params: { isSilent: boolean; url: string }) => Record<string, string | undefined>);
|
|
77
|
+
/**
|
|
78
|
+
* Extra body params to be added to the /token POST request.
|
|
79
|
+
*
|
|
80
|
+
* It will be used when for the initial request, whenever the token is getting refreshed and if you call `renewTokens()`.
|
|
81
|
+
* You can also provide this parameter directly to the `renewTokens()` method.
|
|
82
|
+
*
|
|
83
|
+
* It can be either a string to string record or a function that returns a string to string record.
|
|
84
|
+
*
|
|
85
|
+
* Example: extraTokenParams: ()=> ({ selectedCustomer: "xxx" })
|
|
86
|
+
* extraTokenParams: { selectedCustomer: "xxx" }
|
|
87
|
+
*/
|
|
88
|
+
extraTokenParams?: Record<string, string | undefined> | (() => Record<string, string | undefined>);
|
|
89
|
+
/**
|
|
90
|
+
* Usage discouraged, it's here because we don't want to assume too much on your
|
|
91
|
+
* usecase but I can't think of a scenario where you would want anything
|
|
92
|
+
* other than the current page.
|
|
93
|
+
*
|
|
94
|
+
* Where to redirect after successful login.
|
|
95
|
+
* Default: window.location.href (here)
|
|
96
|
+
*
|
|
97
|
+
* It does not need to include the origin, eg: "/dashboard"
|
|
98
|
+
*
|
|
99
|
+
* This parameter can also be passed to login() directly as `redirectUrl`.
|
|
100
|
+
*/
|
|
101
|
+
postLoginRedirectUrl?: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* What should you put in this parameter?
|
|
105
|
+
* - Vite project: `BASE_URL: import.meta.env.BASE_URL`
|
|
106
|
+
* - Create React App project: `BASE_URL: process.env.PUBLIC_URL`
|
|
107
|
+
* - Other: `BASE_URL: "/"` (Usually, or `/dashboard` if your app is not at the root of the domain)
|
|
108
|
+
*/
|
|
109
|
+
homeUrl: string;
|
|
110
|
+
|
|
111
|
+
decodedIdTokenSchema?: {
|
|
112
|
+
parse: (decodedIdToken_original: Oidc.Tokens.DecodedIdToken_base) => DecodedIdToken;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* This parameter defines after how many seconds of inactivity the user should be
|
|
117
|
+
* logged out automatically.
|
|
118
|
+
*
|
|
119
|
+
* WARNING: It should be configured on the identity server side
|
|
120
|
+
* as it's the authoritative source for security policies and not the client.
|
|
121
|
+
* If you don't provide this parameter it will be inferred from the refresh token expiration time.
|
|
122
|
+
* */
|
|
123
|
+
idleSessionLifetimeInSeconds?: number;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Usage discouraged, this parameter exists because we don't want to assume
|
|
127
|
+
* too much about your usecase but I can't think of a scenario where you would
|
|
128
|
+
* want anything other than the current page.
|
|
129
|
+
*
|
|
130
|
+
* Default: { redirectTo: "current page" }
|
|
131
|
+
*/
|
|
132
|
+
autoLogoutParams?: Parameters<Oidc.LoggedIn<any>["logout"]>[0];
|
|
133
|
+
autoLogin?: AutoLogin;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Default: false
|
|
137
|
+
*
|
|
138
|
+
* See: https://docs.oidc-spa.dev/v/v7/resources/iframe-related-issues
|
|
139
|
+
*/
|
|
140
|
+
noIframe?: boolean;
|
|
141
|
+
|
|
142
|
+
debugLogs?: boolean;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* WARNING: This option exists solely as a workaround
|
|
146
|
+
* for limitations in the Google OAuth API.
|
|
147
|
+
* See: https://docs.oidc-spa.dev/providers-configuration/google-oauth
|
|
148
|
+
*
|
|
149
|
+
* Do not use this for other providers.
|
|
150
|
+
* If you think you need a client secret in a SPA, you are likely
|
|
151
|
+
* trying to use a confidential (private) client in the browser,
|
|
152
|
+
* which is insecure and not supported.
|
|
153
|
+
*/
|
|
154
|
+
__unsafe_clientSecret?: string;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* WARNING: Setting this to true is a workaround for provider
|
|
158
|
+
* like Google OAuth that don't support JWT access token.
|
|
159
|
+
* Use at your own risk, this is a hack.
|
|
160
|
+
*/
|
|
161
|
+
__unsafe_useIdTokenAsAccessToken?: boolean;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* This option should only be used as a last resort.
|
|
165
|
+
*
|
|
166
|
+
* If your OIDC provider is correctly configured, this should not be necessary.
|
|
167
|
+
*
|
|
168
|
+
* The metadata is normally retrieved automatically from:
|
|
169
|
+
* `${issuerUri}/.well-known/openid-configuration`
|
|
170
|
+
*
|
|
171
|
+
* Use this only if that endpoint is not accessible (e.g. due to missing CORS headers
|
|
172
|
+
* or non-standard deployments), and you cannot fix the server-side configuration.
|
|
173
|
+
*/
|
|
174
|
+
__metadata?: Partial<OidcMetadata>;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const globalContext = {
|
|
178
|
+
prOidcByConfigId: new Map<string, Promise<Oidc<any>>>(),
|
|
179
|
+
hasLogoutBeenCalled: id<boolean>(false),
|
|
180
|
+
evtRequestToPersistTokens: createEvt<{ configIdOfInstancePostingTheRequest: string }>()
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** @see: https://docs.oidc-spa.dev/v/v7/usage */
|
|
184
|
+
export async function createOidc<
|
|
185
|
+
DecodedIdToken extends Record<string, unknown> = Record<string, unknown>,
|
|
186
|
+
AutoLogin extends boolean = false
|
|
187
|
+
>(
|
|
188
|
+
params: ParamsOfCreateOidc<DecodedIdToken, AutoLogin>
|
|
189
|
+
): Promise<AutoLogin extends true ? Oidc.LoggedIn<DecodedIdToken> : Oidc<DecodedIdToken>> {
|
|
190
|
+
for (const name of ["issuerUri", "clientId"] as const) {
|
|
191
|
+
const value = params[name];
|
|
192
|
+
if (!value) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`The parameter "${name}" is required, you provided: ${value}. (Forgot a .env variable?)`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { issuerUri: issuerUri_params, clientId, scopes = ["profile"], debugLogs, ...rest } = params;
|
|
200
|
+
|
|
201
|
+
const issuerUri = toFullyQualifiedUrl({
|
|
202
|
+
urlish: issuerUri_params,
|
|
203
|
+
doAssertNoQueryParams: true,
|
|
204
|
+
doOutputWithTrailingSlash: false
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const log = (() => {
|
|
208
|
+
if (!debugLogs) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return id<typeof console.log>((...[first, ...rest]) => {
|
|
213
|
+
const label = "oidc-spa";
|
|
214
|
+
|
|
215
|
+
if (typeof first === "string") {
|
|
216
|
+
console.log(...[`${label}: ${first}`, ...rest]);
|
|
217
|
+
} else {
|
|
218
|
+
console.log(...[`${label}:`, first, ...rest]);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
})();
|
|
222
|
+
|
|
223
|
+
const configId = getConfigId({ issuerUri, clientId });
|
|
224
|
+
|
|
225
|
+
const { prOidcByConfigId } = globalContext;
|
|
226
|
+
|
|
227
|
+
use_previous_instance: {
|
|
228
|
+
const prOidc = prOidcByConfigId.get(configId);
|
|
229
|
+
|
|
230
|
+
if (prOidc === undefined) {
|
|
231
|
+
break use_previous_instance;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
log?.(
|
|
235
|
+
[
|
|
236
|
+
`createOidc was called again with the same config (${JSON.stringify({
|
|
237
|
+
issuerUri,
|
|
238
|
+
clientId
|
|
239
|
+
})})`,
|
|
240
|
+
`Returning the previous instance. All potential different parameters are ignored.`
|
|
241
|
+
].join(" ")
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// @ts-expect-error: We know what we're doing
|
|
245
|
+
return prOidc;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const dOidc = new Deferred<Oidc<any>>();
|
|
249
|
+
|
|
250
|
+
prOidcByConfigId.set(configId, dOidc.pr);
|
|
251
|
+
|
|
252
|
+
const oidc = await createOidc_nonMemoized(rest, {
|
|
253
|
+
issuerUri,
|
|
254
|
+
clientId,
|
|
255
|
+
scopes,
|
|
256
|
+
configId,
|
|
257
|
+
log
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
dOidc.resolve(oidc);
|
|
261
|
+
|
|
262
|
+
return oidc;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function createOidc_nonMemoized<
|
|
266
|
+
DecodedIdToken extends Record<string, unknown> = Record<string, unknown>,
|
|
267
|
+
AutoLogin extends boolean = false
|
|
268
|
+
>(
|
|
269
|
+
params: Omit<
|
|
270
|
+
ParamsOfCreateOidc<DecodedIdToken, AutoLogin>,
|
|
271
|
+
"issuerUri" | "clientId" | "scopes" | "debugLogs"
|
|
272
|
+
>,
|
|
273
|
+
preProcessedParams: {
|
|
274
|
+
issuerUri: string;
|
|
275
|
+
clientId: string;
|
|
276
|
+
scopes: string[];
|
|
277
|
+
configId: string;
|
|
278
|
+
log: typeof console.log | undefined;
|
|
279
|
+
}
|
|
280
|
+
): Promise<AutoLogin extends true ? Oidc.LoggedIn<DecodedIdToken> : Oidc<DecodedIdToken>> {
|
|
281
|
+
const {
|
|
282
|
+
transformUrlBeforeRedirect,
|
|
283
|
+
extraQueryParams: extraQueryParamsOrGetter,
|
|
284
|
+
extraTokenParams: extraTokenParamsOrGetter,
|
|
285
|
+
homeUrl: homeUrl_params,
|
|
286
|
+
decodedIdTokenSchema,
|
|
287
|
+
idleSessionLifetimeInSeconds,
|
|
288
|
+
autoLogoutParams = { redirectTo: "current page" },
|
|
289
|
+
autoLogin = false,
|
|
290
|
+
postLoginRedirectUrl: postLoginRedirectUrl_default,
|
|
291
|
+
__unsafe_clientSecret,
|
|
292
|
+
__unsafe_useIdTokenAsAccessToken = false,
|
|
293
|
+
__metadata,
|
|
294
|
+
noIframe = false
|
|
295
|
+
} = params;
|
|
296
|
+
|
|
297
|
+
const { issuerUri, clientId, scopes, configId, log } = preProcessedParams;
|
|
298
|
+
|
|
299
|
+
const getExtraQueryParams = (() => {
|
|
300
|
+
if (extraQueryParamsOrGetter === undefined) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (typeof extraQueryParamsOrGetter !== "function") {
|
|
305
|
+
return () => extraQueryParamsOrGetter;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return extraQueryParamsOrGetter;
|
|
309
|
+
})();
|
|
310
|
+
|
|
311
|
+
const getExtraTokenParams = (() => {
|
|
312
|
+
if (extraTokenParamsOrGetter === undefined) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (typeof extraTokenParamsOrGetter !== "function") {
|
|
317
|
+
return () => extraTokenParamsOrGetter;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return extraTokenParamsOrGetter;
|
|
321
|
+
})();
|
|
322
|
+
|
|
323
|
+
const homeUrlAndRedirectUri = toFullyQualifiedUrl({
|
|
324
|
+
urlish: homeUrl_params,
|
|
325
|
+
doAssertNoQueryParams: true,
|
|
326
|
+
doOutputWithTrailingSlash: true
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
log?.(
|
|
330
|
+
`Calling createOidc v${VERSION} ${JSON.stringify(
|
|
331
|
+
{
|
|
332
|
+
issuerUri,
|
|
333
|
+
clientId,
|
|
334
|
+
scopes,
|
|
335
|
+
configId,
|
|
336
|
+
homeUrlAndRedirectUri
|
|
337
|
+
},
|
|
338
|
+
null,
|
|
339
|
+
2
|
|
340
|
+
)}`
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
{
|
|
344
|
+
const { isHandled } = handleOidcCallback();
|
|
345
|
+
|
|
346
|
+
if (isHandled) {
|
|
347
|
+
await new Promise<never>(() => {});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const stateUrlParamValue_instance = generateStateUrlParamValue();
|
|
352
|
+
|
|
353
|
+
const canUseIframe = (() => {
|
|
354
|
+
if (noIframe) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
third_party_cookies: {
|
|
359
|
+
const isOidcServerThirdPartyRelativeToApp =
|
|
360
|
+
getHaveSharedParentDomain({
|
|
361
|
+
url1: window.location.origin,
|
|
362
|
+
url2: issuerUri
|
|
363
|
+
}) === false;
|
|
364
|
+
|
|
365
|
+
if (!isOidcServerThirdPartyRelativeToApp) {
|
|
366
|
+
break third_party_cookies;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const isGoogleChrome = (() => {
|
|
370
|
+
const ua = navigator.userAgent;
|
|
371
|
+
const vendor = navigator.vendor;
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
/Chrome/.test(ua) && /Google Inc/.test(vendor) && !/Edg/.test(ua) && !/OPR/.test(ua)
|
|
375
|
+
);
|
|
376
|
+
})();
|
|
377
|
+
|
|
378
|
+
if (window.location.origin.startsWith("http://localhost") && isGoogleChrome) {
|
|
379
|
+
break third_party_cookies;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
log?.(
|
|
383
|
+
[
|
|
384
|
+
"Can't use iframe because your auth server is on a third party domain relative",
|
|
385
|
+
"to the domain of your app and third party cookies are blocked by navigators."
|
|
386
|
+
].join(" ")
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// NOTE: Maybe not, it depend if the app can iframe itself.
|
|
393
|
+
return true;
|
|
394
|
+
})();
|
|
395
|
+
|
|
396
|
+
let isUserStoreInMemoryOnly: boolean;
|
|
397
|
+
|
|
398
|
+
const oidcClientTsUserManager = new OidcClientTsUserManager({
|
|
399
|
+
stateUrlParamValue: stateUrlParamValue_instance,
|
|
400
|
+
authority: issuerUri,
|
|
401
|
+
client_id: clientId,
|
|
402
|
+
redirect_uri: homeUrlAndRedirectUri,
|
|
403
|
+
silent_redirect_uri: homeUrlAndRedirectUri,
|
|
404
|
+
post_logout_redirect_uri: homeUrlAndRedirectUri,
|
|
405
|
+
response_mode: isKeycloak({ issuerUri }) ? "fragment" : "query",
|
|
406
|
+
response_type: "code",
|
|
407
|
+
scope: Array.from(new Set(["openid", ...scopes])).join(" "),
|
|
408
|
+
automaticSilentRenew: false,
|
|
409
|
+
userStore: new WebStorageStateStore({
|
|
410
|
+
store: (() => {
|
|
411
|
+
if (canUseIframe) {
|
|
412
|
+
isUserStoreInMemoryOnly = true;
|
|
413
|
+
return new InMemoryWebStorage();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
isUserStoreInMemoryOnly = false;
|
|
417
|
+
|
|
418
|
+
const storage = createEphemeralSessionStorage({
|
|
419
|
+
sessionStorageTtlMs: 3 * 60_000
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const { evtRequestToPersistTokens } = globalContext;
|
|
423
|
+
|
|
424
|
+
evtRequestToPersistTokens.subscribe(({ configIdOfInstancePostingTheRequest }) => {
|
|
425
|
+
if (configIdOfInstancePostingTheRequest === configId) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
storage.persistCurrentStateAndSubsequentChanges();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return storage;
|
|
433
|
+
})()
|
|
434
|
+
}),
|
|
435
|
+
stateStore: new WebStorageStateStore({ store: localStorage, prefix: STATE_STORE_KEY_PREFIX }),
|
|
436
|
+
client_secret: __unsafe_clientSecret,
|
|
437
|
+
metadata: __metadata
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const evtIsUserLoggedIn = createEvt<boolean>();
|
|
441
|
+
|
|
442
|
+
const { loginOrGoToAuthServer } = createLoginOrGoToAuthServer({
|
|
443
|
+
configId,
|
|
444
|
+
oidcClientTsUserManager,
|
|
445
|
+
transformUrlBeforeRedirect,
|
|
446
|
+
getExtraQueryParams,
|
|
447
|
+
getExtraTokenParams,
|
|
448
|
+
homeUrl: homeUrlAndRedirectUri,
|
|
449
|
+
evtIsUserLoggedIn,
|
|
450
|
+
log
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const { getIsNewBrowserSession } = createGetIsNewBrowserSession({
|
|
454
|
+
configId,
|
|
455
|
+
evtUserNotLoggedIn: (() => {
|
|
456
|
+
const evt = createEvt<void>();
|
|
457
|
+
|
|
458
|
+
evtIsUserLoggedIn.subscribe(isUserLoggedIn => {
|
|
459
|
+
if (!isUserLoggedIn) {
|
|
460
|
+
evt.post();
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return evt;
|
|
465
|
+
})()
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const { completeLoginOrRefreshProcess } = await startLoginOrRefreshProcess();
|
|
469
|
+
|
|
470
|
+
const resultOfLoginProcess = await (async (): Promise<
|
|
471
|
+
| undefined // User is currently not logged in
|
|
472
|
+
| Error // Initialization error
|
|
473
|
+
| {
|
|
474
|
+
oidcClientTsUser: OidcClientTsUser;
|
|
475
|
+
backFromAuthServer: Oidc.LoggedIn["backFromAuthServer"]; // Undefined is silent signin
|
|
476
|
+
}
|
|
477
|
+
> => {
|
|
478
|
+
handle_redirect_auth_response: {
|
|
479
|
+
const authResponseAndStateData = retrieveRedirectAuthResponseAndStateData({ configId });
|
|
480
|
+
|
|
481
|
+
if (authResponseAndStateData === undefined) {
|
|
482
|
+
break handle_redirect_auth_response;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const { authResponse, stateData } = authResponseAndStateData;
|
|
486
|
+
|
|
487
|
+
switch (stateData.action) {
|
|
488
|
+
case "login":
|
|
489
|
+
{
|
|
490
|
+
log?.(
|
|
491
|
+
`Handling login redirect auth response ${JSON.stringify(
|
|
492
|
+
authResponse,
|
|
493
|
+
null,
|
|
494
|
+
2
|
|
495
|
+
)}`
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
const authResponseUrl = authResponseToUrl(authResponse);
|
|
499
|
+
|
|
500
|
+
let oidcClientTsUser: OidcClientTsUser | undefined = undefined;
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
oidcClientTsUser = await oidcClientTsUserManager.signinRedirectCallback(
|
|
504
|
+
authResponseUrl
|
|
505
|
+
);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
assert(error instanceof Error, "741947");
|
|
508
|
+
|
|
509
|
+
if (error.message === "Failed to fetch") {
|
|
510
|
+
return (
|
|
511
|
+
await import("./diagnostic")
|
|
512
|
+
).createFailedToFetchTokenEndpointInitializationError({
|
|
513
|
+
clientId,
|
|
514
|
+
issuerUri
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
{
|
|
519
|
+
const authResponse_error = authResponse.error;
|
|
520
|
+
|
|
521
|
+
if (authResponse_error !== undefined) {
|
|
522
|
+
log?.(
|
|
523
|
+
`The auth server responded with: ${authResponse_error}, trying to restore from the http only cookie`
|
|
524
|
+
);
|
|
525
|
+
break handle_redirect_auth_response;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return error;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
notifyOtherTabsOfLogin({ configId });
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
oidcClientTsUser,
|
|
536
|
+
backFromAuthServer: {
|
|
537
|
+
extraQueryParams: stateData.extraQueryParams,
|
|
538
|
+
result: Object.fromEntries(
|
|
539
|
+
Object.entries(authResponse)
|
|
540
|
+
.map(([name, value]) => {
|
|
541
|
+
if (
|
|
542
|
+
name === "state" ||
|
|
543
|
+
name === "session_state" ||
|
|
544
|
+
name === "iss" ||
|
|
545
|
+
name === "code"
|
|
546
|
+
) {
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (value === undefined) {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return [name, value];
|
|
555
|
+
})
|
|
556
|
+
.filter(entry => entry !== undefined)
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
case "logout":
|
|
563
|
+
{
|
|
564
|
+
log?.("Handling logout redirect auth response", authResponse);
|
|
565
|
+
|
|
566
|
+
const authResponseUrl = authResponseToUrl(authResponse);
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
await oidcClientTsUserManager.signoutRedirectCallback(authResponseUrl);
|
|
570
|
+
} catch {}
|
|
571
|
+
|
|
572
|
+
notifyOtherTabsOfLogout({
|
|
573
|
+
configId,
|
|
574
|
+
sessionId: stateData.sessionId
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
if (autoLogin) {
|
|
578
|
+
location.reload();
|
|
579
|
+
await new Promise<never>(() => {});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// NOTE: The user is no longer logged in.
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
restore_from_session_storage: {
|
|
590
|
+
if (isUserStoreInMemoryOnly) {
|
|
591
|
+
break restore_from_session_storage;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
let oidcClientTsUser: OidcClientTsUser | null;
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
oidcClientTsUser = await oidcClientTsUserManager.getUser();
|
|
598
|
+
} catch {
|
|
599
|
+
// NOTE: Not sure if it can throw, but let's be safe.
|
|
600
|
+
oidcClientTsUser = null;
|
|
601
|
+
try {
|
|
602
|
+
await oidcClientTsUserManager.removeUser();
|
|
603
|
+
} catch {}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (oidcClientTsUser === null) {
|
|
607
|
+
break restore_from_session_storage;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
log?.("Restored the auth from ephemeral session storage");
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
oidcClientTsUser,
|
|
614
|
+
backFromAuthServer: undefined
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
silent_login_if_possible_and_auto_login: {
|
|
619
|
+
const persistedAuthState = getPersistedAuthState({ configId });
|
|
620
|
+
|
|
621
|
+
if (persistedAuthState === "explicitly logged out" && !autoLogin) {
|
|
622
|
+
log?.("Skipping silent signin with iframe, the user has logged out");
|
|
623
|
+
break silent_login_if_possible_and_auto_login;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
{
|
|
627
|
+
const { isOnline, prOnline } = getIsOnline();
|
|
628
|
+
|
|
629
|
+
if (!isOnline) {
|
|
630
|
+
if (autoLogin) {
|
|
631
|
+
log?.(
|
|
632
|
+
[
|
|
633
|
+
"The browser is currently offline",
|
|
634
|
+
"Since autoLogin is enabled we wait until it comes back online",
|
|
635
|
+
"to continue with authentication"
|
|
636
|
+
].join(" ")
|
|
637
|
+
);
|
|
638
|
+
await prOnline;
|
|
639
|
+
} else {
|
|
640
|
+
log?.(
|
|
641
|
+
[
|
|
642
|
+
"The browser is not currently online so we proceed with initialization",
|
|
643
|
+
"assuming the user isn't authenticated"
|
|
644
|
+
].join(" ")
|
|
645
|
+
);
|
|
646
|
+
break silent_login_if_possible_and_auto_login;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
let authResponse_error: string | undefined = undefined;
|
|
652
|
+
let oidcClientTsUser: OidcClientTsUser | undefined = undefined;
|
|
653
|
+
|
|
654
|
+
actual_silent_signin: {
|
|
655
|
+
if (persistedAuthState === "explicitly logged out") {
|
|
656
|
+
break actual_silent_signin;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!canUseIframe) {
|
|
660
|
+
break actual_silent_signin;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
log?.(
|
|
664
|
+
"Trying to restore the auth from the http only cookie (silent signin with iframe)"
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const result_loginSilent = await loginSilent({
|
|
668
|
+
oidcClientTsUserManager,
|
|
669
|
+
stateUrlParamValue_instance,
|
|
670
|
+
configId,
|
|
671
|
+
transformUrlBeforeRedirect,
|
|
672
|
+
getExtraQueryParams,
|
|
673
|
+
getExtraTokenParams,
|
|
674
|
+
autoLogin
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
assert(result_loginSilent.outcome !== "token refreshed using refresh token", "876995");
|
|
678
|
+
|
|
679
|
+
if (result_loginSilent.outcome === "failure") {
|
|
680
|
+
switch (result_loginSilent.cause) {
|
|
681
|
+
case "can't reach well-known oidc endpoint":
|
|
682
|
+
return (
|
|
683
|
+
await import("./diagnostic")
|
|
684
|
+
).createWellKnownOidcConfigurationEndpointUnreachableInitializationError({
|
|
685
|
+
issuerUri
|
|
686
|
+
});
|
|
687
|
+
case "timeout":
|
|
688
|
+
return (await import("./diagnostic")).createIframeTimeoutInitializationError(
|
|
689
|
+
{
|
|
690
|
+
redirectUri: homeUrlAndRedirectUri,
|
|
691
|
+
clientId,
|
|
692
|
+
issuerUri,
|
|
693
|
+
noIframe
|
|
694
|
+
}
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
assert<Equals<typeof result_loginSilent.cause, never>>(false);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
assert<Equals<typeof result_loginSilent.outcome, "got auth response from iframe">>();
|
|
702
|
+
|
|
703
|
+
const { authResponse } = result_loginSilent;
|
|
704
|
+
|
|
705
|
+
log?.(`Silent signin auth response ${JSON.stringify(authResponse, null, 2)}`);
|
|
706
|
+
|
|
707
|
+
authResponse_error = authResponse.error;
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
oidcClientTsUser = await oidcClientTsUserManager.signinRedirectCallback(
|
|
711
|
+
authResponseToUrl(authResponse)
|
|
712
|
+
);
|
|
713
|
+
} catch (error) {
|
|
714
|
+
assert(error instanceof Error, "433344");
|
|
715
|
+
|
|
716
|
+
if (error.message === "Failed to fetch") {
|
|
717
|
+
return (
|
|
718
|
+
await import("./diagnostic")
|
|
719
|
+
).createFailedToFetchTokenEndpointInitializationError({
|
|
720
|
+
clientId,
|
|
721
|
+
issuerUri
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (authResponse_error === undefined) {
|
|
726
|
+
return error;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (oidcClientTsUser === undefined) {
|
|
732
|
+
if (
|
|
733
|
+
autoLogin ||
|
|
734
|
+
(persistedAuthState === "logged in" &&
|
|
735
|
+
(authResponse_error === undefined ||
|
|
736
|
+
authResponse_error === "interaction_required" ||
|
|
737
|
+
authResponse_error === "login_required" ||
|
|
738
|
+
authResponse_error === "consent_required" ||
|
|
739
|
+
authResponse_error === "account_selection_required"))
|
|
740
|
+
) {
|
|
741
|
+
log?.("Performing auto login with redirect");
|
|
742
|
+
|
|
743
|
+
persistAuthState({ configId, state: undefined });
|
|
744
|
+
|
|
745
|
+
completeLoginOrRefreshProcess();
|
|
746
|
+
|
|
747
|
+
if (autoLogin && persistedAuthState !== "logged in") {
|
|
748
|
+
evtIsUserLoggedIn.post(false);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
|
|
752
|
+
prUnlock: new Promise<never>(() => {})
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
if (persistedAuthState === "logged in") {
|
|
756
|
+
globalContext.evtRequestToPersistTokens.post({
|
|
757
|
+
configIdOfInstancePostingTheRequest: configId
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
await loginOrGoToAuthServer({
|
|
762
|
+
action: "login",
|
|
763
|
+
doForceReloadOnBfCache: true,
|
|
764
|
+
redirectUrl: initialLocationHref,
|
|
765
|
+
// NOTE: Wether or not it's the preferred behavior, pushing to history
|
|
766
|
+
// only works on user interaction so it have to be false
|
|
767
|
+
doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack: false,
|
|
768
|
+
extraQueryParams_local: undefined,
|
|
769
|
+
transformUrlBeforeRedirect_local: undefined,
|
|
770
|
+
interaction: (() => {
|
|
771
|
+
if (persistedAuthState === "explicitly logged out") {
|
|
772
|
+
return "ensure interaction";
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (autoLogin) {
|
|
776
|
+
return "directly redirect if active session show login otherwise";
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return "ensure no interaction";
|
|
780
|
+
})()
|
|
781
|
+
});
|
|
782
|
+
assert(false, "321389");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (authResponse_error !== undefined) {
|
|
786
|
+
log?.(
|
|
787
|
+
[
|
|
788
|
+
`The auth server responded with: ${authResponse_error} `,
|
|
789
|
+
"login_required" === authResponse_error
|
|
790
|
+
? `(login_required just means that there's no active session for the user)`
|
|
791
|
+
: ""
|
|
792
|
+
].join("")
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
break silent_login_if_possible_and_auto_login;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
log?.("Successful silent signed in");
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
oidcClientTsUser,
|
|
803
|
+
backFromAuthServer: undefined
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// NOTE: The user is not logged in.
|
|
808
|
+
return undefined;
|
|
809
|
+
})();
|
|
810
|
+
|
|
811
|
+
completeLoginOrRefreshProcess();
|
|
812
|
+
|
|
813
|
+
await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
|
|
814
|
+
prUnlock: Promise.resolve()
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const oidc_common: Oidc.Common = {
|
|
818
|
+
params: {
|
|
819
|
+
issuerUri,
|
|
820
|
+
clientId
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
not_loggedIn_case: {
|
|
825
|
+
if (!(resultOfLoginProcess instanceof Error) && resultOfLoginProcess !== undefined) {
|
|
826
|
+
break not_loggedIn_case;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
evtIsUserLoggedIn.post(false);
|
|
830
|
+
|
|
831
|
+
if (getPersistedAuthState({ configId }) !== "explicitly logged out") {
|
|
832
|
+
persistAuthState({ configId, state: undefined });
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const oidc_notLoggedIn: Oidc.NotLoggedIn = (() => {
|
|
836
|
+
if (resultOfLoginProcess instanceof Error) {
|
|
837
|
+
log?.("User not logged in and there was an initialization error");
|
|
838
|
+
|
|
839
|
+
const error = resultOfLoginProcess;
|
|
840
|
+
|
|
841
|
+
const initializationError =
|
|
842
|
+
error instanceof OidcInitializationError
|
|
843
|
+
? error
|
|
844
|
+
: new OidcInitializationError({
|
|
845
|
+
isAuthServerLikelyDown: false,
|
|
846
|
+
messageOrCause: error
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
if (autoLogin) {
|
|
850
|
+
throw initializationError;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
console.error(
|
|
854
|
+
[
|
|
855
|
+
`oidc-spa Initialization Error: `,
|
|
856
|
+
`isAuthServerLikelyDown: ${initializationError.isAuthServerLikelyDown}`,
|
|
857
|
+
``,
|
|
858
|
+
initializationError.message
|
|
859
|
+
].join("\n")
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
return id<Oidc.NotLoggedIn>({
|
|
863
|
+
...oidc_common,
|
|
864
|
+
isUserLoggedIn: false,
|
|
865
|
+
login: async () => {
|
|
866
|
+
alert("Authentication is currently unavailable. Please try again later.");
|
|
867
|
+
return new Promise<never>(() => {});
|
|
868
|
+
},
|
|
869
|
+
initializationError
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (resultOfLoginProcess === undefined) {
|
|
874
|
+
log?.("User not logged in");
|
|
875
|
+
|
|
876
|
+
return id<Oidc.NotLoggedIn>({
|
|
877
|
+
...oidc_common,
|
|
878
|
+
isUserLoggedIn: false,
|
|
879
|
+
login: async ({
|
|
880
|
+
doesCurrentHrefRequiresAuth,
|
|
881
|
+
extraQueryParams,
|
|
882
|
+
redirectUrl,
|
|
883
|
+
transformUrlBeforeRedirect
|
|
884
|
+
}) => {
|
|
885
|
+
await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
|
|
886
|
+
prUnlock: getPrSafelyRestoredFromBfCacheAfterLoginBackNavigation()
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
return loginOrGoToAuthServer({
|
|
890
|
+
action: "login",
|
|
891
|
+
doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack:
|
|
892
|
+
doesCurrentHrefRequiresAuth,
|
|
893
|
+
doForceReloadOnBfCache: false,
|
|
894
|
+
redirectUrl:
|
|
895
|
+
redirectUrl ?? postLoginRedirectUrl_default ?? window.location.href,
|
|
896
|
+
extraQueryParams_local: extraQueryParams,
|
|
897
|
+
transformUrlBeforeRedirect_local: transformUrlBeforeRedirect,
|
|
898
|
+
interaction:
|
|
899
|
+
getPersistedAuthState({ configId }) === "explicitly logged out"
|
|
900
|
+
? "ensure interaction"
|
|
901
|
+
: "directly redirect if active session show login otherwise"
|
|
902
|
+
});
|
|
903
|
+
},
|
|
904
|
+
initializationError: undefined
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
assert<Equals<typeof resultOfLoginProcess, never>>(false);
|
|
909
|
+
})();
|
|
910
|
+
|
|
911
|
+
{
|
|
912
|
+
const { prOtherTabLogin } = getPrOtherTabLogin({
|
|
913
|
+
configId
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
prOtherTabLogin.then(async () => {
|
|
917
|
+
log?.(`Other tab has logged in, reloading this tab`);
|
|
918
|
+
|
|
919
|
+
await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
|
|
920
|
+
prUnlock: new Promise<never>(() => {})
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
window.location.reload();
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// @ts-expect-error: We know what we're doing
|
|
928
|
+
return oidc_notLoggedIn;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
log?.("User is logged in");
|
|
932
|
+
|
|
933
|
+
evtIsUserLoggedIn.post(true);
|
|
934
|
+
|
|
935
|
+
let currentTokens = oidcClientTsUserToTokens({
|
|
936
|
+
oidcClientTsUser: resultOfLoginProcess.oidcClientTsUser,
|
|
937
|
+
decodedIdTokenSchema,
|
|
938
|
+
__unsafe_useIdTokenAsAccessToken,
|
|
939
|
+
decodedIdToken_previous: undefined,
|
|
940
|
+
log
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
{
|
|
944
|
+
if (getPersistedAuthState({ configId }) !== undefined) {
|
|
945
|
+
persistAuthState({ configId, state: undefined });
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (!canUseIframe) {
|
|
949
|
+
persistAuthState({
|
|
950
|
+
configId,
|
|
951
|
+
state: {
|
|
952
|
+
stateDescription: "logged in",
|
|
953
|
+
refreshTokenExpirationTime: currentTokens.refreshTokenExpirationTime,
|
|
954
|
+
idleSessionLifetimeInSeconds
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const autoLogoutCountdownTickCallbacks = new Set<
|
|
961
|
+
(params: { secondsLeft: number | undefined }) => void
|
|
962
|
+
>();
|
|
963
|
+
|
|
964
|
+
const onTokenChanges = new Set<(tokens: Oidc.Tokens<DecodedIdToken>) => void>();
|
|
965
|
+
|
|
966
|
+
const { sid: sessionId, sub: subjectId } = currentTokens.decodedIdToken_original;
|
|
967
|
+
|
|
968
|
+
assert(subjectId !== undefined, "The 'sub' claim is missing from the id token");
|
|
969
|
+
assert(sessionId === undefined || typeof sessionId === "string");
|
|
970
|
+
|
|
971
|
+
let wouldHaveAutoLoggedOutIfBrowserWasOnline = false;
|
|
972
|
+
|
|
973
|
+
const oidc_loggedIn = id<Oidc.LoggedIn<DecodedIdToken>>({
|
|
974
|
+
...oidc_common,
|
|
975
|
+
isUserLoggedIn: true,
|
|
976
|
+
getTokens: async () => {
|
|
977
|
+
if (wouldHaveAutoLoggedOutIfBrowserWasOnline) {
|
|
978
|
+
await oidc_loggedIn.logout(autoLogoutParams);
|
|
979
|
+
assert(false);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
renew_tokens: {
|
|
983
|
+
{
|
|
984
|
+
const msBeforeExpirationOfTheAccessToken =
|
|
985
|
+
currentTokens.accessTokenExpirationTime - Date.now();
|
|
986
|
+
|
|
987
|
+
if (msBeforeExpirationOfTheAccessToken > 30_000) {
|
|
988
|
+
break renew_tokens;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
{
|
|
993
|
+
const msElapsedSinceCurrentTokenWereIssued = Date.now() - currentTokens.issuedAtTime;
|
|
994
|
+
|
|
995
|
+
if (msElapsedSinceCurrentTokenWereIssued < 5_000) {
|
|
996
|
+
break renew_tokens;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
await oidc_loggedIn.renewTokens();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return currentTokens;
|
|
1004
|
+
},
|
|
1005
|
+
getDecodedIdToken: () => currentTokens.decodedIdToken,
|
|
1006
|
+
logout: async params => {
|
|
1007
|
+
if (globalContext.hasLogoutBeenCalled) {
|
|
1008
|
+
log?.("logout() has already been called, ignoring the call");
|
|
1009
|
+
return new Promise<never>(() => {});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
globalContext.hasLogoutBeenCalled = true;
|
|
1013
|
+
|
|
1014
|
+
const postLogoutRedirectUrl: string = (() => {
|
|
1015
|
+
switch (params.redirectTo) {
|
|
1016
|
+
case "current page":
|
|
1017
|
+
return window.location.href;
|
|
1018
|
+
case "home":
|
|
1019
|
+
return homeUrlAndRedirectUri;
|
|
1020
|
+
case "specific url":
|
|
1021
|
+
return toFullyQualifiedUrl({
|
|
1022
|
+
urlish: params.url,
|
|
1023
|
+
doAssertNoQueryParams: false
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
})();
|
|
1027
|
+
|
|
1028
|
+
await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
|
|
1029
|
+
prUnlock: new Promise<never>(() => {})
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
window.addEventListener("pageshow", () => {
|
|
1033
|
+
location.reload();
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
try {
|
|
1037
|
+
await oidcClientTsUserManager.signoutRedirect({
|
|
1038
|
+
state: id<StateData>({
|
|
1039
|
+
configId,
|
|
1040
|
+
context: "redirect",
|
|
1041
|
+
redirectUrl: postLogoutRedirectUrl,
|
|
1042
|
+
hasBeenProcessedByCallback: false,
|
|
1043
|
+
action: "logout",
|
|
1044
|
+
sessionId
|
|
1045
|
+
}),
|
|
1046
|
+
redirectMethod: "assign"
|
|
1047
|
+
});
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
assert(is<Error>(error));
|
|
1050
|
+
|
|
1051
|
+
if (error.message === "No end session endpoint") {
|
|
1052
|
+
log?.("No end session endpoint, managing logging state locally");
|
|
1053
|
+
|
|
1054
|
+
persistAuthState({ configId, state: { stateDescription: "explicitly logged out" } });
|
|
1055
|
+
|
|
1056
|
+
try {
|
|
1057
|
+
await oidcClientTsUserManager.removeUser();
|
|
1058
|
+
} catch {
|
|
1059
|
+
// NOTE: Not sure if it can throw
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
notifyOtherTabsOfLogout({
|
|
1063
|
+
configId,
|
|
1064
|
+
sessionId
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
window.location.href = postLogoutRedirectUrl;
|
|
1068
|
+
} else {
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return new Promise<never>(() => {});
|
|
1074
|
+
},
|
|
1075
|
+
renewTokens: (() => {
|
|
1076
|
+
async function renewTokens_nonMutexed(params: {
|
|
1077
|
+
extraTokenParams: Record<string, string | undefined>;
|
|
1078
|
+
}) {
|
|
1079
|
+
const { extraTokenParams } = params;
|
|
1080
|
+
|
|
1081
|
+
const fallbackToFullPageReload = async (): Promise<never> => {
|
|
1082
|
+
persistAuthState({ configId, state: undefined });
|
|
1083
|
+
|
|
1084
|
+
await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
|
|
1085
|
+
prUnlock: new Promise<never>(() => {})
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
globalContext.evtRequestToPersistTokens.post({
|
|
1089
|
+
configIdOfInstancePostingTheRequest: configId
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
await loginOrGoToAuthServer({
|
|
1093
|
+
action: "login",
|
|
1094
|
+
redirectUrl: window.location.href,
|
|
1095
|
+
doForceReloadOnBfCache: true,
|
|
1096
|
+
extraQueryParams_local: undefined,
|
|
1097
|
+
transformUrlBeforeRedirect_local: undefined,
|
|
1098
|
+
doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack: false,
|
|
1099
|
+
interaction: "directly redirect if active session show login otherwise"
|
|
1100
|
+
});
|
|
1101
|
+
assert(false, "136134");
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
if (!currentTokens.hasRefreshToken && !canUseIframe) {
|
|
1105
|
+
log?.(
|
|
1106
|
+
[
|
|
1107
|
+
"Unable to refresh tokens without a full app reload,",
|
|
1108
|
+
"because no refresh token is available",
|
|
1109
|
+
"and your app setup prevents silent sign-in via iframe.",
|
|
1110
|
+
"Your only option to refresh tokens is to call `window.location.reload()`"
|
|
1111
|
+
].join(" ")
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
await fallbackToFullPageReload();
|
|
1115
|
+
|
|
1116
|
+
assert(false, "136135");
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
log?.("Renewing tokens");
|
|
1120
|
+
|
|
1121
|
+
const { completeLoginOrRefreshProcess } = await startLoginOrRefreshProcess();
|
|
1122
|
+
|
|
1123
|
+
const result_loginSilent = await loginSilent({
|
|
1124
|
+
oidcClientTsUserManager,
|
|
1125
|
+
stateUrlParamValue_instance,
|
|
1126
|
+
configId,
|
|
1127
|
+
transformUrlBeforeRedirect,
|
|
1128
|
+
getExtraQueryParams,
|
|
1129
|
+
getExtraTokenParams: () => extraTokenParams,
|
|
1130
|
+
autoLogin
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
if (result_loginSilent.outcome === "failure") {
|
|
1134
|
+
completeLoginOrRefreshProcess();
|
|
1135
|
+
// NOTE: This is a configuration or network error, okay to throw,
|
|
1136
|
+
// this exception doesn't have to be handle if it fails it fails.
|
|
1137
|
+
throw new Error(result_loginSilent.cause);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
let oidcClientTsUser: OidcClientTsUser;
|
|
1141
|
+
|
|
1142
|
+
switch (result_loginSilent.outcome) {
|
|
1143
|
+
case "token refreshed using refresh token":
|
|
1144
|
+
{
|
|
1145
|
+
log?.("Refresh token used");
|
|
1146
|
+
oidcClientTsUser = result_loginSilent.oidcClientTsUser;
|
|
1147
|
+
}
|
|
1148
|
+
break;
|
|
1149
|
+
case "got auth response from iframe":
|
|
1150
|
+
{
|
|
1151
|
+
const { authResponse } = result_loginSilent;
|
|
1152
|
+
|
|
1153
|
+
log?.("Tokens refresh using iframe", authResponse);
|
|
1154
|
+
|
|
1155
|
+
const authResponse_error = authResponse.error;
|
|
1156
|
+
|
|
1157
|
+
let oidcClientTsUser_scope: OidcClientTsUser | undefined = undefined;
|
|
1158
|
+
|
|
1159
|
+
try {
|
|
1160
|
+
oidcClientTsUser_scope =
|
|
1161
|
+
await oidcClientTsUserManager.signinRedirectCallback(
|
|
1162
|
+
authResponseToUrl(authResponse)
|
|
1163
|
+
);
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
assert(error instanceof Error, "321389");
|
|
1166
|
+
|
|
1167
|
+
if (authResponse_error === undefined) {
|
|
1168
|
+
completeLoginOrRefreshProcess();
|
|
1169
|
+
// Same here, if it fails it fails.
|
|
1170
|
+
throw error;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (oidcClientTsUser_scope === undefined) {
|
|
1175
|
+
// NOTE: Here we got a response but it's an error, session might have been
|
|
1176
|
+
// deleted or other edge case.
|
|
1177
|
+
|
|
1178
|
+
completeLoginOrRefreshProcess();
|
|
1179
|
+
|
|
1180
|
+
log?.(
|
|
1181
|
+
[
|
|
1182
|
+
"The user is probably not logged in anymore,",
|
|
1183
|
+
"need to redirect to login pages"
|
|
1184
|
+
].join(" ")
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
await fallbackToFullPageReload();
|
|
1188
|
+
|
|
1189
|
+
assert(false, "136135");
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
oidcClientTsUser = oidcClientTsUser_scope;
|
|
1193
|
+
}
|
|
1194
|
+
break;
|
|
1195
|
+
default:
|
|
1196
|
+
assert<Equals<typeof result_loginSilent, never>>(false);
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
currentTokens = oidcClientTsUserToTokens({
|
|
1201
|
+
oidcClientTsUser,
|
|
1202
|
+
decodedIdTokenSchema,
|
|
1203
|
+
__unsafe_useIdTokenAsAccessToken,
|
|
1204
|
+
decodedIdToken_previous: currentTokens.decodedIdToken,
|
|
1205
|
+
log
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
if (getPersistedAuthState({ configId }) !== undefined) {
|
|
1209
|
+
persistAuthState({
|
|
1210
|
+
configId,
|
|
1211
|
+
state: {
|
|
1212
|
+
stateDescription: "logged in",
|
|
1213
|
+
refreshTokenExpirationTime: currentTokens.refreshTokenExpirationTime,
|
|
1214
|
+
idleSessionLifetimeInSeconds
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
Array.from(onTokenChanges).forEach(onTokenChange => onTokenChange(currentTokens));
|
|
1220
|
+
|
|
1221
|
+
completeLoginOrRefreshProcess();
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
let ongoingCall:
|
|
1225
|
+
| {
|
|
1226
|
+
pr: Promise<void>;
|
|
1227
|
+
extraTokenParams: Record<string, string | undefined>;
|
|
1228
|
+
}
|
|
1229
|
+
| undefined = undefined;
|
|
1230
|
+
|
|
1231
|
+
function handleFinally() {
|
|
1232
|
+
assert(ongoingCall !== undefined, "131276");
|
|
1233
|
+
|
|
1234
|
+
const { pr } = ongoingCall;
|
|
1235
|
+
|
|
1236
|
+
pr.finally(() => {
|
|
1237
|
+
assert(ongoingCall !== undefined, "549462");
|
|
1238
|
+
|
|
1239
|
+
if (ongoingCall.pr !== pr) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
ongoingCall = undefined;
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
return async params => {
|
|
1248
|
+
const { extraTokenParams: extraTokenParams_local } = params ?? {};
|
|
1249
|
+
|
|
1250
|
+
const extraTokenParams = {
|
|
1251
|
+
...getExtraTokenParams?.(),
|
|
1252
|
+
...extraTokenParams_local
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
if (ongoingCall === undefined) {
|
|
1256
|
+
ongoingCall = {
|
|
1257
|
+
pr: renewTokens_nonMutexed({ extraTokenParams }),
|
|
1258
|
+
extraTokenParams
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
handleFinally();
|
|
1262
|
+
|
|
1263
|
+
return ongoingCall.pr;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (JSON.stringify(extraTokenParams) === JSON.stringify(ongoingCall.extraTokenParams)) {
|
|
1267
|
+
return ongoingCall.pr;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
ongoingCall = {
|
|
1271
|
+
pr: (async () => {
|
|
1272
|
+
try {
|
|
1273
|
+
await ongoingCall.pr;
|
|
1274
|
+
} catch {}
|
|
1275
|
+
|
|
1276
|
+
return renewTokens_nonMutexed({ extraTokenParams });
|
|
1277
|
+
})(),
|
|
1278
|
+
extraTokenParams
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
handleFinally();
|
|
1282
|
+
|
|
1283
|
+
return ongoingCall.pr;
|
|
1284
|
+
};
|
|
1285
|
+
})(),
|
|
1286
|
+
subscribeToTokensChange: onTokenChange => {
|
|
1287
|
+
onTokenChanges.add(onTokenChange);
|
|
1288
|
+
|
|
1289
|
+
return {
|
|
1290
|
+
unsubscribe: () => {
|
|
1291
|
+
onTokenChanges.delete(onTokenChange);
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
},
|
|
1295
|
+
subscribeToAutoLogoutCountdown: tickCallback => {
|
|
1296
|
+
autoLogoutCountdownTickCallbacks.add(tickCallback);
|
|
1297
|
+
|
|
1298
|
+
const unsubscribeFromAutoLogoutCountdown = () => {
|
|
1299
|
+
autoLogoutCountdownTickCallbacks.delete(tickCallback);
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
return { unsubscribeFromAutoLogoutCountdown };
|
|
1303
|
+
},
|
|
1304
|
+
goToAuthServer: ({ extraQueryParams, redirectUrl, transformUrlBeforeRedirect }) =>
|
|
1305
|
+
loginOrGoToAuthServer({
|
|
1306
|
+
action: "go to auth server",
|
|
1307
|
+
redirectUrl: redirectUrl ?? window.location.href,
|
|
1308
|
+
extraQueryParams_local: extraQueryParams,
|
|
1309
|
+
transformUrlBeforeRedirect_local: transformUrlBeforeRedirect
|
|
1310
|
+
}),
|
|
1311
|
+
backFromAuthServer: resultOfLoginProcess.backFromAuthServer,
|
|
1312
|
+
isNewBrowserSession: (() => {
|
|
1313
|
+
const value = getIsNewBrowserSession({ subjectId });
|
|
1314
|
+
|
|
1315
|
+
log?.(`isNewBrowserSession: ${value}`);
|
|
1316
|
+
|
|
1317
|
+
return value;
|
|
1318
|
+
})()
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
{
|
|
1322
|
+
const { prOtherTabLogout } = getPrOtherTabLogout({
|
|
1323
|
+
configId,
|
|
1324
|
+
sessionId
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
prOtherTabLogout.then(async () => {
|
|
1328
|
+
log?.(`Other tab has logged out, refreshing current tab`);
|
|
1329
|
+
|
|
1330
|
+
await waitForAllOtherOngoingLoginOrRefreshProcessesToComplete({
|
|
1331
|
+
prUnlock: new Promise<never>(() => {})
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
location.reload();
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
(function scheduleRenew() {
|
|
1339
|
+
if (!currentTokens.hasRefreshToken && !canUseIframe) {
|
|
1340
|
+
log?.(
|
|
1341
|
+
[
|
|
1342
|
+
"Disabling token auto refresh mechanism because we",
|
|
1343
|
+
"have no way to renew the tokens without a full page reload"
|
|
1344
|
+
].join(" ")
|
|
1345
|
+
);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const msBeforeExpiration =
|
|
1350
|
+
(currentTokens.refreshTokenExpirationTime ?? currentTokens.accessTokenExpirationTime) -
|
|
1351
|
+
Date.now();
|
|
1352
|
+
|
|
1353
|
+
const typeOfTheTokenWeGotTheTtlFrom =
|
|
1354
|
+
currentTokens.refreshTokenExpirationTime !== undefined ? "refresh" : "access";
|
|
1355
|
+
|
|
1356
|
+
const RENEW_MS_BEFORE_EXPIRES = 30_000;
|
|
1357
|
+
|
|
1358
|
+
if (msBeforeExpiration <= RENEW_MS_BEFORE_EXPIRES) {
|
|
1359
|
+
// NOTE: We just got a new token that is about to expire. This means that
|
|
1360
|
+
// the refresh token has reached it's max SSO time.
|
|
1361
|
+
// ...or that the refresh token have a very short lifespan...
|
|
1362
|
+
// anyway, no need to keep alive, it will probably redirect on the next getTokens() or refreshTokens() call
|
|
1363
|
+
log?.(
|
|
1364
|
+
[
|
|
1365
|
+
"Disabling auto renew mechanism. We just got fresh tokens",
|
|
1366
|
+
(() => {
|
|
1367
|
+
switch (typeOfTheTokenWeGotTheTtlFrom) {
|
|
1368
|
+
case "refresh":
|
|
1369
|
+
return [
|
|
1370
|
+
" and the refresh token is already about to expires.",
|
|
1371
|
+
"This means that we have reached the max session lifespan, we can't keep",
|
|
1372
|
+
"the session alive any longer.",
|
|
1373
|
+
"(This can also mean that the refresh token was configured with a TTL,",
|
|
1374
|
+
"aka the idle session lifespan, too low to make sense)"
|
|
1375
|
+
].join(" ");
|
|
1376
|
+
case "access":
|
|
1377
|
+
return [
|
|
1378
|
+
currentTokens.hasRefreshToken
|
|
1379
|
+
? ", we can't read the expiration time of the refresh token"
|
|
1380
|
+
: ", we don't have a refresh token",
|
|
1381
|
+
` and the access token is already about to expire`,
|
|
1382
|
+
"we would spam the auth server by constantly renewing the access token in the background",
|
|
1383
|
+
"avoiding to do so."
|
|
1384
|
+
].join(" ");
|
|
1385
|
+
}
|
|
1386
|
+
})()
|
|
1387
|
+
].join(" ")
|
|
1388
|
+
);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
log?.(
|
|
1393
|
+
[
|
|
1394
|
+
toHumanReadableDuration(msBeforeExpiration),
|
|
1395
|
+
`before expiration of the ${typeOfTheTokenWeGotTheTtlFrom} token.`,
|
|
1396
|
+
`Scheduling renewal ${toHumanReadableDuration(
|
|
1397
|
+
RENEW_MS_BEFORE_EXPIRES
|
|
1398
|
+
)} before expiration to keep the session alive on the OIDC server.`
|
|
1399
|
+
].join(" ")
|
|
1400
|
+
);
|
|
1401
|
+
|
|
1402
|
+
const timer = setTimeout(
|
|
1403
|
+
async () => {
|
|
1404
|
+
{
|
|
1405
|
+
const { isOnline, prOnline } = getIsOnline();
|
|
1406
|
+
|
|
1407
|
+
if (!isOnline) {
|
|
1408
|
+
const didCameBackOnlineInTime = await Promise.race([
|
|
1409
|
+
new Promise<false>(resolve =>
|
|
1410
|
+
setTimeout(() => resolve(false), RENEW_MS_BEFORE_EXPIRES - 1_000)
|
|
1411
|
+
),
|
|
1412
|
+
prOnline.then(() => true)
|
|
1413
|
+
]);
|
|
1414
|
+
|
|
1415
|
+
if (!didCameBackOnlineInTime) {
|
|
1416
|
+
log?.(
|
|
1417
|
+
[
|
|
1418
|
+
"The session expired on the OIDC server.",
|
|
1419
|
+
"We couldn't keep it alive because the browser was offline.",
|
|
1420
|
+
"We are not redirecting to the login page to support PWAs with offline features.",
|
|
1421
|
+
"However, the next getTokens() call will trigger a redirect to the Auth server login page."
|
|
1422
|
+
].join(" ")
|
|
1423
|
+
);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
log?.(
|
|
1430
|
+
`Renewing the tokens now as the ${typeOfTheTokenWeGotTheTtlFrom} token will expire in ${toHumanReadableDuration(
|
|
1431
|
+
RENEW_MS_BEFORE_EXPIRES
|
|
1432
|
+
)}`
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
await oidc_loggedIn.renewTokens();
|
|
1436
|
+
},
|
|
1437
|
+
Math.min(
|
|
1438
|
+
msBeforeExpiration - RENEW_MS_BEFORE_EXPIRES,
|
|
1439
|
+
// NOTE: We want to make sure we do not overflow the setTimeout
|
|
1440
|
+
// that must be a 32 bit unsigned integer.
|
|
1441
|
+
// This can happen if the tokenExpirationTime is more than 24.8 days in the future.
|
|
1442
|
+
Math.pow(2, 31) - 1
|
|
1443
|
+
)
|
|
1444
|
+
);
|
|
1445
|
+
|
|
1446
|
+
const { unsubscribe: tokenChangeUnsubscribe } = oidc_loggedIn.subscribeToTokensChange(() => {
|
|
1447
|
+
clearTimeout(timer);
|
|
1448
|
+
tokenChangeUnsubscribe();
|
|
1449
|
+
scheduleRenew();
|
|
1450
|
+
});
|
|
1451
|
+
})();
|
|
1452
|
+
|
|
1453
|
+
auto_logout: {
|
|
1454
|
+
const getCurrentRefreshTokenTtlInSeconds = () => {
|
|
1455
|
+
if (idleSessionLifetimeInSeconds !== undefined) {
|
|
1456
|
+
return idleSessionLifetimeInSeconds;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (currentTokens.refreshTokenExpirationTime === undefined) {
|
|
1460
|
+
return undefined;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
return (currentTokens.refreshTokenExpirationTime - currentTokens.issuedAtTime) / 1000;
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
if (getCurrentRefreshTokenTtlInSeconds() === undefined) {
|
|
1467
|
+
log?.(
|
|
1468
|
+
`${
|
|
1469
|
+
currentTokens.hasRefreshToken
|
|
1470
|
+
? "The refresh token is opaque, we can't read it's expiration time"
|
|
1471
|
+
: "No refresh token"
|
|
1472
|
+
}, and idleSessionLifetimeInSeconds was not set, can't implement auto logout mechanism`
|
|
1473
|
+
);
|
|
1474
|
+
break auto_logout;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const { startCountdown } = createStartCountdown({
|
|
1478
|
+
tickCallback: async ({ secondsLeft }) => {
|
|
1479
|
+
const invokeAllCallbacks = (params: { secondsLeft: number | undefined }) => {
|
|
1480
|
+
const { secondsLeft } = params;
|
|
1481
|
+
Array.from(autoLogoutCountdownTickCallbacks).forEach(tickCallback =>
|
|
1482
|
+
tickCallback({ secondsLeft })
|
|
1483
|
+
);
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
invokeAllCallbacks({ secondsLeft });
|
|
1487
|
+
|
|
1488
|
+
if (secondsLeft === 0) {
|
|
1489
|
+
cancel_if_offline: {
|
|
1490
|
+
const { isOnline, prOnline } = getIsOnline();
|
|
1491
|
+
|
|
1492
|
+
if (isOnline) {
|
|
1493
|
+
break cancel_if_offline;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const didCameBackOnline = await Promise.race([
|
|
1497
|
+
new Promise<false>(resolve => setTimeout(() => resolve(false), 10_000)),
|
|
1498
|
+
prOnline.then(() => true)
|
|
1499
|
+
]);
|
|
1500
|
+
|
|
1501
|
+
if (didCameBackOnline) {
|
|
1502
|
+
break cancel_if_offline;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
log?.(
|
|
1506
|
+
[
|
|
1507
|
+
"Normally now we should auto logout.",
|
|
1508
|
+
"However since the browser is currently offline",
|
|
1509
|
+
"we avoid calling logout() now to play nice in case",
|
|
1510
|
+
"this app is a PWA.",
|
|
1511
|
+
"Next getTokens() is called logout will be called"
|
|
1512
|
+
].join(" ")
|
|
1513
|
+
);
|
|
1514
|
+
|
|
1515
|
+
unsubscribeFromIsUserActive();
|
|
1516
|
+
|
|
1517
|
+
invokeAllCallbacks({ secondsLeft: undefined });
|
|
1518
|
+
|
|
1519
|
+
wouldHaveAutoLoggedOutIfBrowserWasOnline = true;
|
|
1520
|
+
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
await oidc_loggedIn.logout(autoLogoutParams);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
let stopCountdown: (() => void) | undefined = undefined;
|
|
1530
|
+
|
|
1531
|
+
const evtIsUserActive = createEvtIsUserActive({
|
|
1532
|
+
configId,
|
|
1533
|
+
sessionId
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
const { unsubscribe: unsubscribeFromIsUserActive } = evtIsUserActive.subscribe(isUserActive => {
|
|
1537
|
+
if (isUserActive) {
|
|
1538
|
+
if (stopCountdown !== undefined) {
|
|
1539
|
+
stopCountdown();
|
|
1540
|
+
stopCountdown = undefined;
|
|
1541
|
+
}
|
|
1542
|
+
} else {
|
|
1543
|
+
assert(stopCountdown === undefined, "902992");
|
|
1544
|
+
|
|
1545
|
+
const currentRefreshTokenTtlInSeconds = getCurrentRefreshTokenTtlInSeconds();
|
|
1546
|
+
|
|
1547
|
+
assert(currentRefreshTokenTtlInSeconds !== undefined, "902992326");
|
|
1548
|
+
|
|
1549
|
+
stopCountdown = startCountdown({
|
|
1550
|
+
countDownFromSeconds: currentRefreshTokenTtlInSeconds
|
|
1551
|
+
}).stopCountdown;
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
{
|
|
1556
|
+
const currentRefreshTokenTtlInSeconds = getCurrentRefreshTokenTtlInSeconds();
|
|
1557
|
+
|
|
1558
|
+
assert(currentRefreshTokenTtlInSeconds !== undefined, "9029923253");
|
|
1559
|
+
|
|
1560
|
+
log?.(
|
|
1561
|
+
[
|
|
1562
|
+
`The user will be automatically logged out after ${toHumanReadableDuration(
|
|
1563
|
+
currentRefreshTokenTtlInSeconds * 1_000
|
|
1564
|
+
)} of inactivity.`,
|
|
1565
|
+
idleSessionLifetimeInSeconds === undefined
|
|
1566
|
+
? undefined
|
|
1567
|
+
: `It was artificially defined by using the idleSessionLifetimeInSeconds param.`
|
|
1568
|
+
]
|
|
1569
|
+
.filter(x => x !== undefined)
|
|
1570
|
+
.join("\n")
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
return oidc_loggedIn;
|
|
1576
|
+
}
|