@lvce-editor/auth-worker 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authWorkerMain.js +631 -338
- package/package.json +1 -1
package/dist/authWorkerMain.js
CHANGED
|
@@ -1113,155 +1113,6 @@ const logoutFromBackend = async backendUrl => {
|
|
|
1113
1113
|
}
|
|
1114
1114
|
};
|
|
1115
1115
|
|
|
1116
|
-
const getBackendRefreshUrl = backendUrl => {
|
|
1117
|
-
return getBackendAuthUrl(backendUrl, '/auth/refresh');
|
|
1118
|
-
};
|
|
1119
|
-
|
|
1120
|
-
const delay = async ms => {
|
|
1121
|
-
await new Promise(resolve => setTimeout(resolve, ms));
|
|
1122
|
-
};
|
|
1123
|
-
|
|
1124
|
-
let nextLoginResponse;
|
|
1125
|
-
let nextRefreshResponse;
|
|
1126
|
-
const setNextLoginResponse = response => {
|
|
1127
|
-
nextLoginResponse = response;
|
|
1128
|
-
};
|
|
1129
|
-
const setNextRefreshResponse = response => {
|
|
1130
|
-
nextRefreshResponse = response;
|
|
1131
|
-
};
|
|
1132
|
-
const clear = () => {
|
|
1133
|
-
nextLoginResponse = undefined;
|
|
1134
|
-
nextRefreshResponse = undefined;
|
|
1135
|
-
};
|
|
1136
|
-
const hasPendingMockLoginResponse = () => {
|
|
1137
|
-
return !!nextLoginResponse;
|
|
1138
|
-
};
|
|
1139
|
-
const hasPendingMockRefreshResponse = () => {
|
|
1140
|
-
return !!nextRefreshResponse;
|
|
1141
|
-
};
|
|
1142
|
-
const consumeNextLoginResponse = async () => {
|
|
1143
|
-
if (!nextLoginResponse) {
|
|
1144
|
-
return undefined;
|
|
1145
|
-
}
|
|
1146
|
-
const response = nextLoginResponse;
|
|
1147
|
-
nextLoginResponse = undefined;
|
|
1148
|
-
if (response.delay > 0) {
|
|
1149
|
-
await delay(response.delay);
|
|
1150
|
-
}
|
|
1151
|
-
if (response.type === 'error') {
|
|
1152
|
-
throw new Error(response.message);
|
|
1153
|
-
}
|
|
1154
|
-
return response.response;
|
|
1155
|
-
};
|
|
1156
|
-
const consumeNextRefreshResponse = async () => {
|
|
1157
|
-
if (!nextRefreshResponse) {
|
|
1158
|
-
return undefined;
|
|
1159
|
-
}
|
|
1160
|
-
const response = nextRefreshResponse;
|
|
1161
|
-
nextRefreshResponse = undefined;
|
|
1162
|
-
if (response.delay > 0) {
|
|
1163
|
-
await new Promise(resolve => setTimeout(resolve, response.delay));
|
|
1164
|
-
}
|
|
1165
|
-
if (response.type === 'error') {
|
|
1166
|
-
throw new Error(response.message);
|
|
1167
|
-
}
|
|
1168
|
-
return response.response;
|
|
1169
|
-
};
|
|
1170
|
-
|
|
1171
|
-
const isObject = value => {
|
|
1172
|
-
return !!value && typeof value === 'object';
|
|
1173
|
-
};
|
|
1174
|
-
|
|
1175
|
-
const isBackendAuthResponse = value => {
|
|
1176
|
-
return isObject(value);
|
|
1177
|
-
};
|
|
1178
|
-
|
|
1179
|
-
const getNumber = (value, fallback = 0) => {
|
|
1180
|
-
return typeof value === 'number' ? value : fallback;
|
|
1181
|
-
};
|
|
1182
|
-
|
|
1183
|
-
const getString = (value, fallback = '') => {
|
|
1184
|
-
return typeof value === 'string' ? value : fallback;
|
|
1185
|
-
};
|
|
1186
|
-
|
|
1187
|
-
const toBackendAuthState = value => {
|
|
1188
|
-
return {
|
|
1189
|
-
authAccessToken: getString(value.accessToken),
|
|
1190
|
-
authErrorMessage: getString(value.error),
|
|
1191
|
-
authRefreshToken: getString(value.refreshToken),
|
|
1192
|
-
userName: getString(value.userName),
|
|
1193
|
-
userState: value.accessToken ? 'loggedIn' : 'loggedOut',
|
|
1194
|
-
userSubscriptionPlan: getString(value.subscriptionPlan),
|
|
1195
|
-
userUsedTokens: getNumber(value.usedTokens)
|
|
1196
|
-
};
|
|
1197
|
-
};
|
|
1198
|
-
|
|
1199
|
-
const parseBackendAuthResponse = value => {
|
|
1200
|
-
if (!isBackendAuthResponse(value)) {
|
|
1201
|
-
return getLoggedOutBackendAuthState('Backend returned an invalid authentication response.');
|
|
1202
|
-
}
|
|
1203
|
-
return toBackendAuthState(value);
|
|
1204
|
-
};
|
|
1205
|
-
|
|
1206
|
-
const syncBackendAuth = async backendUrl => {
|
|
1207
|
-
if (!backendUrl) {
|
|
1208
|
-
return getLoggedOutBackendAuthState('Backend URL is missing.');
|
|
1209
|
-
}
|
|
1210
|
-
try {
|
|
1211
|
-
if (hasPendingMockRefreshResponse()) {
|
|
1212
|
-
const mockResponse = await consumeNextRefreshResponse();
|
|
1213
|
-
return parseBackendAuthResponse(mockResponse);
|
|
1214
|
-
}
|
|
1215
|
-
const response = await fetch(getBackendRefreshUrl(backendUrl), {
|
|
1216
|
-
credentials: 'include',
|
|
1217
|
-
headers: {
|
|
1218
|
-
Accept: 'application/json'
|
|
1219
|
-
},
|
|
1220
|
-
method: 'POST'
|
|
1221
|
-
});
|
|
1222
|
-
if (response.status === 401 || response.status === 403) {
|
|
1223
|
-
return getLoggedOutBackendAuthState();
|
|
1224
|
-
}
|
|
1225
|
-
let payload = undefined;
|
|
1226
|
-
try {
|
|
1227
|
-
payload = await response.json();
|
|
1228
|
-
} catch {
|
|
1229
|
-
payload = undefined;
|
|
1230
|
-
}
|
|
1231
|
-
if (!response.ok) {
|
|
1232
|
-
const parsed = parseBackendAuthResponse(payload);
|
|
1233
|
-
return getLoggedOutBackendAuthState(parsed.authErrorMessage || 'Backend authentication failed.');
|
|
1234
|
-
}
|
|
1235
|
-
const parsed = parseBackendAuthResponse(payload);
|
|
1236
|
-
if (parsed.authErrorMessage) {
|
|
1237
|
-
return getLoggedOutBackendAuthState(parsed.authErrorMessage);
|
|
1238
|
-
}
|
|
1239
|
-
if (!parsed.authAccessToken) {
|
|
1240
|
-
return getLoggedOutBackendAuthState();
|
|
1241
|
-
}
|
|
1242
|
-
return parsed;
|
|
1243
|
-
} catch (error) {
|
|
1244
|
-
const authErrorMessage = error instanceof Error && error.message ? error.message : 'Backend authentication failed.';
|
|
1245
|
-
return getLoggedOutBackendAuthState(authErrorMessage);
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1249
|
-
const waitForBackendLogin = async (backendUrl, timeoutMs = 30_000, pollIntervalMs = 1000) => {
|
|
1250
|
-
const deadline = Date.now() + timeoutMs;
|
|
1251
|
-
let lastErrorMessage = '';
|
|
1252
|
-
while (Date.now() < deadline) {
|
|
1253
|
-
const authState = await syncBackendAuth(backendUrl);
|
|
1254
|
-
if (authState.userState === 'loggedIn') {
|
|
1255
|
-
return authState;
|
|
1256
|
-
}
|
|
1257
|
-
if (authState.authErrorMessage) {
|
|
1258
|
-
lastErrorMessage = authState.authErrorMessage;
|
|
1259
|
-
}
|
|
1260
|
-
await delay(pollIntervalMs);
|
|
1261
|
-
}
|
|
1262
|
-
return getLoggedOutBackendAuthState(lastErrorMessage);
|
|
1263
|
-
};
|
|
1264
|
-
|
|
1265
1116
|
let USER_AGENT;
|
|
1266
1117
|
if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
|
|
1267
1118
|
const NAME = 'oauth4webapi';
|
|
@@ -2055,14 +1906,34 @@ async function getResponseJsonBody(response, check = assertApplicationJson) {
|
|
|
2055
1906
|
}
|
|
2056
1907
|
const _expectedIssuer = Symbol();
|
|
2057
1908
|
|
|
2058
|
-
|
|
1909
|
+
const getBackendOidcTokenUrl = backendUrl => {
|
|
1910
|
+
return getBackendAuthUrl(backendUrl, '/oidc/token');
|
|
1911
|
+
};
|
|
2059
1912
|
|
|
2060
|
-
const
|
|
2061
|
-
const codeVerifier = generateRandomCodeVerifier();
|
|
2062
|
-
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
|
1913
|
+
const getAuthorizationServer$1 = backendUrl => {
|
|
2063
1914
|
return {
|
|
2064
|
-
|
|
2065
|
-
|
|
1915
|
+
issuer: getBackendAuthUrl(backendUrl, '/oidc'),
|
|
1916
|
+
jwks_uri: getBackendAuthUrl(backendUrl, '/oidc/jwks'),
|
|
1917
|
+
token_endpoint: getBackendOidcTokenUrl(backendUrl)
|
|
1918
|
+
};
|
|
1919
|
+
};
|
|
1920
|
+
const getClient$1 = clientId => {
|
|
1921
|
+
return {
|
|
1922
|
+
client_id: clientId
|
|
1923
|
+
};
|
|
1924
|
+
};
|
|
1925
|
+
const exchangeAuthorizationCode = async (backendUrl, clientId, code, redirectUri, codeVerifier, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
|
|
1926
|
+
const authorizationServer = getAuthorizationServer$1(backendUrl);
|
|
1927
|
+
const client = getClient$1(clientId);
|
|
1928
|
+
const response = await requestTokenEndpoint(authorizationServer, client, None(), 'authorization_code', new URLSearchParams({
|
|
1929
|
+
code,
|
|
1930
|
+
code_verifier: codeVerifier,
|
|
1931
|
+
redirect_uri: redirectUri
|
|
1932
|
+
}));
|
|
1933
|
+
const tokenResponse = await processTokenEndpointResponse(authorizationServer, client, response);
|
|
1934
|
+
return {
|
|
1935
|
+
accessToken: tokenResponse.access_token,
|
|
1936
|
+
refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : ''
|
|
2066
1937
|
};
|
|
2067
1938
|
};
|
|
2068
1939
|
|
|
@@ -2078,71 +1949,533 @@ const getCurrentHref = async () => {
|
|
|
2078
1949
|
return globalThis.location.href;
|
|
2079
1950
|
};
|
|
2080
1951
|
|
|
2081
|
-
const
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
font-family: Inter, sans-serif;
|
|
2109
|
-
background: var(--background);
|
|
2110
|
-
color: var(--text);
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
body {
|
|
2114
|
-
display: flex;
|
|
2115
|
-
align-items: center;
|
|
2116
|
-
justify-content: center;
|
|
2117
|
-
padding: 24px;
|
|
2118
|
-
}
|
|
1952
|
+
const getPayload$1 = async response => {
|
|
1953
|
+
try {
|
|
1954
|
+
return await response.json();
|
|
1955
|
+
} catch {
|
|
1956
|
+
return undefined;
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
const getOidcUserName = async (backendUrl, accessToken, fetchFn = fetch) => {
|
|
1960
|
+
if (!backendUrl || !accessToken) {
|
|
1961
|
+
return '';
|
|
1962
|
+
}
|
|
1963
|
+
const response = await fetchFn(new URL('/account/me', backendUrl), {
|
|
1964
|
+
headers: {
|
|
1965
|
+
Accept: 'application/json',
|
|
1966
|
+
Authorization: `Bearer ${accessToken}`
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
if (!response.ok) {
|
|
1970
|
+
return '';
|
|
1971
|
+
}
|
|
1972
|
+
const payload = await getPayload$1(response);
|
|
1973
|
+
if (!payload || typeof payload !== 'object') {
|
|
1974
|
+
return '';
|
|
1975
|
+
}
|
|
1976
|
+
const displayName = Reflect.get(payload, 'displayName');
|
|
1977
|
+
return typeof displayName === 'string' ? displayName : '';
|
|
1978
|
+
};
|
|
2119
1979
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
border-radius: 20px;
|
|
2125
|
-
background: var(--panel);
|
|
2126
|
-
box-shadow: var(--shadow);
|
|
2127
|
-
text-align: center;
|
|
2128
|
-
backdrop-filter: blur(12px);
|
|
2129
|
-
}
|
|
1980
|
+
const databaseName = 'auth-worker';
|
|
1981
|
+
const objectStoreName = 'auth';
|
|
1982
|
+
const memoryStorage = new Map();
|
|
1983
|
+
let databasePromise;
|
|
2130
1984
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
1985
|
+
// IndexedDB request objects are mutable browser primitives and cannot satisfy the readonly rule structurally.
|
|
1986
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1987
|
+
const requestToPromise = request => {
|
|
1988
|
+
return new Promise((resolve, reject) => {
|
|
1989
|
+
request.addEventListener('success', () => {
|
|
1990
|
+
resolve(request.result);
|
|
1991
|
+
});
|
|
1992
|
+
request.addEventListener('error', () => {
|
|
1993
|
+
reject(request.error ?? new Error('Persistent storage request failed.'));
|
|
1994
|
+
});
|
|
1995
|
+
});
|
|
1996
|
+
};
|
|
2142
1997
|
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
1998
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1999
|
+
const transactionToPromise = transaction => {
|
|
2000
|
+
return new Promise((resolve, reject) => {
|
|
2001
|
+
transaction.addEventListener('complete', () => {
|
|
2002
|
+
resolve();
|
|
2003
|
+
});
|
|
2004
|
+
transaction.addEventListener('abort', () => {
|
|
2005
|
+
reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
|
|
2006
|
+
});
|
|
2007
|
+
transaction.addEventListener('error', () => {
|
|
2008
|
+
reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
|
|
2009
|
+
});
|
|
2010
|
+
});
|
|
2011
|
+
};
|
|
2012
|
+
const getDatabase = async () => {
|
|
2013
|
+
if (typeof indexedDB === 'undefined') {
|
|
2014
|
+
return undefined;
|
|
2015
|
+
}
|
|
2016
|
+
if (!databasePromise) {
|
|
2017
|
+
databasePromise = new Promise((resolve, reject) => {
|
|
2018
|
+
const request = indexedDB.open(databaseName, 1);
|
|
2019
|
+
request.addEventListener('upgradeneeded', () => {
|
|
2020
|
+
const database = request.result;
|
|
2021
|
+
if (!database.objectStoreNames.contains(objectStoreName)) {
|
|
2022
|
+
database.createObjectStore(objectStoreName);
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
request.addEventListener('success', () => {
|
|
2026
|
+
resolve(request.result);
|
|
2027
|
+
});
|
|
2028
|
+
request.addEventListener('error', () => {
|
|
2029
|
+
reject(request.error ?? new Error('Failed to open persistent auth storage.'));
|
|
2030
|
+
});
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
return databasePromise;
|
|
2034
|
+
};
|
|
2035
|
+
const getPersistentAuthValue = async key => {
|
|
2036
|
+
const database = await getDatabase();
|
|
2037
|
+
if (!database) {
|
|
2038
|
+
return memoryStorage.get(key) ?? '';
|
|
2039
|
+
}
|
|
2040
|
+
const transaction = database.transaction(objectStoreName, 'readonly');
|
|
2041
|
+
const objectStore = transaction.objectStore(objectStoreName);
|
|
2042
|
+
const value = await requestToPromise(objectStore.get(key));
|
|
2043
|
+
return typeof value === 'string' ? value : '';
|
|
2044
|
+
};
|
|
2045
|
+
const setPersistentAuthValue = async (key, value) => {
|
|
2046
|
+
memoryStorage.set(key, value);
|
|
2047
|
+
const database = await getDatabase();
|
|
2048
|
+
if (!database) {
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
const transaction = database.transaction(objectStoreName, 'readwrite');
|
|
2052
|
+
const objectStore = transaction.objectStore(objectStoreName);
|
|
2053
|
+
objectStore.put(value, key);
|
|
2054
|
+
await transactionToPromise(transaction);
|
|
2055
|
+
};
|
|
2056
|
+
const clearPersistentAuthValue = async key => {
|
|
2057
|
+
memoryStorage.delete(key);
|
|
2058
|
+
const database = await getDatabase();
|
|
2059
|
+
if (!database) {
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
const transaction = database.transaction(objectStoreName, 'readwrite');
|
|
2063
|
+
const objectStore = transaction.objectStore(objectStoreName);
|
|
2064
|
+
objectStore.delete(key);
|
|
2065
|
+
await transactionToPromise(transaction);
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
const callbackUrlKey = 'oidcCallbackUrl';
|
|
2069
|
+
const clientIdKey = 'oidcClientId';
|
|
2070
|
+
const pendingClientIdKey = 'pendingOidcClientId';
|
|
2071
|
+
const pendingCodeVerifierKey = 'pendingOidcCodeVerifier';
|
|
2072
|
+
const pendingRedirectUriKey = 'pendingOidcRedirectUri';
|
|
2073
|
+
const pendingStateKey = 'pendingOidcState';
|
|
2074
|
+
const clearOidcCallbackUrl = async () => {
|
|
2075
|
+
await clearPersistentAuthValue(callbackUrlKey);
|
|
2076
|
+
};
|
|
2077
|
+
const clearStoredOidcClientId = async () => {
|
|
2078
|
+
await clearPersistentAuthValue(clientIdKey);
|
|
2079
|
+
};
|
|
2080
|
+
const clearPendingOidcAuthState = async () => {
|
|
2081
|
+
await Promise.all([clearPersistentAuthValue(pendingClientIdKey), clearPersistentAuthValue(pendingCodeVerifierKey), clearPersistentAuthValue(pendingRedirectUriKey), clearPersistentAuthValue(pendingStateKey)]);
|
|
2082
|
+
};
|
|
2083
|
+
const getOidcCallbackUrl = async () => {
|
|
2084
|
+
return getPersistentAuthValue(callbackUrlKey);
|
|
2085
|
+
};
|
|
2086
|
+
const getStoredOidcClientId = async () => {
|
|
2087
|
+
return getPersistentAuthValue(clientIdKey);
|
|
2088
|
+
};
|
|
2089
|
+
const loadPendingOidcAuthState = async () => {
|
|
2090
|
+
const [clientId, codeVerifier, redirectUri, state] = await Promise.all([getPersistentAuthValue(pendingClientIdKey), getPersistentAuthValue(pendingCodeVerifierKey), getPersistentAuthValue(pendingRedirectUriKey), getPersistentAuthValue(pendingStateKey)]);
|
|
2091
|
+
if (!clientId || !codeVerifier || !redirectUri || !state) {
|
|
2092
|
+
return undefined;
|
|
2093
|
+
}
|
|
2094
|
+
return {
|
|
2095
|
+
clientId,
|
|
2096
|
+
codeVerifier,
|
|
2097
|
+
redirectUri,
|
|
2098
|
+
state
|
|
2099
|
+
};
|
|
2100
|
+
};
|
|
2101
|
+
const saveOidcClientId = async clientId => {
|
|
2102
|
+
await setPersistentAuthValue(clientIdKey, clientId);
|
|
2103
|
+
};
|
|
2104
|
+
const savePendingOidcAuthState = async value => {
|
|
2105
|
+
await Promise.all([setPersistentAuthValue(pendingClientIdKey, value.clientId), setPersistentAuthValue(pendingCodeVerifierKey, value.codeVerifier), setPersistentAuthValue(pendingRedirectUriKey, value.redirectUri), setPersistentAuthValue(pendingStateKey, value.state)]);
|
|
2106
|
+
};
|
|
2107
|
+
|
|
2108
|
+
const normalizeRedirectUri = value => {
|
|
2109
|
+
const url = new URL(value);
|
|
2110
|
+
url.hash = '';
|
|
2111
|
+
url.search = '';
|
|
2112
|
+
return url.toString();
|
|
2113
|
+
};
|
|
2114
|
+
const getCallbackHref = async () => {
|
|
2115
|
+
const storedCallbackUrl = await getOidcCallbackUrl();
|
|
2116
|
+
if (storedCallbackUrl) {
|
|
2117
|
+
await clearOidcCallbackUrl();
|
|
2118
|
+
return storedCallbackUrl;
|
|
2119
|
+
}
|
|
2120
|
+
return getCurrentHref();
|
|
2121
|
+
};
|
|
2122
|
+
const completeBrowserOidcLogin = async backendUrl => {
|
|
2123
|
+
const href = await getCallbackHref();
|
|
2124
|
+
if (!href) {
|
|
2125
|
+
return undefined;
|
|
2126
|
+
}
|
|
2127
|
+
let url;
|
|
2128
|
+
try {
|
|
2129
|
+
url = new URL(href);
|
|
2130
|
+
} catch {
|
|
2131
|
+
return undefined;
|
|
2132
|
+
}
|
|
2133
|
+
const code = url.searchParams.get('code') || '';
|
|
2134
|
+
const error = url.searchParams.get('error') || '';
|
|
2135
|
+
const errorDescription = url.searchParams.get('error_description') || '';
|
|
2136
|
+
if (!code && !error) {
|
|
2137
|
+
return undefined;
|
|
2138
|
+
}
|
|
2139
|
+
const pendingAuthState = await loadPendingOidcAuthState();
|
|
2140
|
+
if (!pendingAuthState) {
|
|
2141
|
+
await clearPendingOidcAuthState();
|
|
2142
|
+
return getLoggedOutBackendAuthState('Authentication state is missing.');
|
|
2143
|
+
}
|
|
2144
|
+
if (normalizeRedirectUri(href) !== normalizeRedirectUri(pendingAuthState.redirectUri)) {
|
|
2145
|
+
await clearPendingOidcAuthState();
|
|
2146
|
+
return getLoggedOutBackendAuthState('Authentication returned to an unexpected redirect URI.');
|
|
2147
|
+
}
|
|
2148
|
+
if (error) {
|
|
2149
|
+
await clearPendingOidcAuthState();
|
|
2150
|
+
return getLoggedOutBackendAuthState(errorDescription || error);
|
|
2151
|
+
}
|
|
2152
|
+
const returnedState = url.searchParams.get('state') || '';
|
|
2153
|
+
if (!returnedState || returnedState !== pendingAuthState.state) {
|
|
2154
|
+
await clearPendingOidcAuthState();
|
|
2155
|
+
return getLoggedOutBackendAuthState('Authentication state mismatch.');
|
|
2156
|
+
}
|
|
2157
|
+
const exchanged = await exchangeAuthorizationCode(backendUrl, pendingAuthState.clientId, code, pendingAuthState.redirectUri, pendingAuthState.codeVerifier);
|
|
2158
|
+
const userName = await getOidcUserName(backendUrl, exchanged.accessToken);
|
|
2159
|
+
await clearPendingOidcAuthState();
|
|
2160
|
+
return {
|
|
2161
|
+
authAccessToken: exchanged.accessToken,
|
|
2162
|
+
authClientId: pendingAuthState.clientId,
|
|
2163
|
+
authErrorMessage: '',
|
|
2164
|
+
authRefreshToken: exchanged.refreshToken,
|
|
2165
|
+
userName,
|
|
2166
|
+
userState: exchanged.accessToken ? 'loggedIn' : 'loggedOut'
|
|
2167
|
+
};
|
|
2168
|
+
};
|
|
2169
|
+
|
|
2170
|
+
const getBackendRefreshUrl = backendUrl => {
|
|
2171
|
+
return getBackendAuthUrl(backendUrl, '/auth/refresh');
|
|
2172
|
+
};
|
|
2173
|
+
|
|
2174
|
+
const delay = async ms => {
|
|
2175
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
2176
|
+
};
|
|
2177
|
+
|
|
2178
|
+
let nextLoginResponse;
|
|
2179
|
+
let nextRefreshResponse;
|
|
2180
|
+
const setNextLoginResponse = response => {
|
|
2181
|
+
nextLoginResponse = response;
|
|
2182
|
+
};
|
|
2183
|
+
const setNextRefreshResponse = response => {
|
|
2184
|
+
nextRefreshResponse = response;
|
|
2185
|
+
};
|
|
2186
|
+
const clear = () => {
|
|
2187
|
+
nextLoginResponse = undefined;
|
|
2188
|
+
nextRefreshResponse = undefined;
|
|
2189
|
+
};
|
|
2190
|
+
const hasPendingMockLoginResponse = () => {
|
|
2191
|
+
return !!nextLoginResponse;
|
|
2192
|
+
};
|
|
2193
|
+
const hasPendingMockRefreshResponse = () => {
|
|
2194
|
+
return !!nextRefreshResponse;
|
|
2195
|
+
};
|
|
2196
|
+
const consumeNextLoginResponse = async () => {
|
|
2197
|
+
if (!nextLoginResponse) {
|
|
2198
|
+
return undefined;
|
|
2199
|
+
}
|
|
2200
|
+
const response = nextLoginResponse;
|
|
2201
|
+
nextLoginResponse = undefined;
|
|
2202
|
+
if (response.delay > 0) {
|
|
2203
|
+
await delay(response.delay);
|
|
2204
|
+
}
|
|
2205
|
+
if (response.type === 'error') {
|
|
2206
|
+
throw new Error(response.message);
|
|
2207
|
+
}
|
|
2208
|
+
return response.response;
|
|
2209
|
+
};
|
|
2210
|
+
const consumeNextRefreshResponse = async () => {
|
|
2211
|
+
if (!nextRefreshResponse) {
|
|
2212
|
+
return undefined;
|
|
2213
|
+
}
|
|
2214
|
+
const response = nextRefreshResponse;
|
|
2215
|
+
nextRefreshResponse = undefined;
|
|
2216
|
+
if (response.delay > 0) {
|
|
2217
|
+
await new Promise(resolve => setTimeout(resolve, response.delay));
|
|
2218
|
+
}
|
|
2219
|
+
if (response.type === 'error') {
|
|
2220
|
+
throw new Error(response.message);
|
|
2221
|
+
}
|
|
2222
|
+
return response.response;
|
|
2223
|
+
};
|
|
2224
|
+
|
|
2225
|
+
const isObject = value => {
|
|
2226
|
+
return !!value && typeof value === 'object';
|
|
2227
|
+
};
|
|
2228
|
+
|
|
2229
|
+
const isBackendAuthResponse = value => {
|
|
2230
|
+
return isObject(value);
|
|
2231
|
+
};
|
|
2232
|
+
|
|
2233
|
+
const getNumber = (value, fallback = 0) => {
|
|
2234
|
+
return typeof value === 'number' ? value : fallback;
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
const getString = (value, fallback = '') => {
|
|
2238
|
+
return typeof value === 'string' ? value : fallback;
|
|
2239
|
+
};
|
|
2240
|
+
|
|
2241
|
+
const getUserName = value => {
|
|
2242
|
+
if (typeof value.userName === 'string') {
|
|
2243
|
+
return value.userName;
|
|
2244
|
+
}
|
|
2245
|
+
return getString(value.user?.displayName);
|
|
2246
|
+
};
|
|
2247
|
+
const toBackendAuthState = value => {
|
|
2248
|
+
return {
|
|
2249
|
+
authAccessToken: getString(value.accessToken),
|
|
2250
|
+
authErrorMessage: getString(value.error),
|
|
2251
|
+
authRefreshToken: getString(value.refreshToken),
|
|
2252
|
+
userName: getUserName(value),
|
|
2253
|
+
userState: value.accessToken ? 'loggedIn' : 'loggedOut',
|
|
2254
|
+
userSubscriptionPlan: getString(value.subscriptionPlan),
|
|
2255
|
+
userSubscriptionStatus: getString(value.subscriptionStatus),
|
|
2256
|
+
userUsedTokens: getNumber(value.usedTokens)
|
|
2257
|
+
};
|
|
2258
|
+
};
|
|
2259
|
+
|
|
2260
|
+
const parseBackendAuthResponse = value => {
|
|
2261
|
+
if (!isBackendAuthResponse(value)) {
|
|
2262
|
+
return getLoggedOutBackendAuthState('Backend returned an invalid authentication response.');
|
|
2263
|
+
}
|
|
2264
|
+
return toBackendAuthState(value);
|
|
2265
|
+
};
|
|
2266
|
+
|
|
2267
|
+
const persistLoginResult = async loginResult => {
|
|
2268
|
+
if (loginResult.userState !== 'loggedIn') {
|
|
2269
|
+
return loginResult;
|
|
2270
|
+
}
|
|
2271
|
+
await Promise.all([setPersistentAuthValue('accessToken', loginResult.authAccessToken ?? ''), setPersistentAuthValue('refreshToken', loginResult.authRefreshToken ?? ''), loginResult.authClientId ? saveOidcClientId(loginResult.authClientId) : Promise.resolve()]);
|
|
2272
|
+
return loginResult;
|
|
2273
|
+
};
|
|
2274
|
+
|
|
2275
|
+
const getAuthorizationServer = backendUrl => {
|
|
2276
|
+
return {
|
|
2277
|
+
issuer: getBackendAuthUrl(backendUrl, '/oidc'),
|
|
2278
|
+
jwks_uri: getBackendAuthUrl(backendUrl, '/oidc/jwks'),
|
|
2279
|
+
token_endpoint: getBackendOidcTokenUrl(backendUrl)
|
|
2280
|
+
};
|
|
2281
|
+
};
|
|
2282
|
+
const getClient = clientId => {
|
|
2283
|
+
return {
|
|
2284
|
+
client_id: clientId
|
|
2285
|
+
};
|
|
2286
|
+
};
|
|
2287
|
+
const refreshOidcTokens = async (backendUrl, clientId, refreshToken, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
|
|
2288
|
+
const authorizationServer = getAuthorizationServer(backendUrl);
|
|
2289
|
+
const client = getClient(clientId);
|
|
2290
|
+
const response = await requestTokenEndpoint(authorizationServer, client, None(), 'refresh_token', new URLSearchParams({
|
|
2291
|
+
refresh_token: refreshToken
|
|
2292
|
+
}));
|
|
2293
|
+
const tokenResponse = await processTokenEndpointResponse(authorizationServer, client, response);
|
|
2294
|
+
return {
|
|
2295
|
+
accessToken: tokenResponse.access_token,
|
|
2296
|
+
refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : refreshToken
|
|
2297
|
+
};
|
|
2298
|
+
};
|
|
2299
|
+
|
|
2300
|
+
const clearStoredOidcAuth = async () => {
|
|
2301
|
+
await Promise.all([clearPersistentAuthValue('accessToken'), clearPersistentAuthValue('refreshToken'), clearStoredOidcClientId()]);
|
|
2302
|
+
};
|
|
2303
|
+
const toLoginResult = (accessToken, refreshToken, clientId, userName) => {
|
|
2304
|
+
return {
|
|
2305
|
+
authAccessToken: accessToken,
|
|
2306
|
+
authClientId: clientId,
|
|
2307
|
+
authErrorMessage: '',
|
|
2308
|
+
authRefreshToken: refreshToken,
|
|
2309
|
+
userName,
|
|
2310
|
+
userState: accessToken ? 'loggedIn' : 'loggedOut'
|
|
2311
|
+
};
|
|
2312
|
+
};
|
|
2313
|
+
const restoreOidcAuth = async backendUrl => {
|
|
2314
|
+
const [accessToken, refreshToken, clientId] = await Promise.all([getPersistentAuthValue('accessToken'), getPersistentAuthValue('refreshToken'), getStoredOidcClientId()]);
|
|
2315
|
+
if (!refreshToken || !clientId) {
|
|
2316
|
+
return undefined;
|
|
2317
|
+
}
|
|
2318
|
+
try {
|
|
2319
|
+
const refreshedTokens = await refreshOidcTokens(backendUrl, clientId, refreshToken);
|
|
2320
|
+
const userName = await getOidcUserName(backendUrl, refreshedTokens.accessToken);
|
|
2321
|
+
return toLoginResult(refreshedTokens.accessToken, refreshedTokens.refreshToken, clientId, userName);
|
|
2322
|
+
} catch {
|
|
2323
|
+
if (accessToken) {
|
|
2324
|
+
const userName = await getOidcUserName(backendUrl, accessToken);
|
|
2325
|
+
if (userName) {
|
|
2326
|
+
return toLoginResult(accessToken, refreshToken, clientId, userName);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
await clearStoredOidcAuth();
|
|
2330
|
+
return undefined;
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
|
|
2334
|
+
const getPayload = async response => {
|
|
2335
|
+
try {
|
|
2336
|
+
return await response.json();
|
|
2337
|
+
} catch {
|
|
2338
|
+
return undefined;
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
const syncBackendAuth = async backendUrl => {
|
|
2342
|
+
if (!backendUrl) {
|
|
2343
|
+
return getLoggedOutBackendAuthState('Backend URL is missing.');
|
|
2344
|
+
}
|
|
2345
|
+
try {
|
|
2346
|
+
const completedBrowserLogin = await completeBrowserOidcLogin(backendUrl);
|
|
2347
|
+
if (completedBrowserLogin) {
|
|
2348
|
+
return persistLoginResult(completedBrowserLogin);
|
|
2349
|
+
}
|
|
2350
|
+
const restoredOidcAuth = await restoreOidcAuth(backendUrl);
|
|
2351
|
+
if (restoredOidcAuth) {
|
|
2352
|
+
return persistLoginResult(restoredOidcAuth);
|
|
2353
|
+
}
|
|
2354
|
+
if (hasPendingMockRefreshResponse()) {
|
|
2355
|
+
const mockResponse = await consumeNextRefreshResponse();
|
|
2356
|
+
return parseBackendAuthResponse(mockResponse);
|
|
2357
|
+
}
|
|
2358
|
+
const response = await fetch(getBackendRefreshUrl(backendUrl), {
|
|
2359
|
+
credentials: 'include',
|
|
2360
|
+
headers: {
|
|
2361
|
+
Accept: 'application/json'
|
|
2362
|
+
},
|
|
2363
|
+
method: 'POST'
|
|
2364
|
+
});
|
|
2365
|
+
if (response.status === 401 || response.status === 403) {
|
|
2366
|
+
return getLoggedOutBackendAuthState();
|
|
2367
|
+
}
|
|
2368
|
+
const payload = await getPayload(response);
|
|
2369
|
+
if (!response.ok) {
|
|
2370
|
+
const parsed = parseBackendAuthResponse(payload);
|
|
2371
|
+
return getLoggedOutBackendAuthState(parsed.authErrorMessage || 'Backend authentication failed.');
|
|
2372
|
+
}
|
|
2373
|
+
const parsed = parseBackendAuthResponse(payload);
|
|
2374
|
+
if (parsed.authErrorMessage) {
|
|
2375
|
+
return getLoggedOutBackendAuthState(parsed.authErrorMessage);
|
|
2376
|
+
}
|
|
2377
|
+
if (!parsed.authAccessToken) {
|
|
2378
|
+
return getLoggedOutBackendAuthState();
|
|
2379
|
+
}
|
|
2380
|
+
return parsed;
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
const authErrorMessage = error instanceof Error && error.message ? error.message : 'Backend authentication failed.';
|
|
2383
|
+
return getLoggedOutBackendAuthState(authErrorMessage);
|
|
2384
|
+
}
|
|
2385
|
+
};
|
|
2386
|
+
|
|
2387
|
+
const waitForBackendLogin = async (backendUrl, timeoutMs = 30_000, pollIntervalMs = 1000) => {
|
|
2388
|
+
const deadline = Date.now() + timeoutMs;
|
|
2389
|
+
let lastErrorMessage = '';
|
|
2390
|
+
while (Date.now() < deadline) {
|
|
2391
|
+
const authState = await syncBackendAuth(backendUrl);
|
|
2392
|
+
if (authState.userState === 'loggedIn') {
|
|
2393
|
+
return authState;
|
|
2394
|
+
}
|
|
2395
|
+
if (authState.authErrorMessage) {
|
|
2396
|
+
lastErrorMessage = authState.authErrorMessage;
|
|
2397
|
+
}
|
|
2398
|
+
await delay(pollIntervalMs);
|
|
2399
|
+
}
|
|
2400
|
+
return getLoggedOutBackendAuthState(lastErrorMessage);
|
|
2401
|
+
};
|
|
2402
|
+
|
|
2403
|
+
// cspell:ignore pkce
|
|
2404
|
+
|
|
2405
|
+
const createPkceValues = async () => {
|
|
2406
|
+
const codeVerifier = generateRandomCodeVerifier();
|
|
2407
|
+
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
|
2408
|
+
return {
|
|
2409
|
+
codeChallenge,
|
|
2410
|
+
codeVerifier
|
|
2411
|
+
};
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2414
|
+
const successHtml = `<!doctype html>
|
|
2415
|
+
<html lang="en">
|
|
2416
|
+
<head>
|
|
2417
|
+
<meta charset="utf-8">
|
|
2418
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2419
|
+
<title>Authentication Complete</title>
|
|
2420
|
+
<style>
|
|
2421
|
+
:root {
|
|
2422
|
+
color-scheme: light;
|
|
2423
|
+
--background: linear-gradient(180deg, #f4f7fb 0%, #e9eef8 100%);
|
|
2424
|
+
--panel: rgba(255, 255, 255, 0.92);
|
|
2425
|
+
--panel-border: rgba(33, 52, 88, 0.08);
|
|
2426
|
+
--text: #132238;
|
|
2427
|
+
--muted: #5f6f86;
|
|
2428
|
+
--accent: #1f7a5a;
|
|
2429
|
+
--accent-soft: #e7f6ef;
|
|
2430
|
+
--shadow: 0 24px 60px rgba(44, 65, 98, 0.16);
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
* {
|
|
2434
|
+
box-sizing: border-box;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
html,
|
|
2438
|
+
body {
|
|
2439
|
+
margin: 0;
|
|
2440
|
+
min-height: 100%;
|
|
2441
|
+
font-family: Inter, sans-serif;
|
|
2442
|
+
background: var(--background);
|
|
2443
|
+
color: var(--text);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
body {
|
|
2447
|
+
display: flex;
|
|
2448
|
+
align-items: center;
|
|
2449
|
+
justify-content: center;
|
|
2450
|
+
padding: 24px;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
.card {
|
|
2454
|
+
width: min(100%, 460px);
|
|
2455
|
+
padding: 32px 28px;
|
|
2456
|
+
border: 1px solid var(--panel-border);
|
|
2457
|
+
border-radius: 20px;
|
|
2458
|
+
background: var(--panel);
|
|
2459
|
+
box-shadow: var(--shadow);
|
|
2460
|
+
text-align: center;
|
|
2461
|
+
backdrop-filter: blur(12px);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
.badge {
|
|
2465
|
+
display: inline-flex;
|
|
2466
|
+
align-items: center;
|
|
2467
|
+
justify-content: center;
|
|
2468
|
+
width: 64px;
|
|
2469
|
+
height: 64px;
|
|
2470
|
+
margin-bottom: 20px;
|
|
2471
|
+
border-radius: 999px;
|
|
2472
|
+
background: var(--accent-soft);
|
|
2473
|
+
color: var(--accent);
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
h1 {
|
|
2477
|
+
margin: 0;
|
|
2478
|
+
font-size: 28px;
|
|
2146
2479
|
line-height: 1.15;
|
|
2147
2480
|
letter-spacing: -0.03em;
|
|
2148
2481
|
}
|
|
@@ -2329,6 +2662,18 @@ const getElectronRedirectUri = async uid => {
|
|
|
2329
2662
|
return `http://localhost:${localOauthServerPort}/callback`;
|
|
2330
2663
|
};
|
|
2331
2664
|
|
|
2665
|
+
const getWebRedirectUri = async () => {
|
|
2666
|
+
const href = await getCurrentHref();
|
|
2667
|
+
if (!href) {
|
|
2668
|
+
return '';
|
|
2669
|
+
}
|
|
2670
|
+
try {
|
|
2671
|
+
const url = new URL(href);
|
|
2672
|
+
return `${url.origin}/auth/callback`;
|
|
2673
|
+
} catch {
|
|
2674
|
+
return '';
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2332
2677
|
const getEffectiveRedirectUri = async (platform, uid, redirectUri) => {
|
|
2333
2678
|
if (redirectUri) {
|
|
2334
2679
|
return redirectUri;
|
|
@@ -2336,11 +2681,15 @@ const getEffectiveRedirectUri = async (platform, uid, redirectUri) => {
|
|
|
2336
2681
|
if (platform === Electron) {
|
|
2337
2682
|
return getElectronRedirectUri(uid);
|
|
2338
2683
|
}
|
|
2339
|
-
return
|
|
2684
|
+
return getWebRedirectUri();
|
|
2340
2685
|
};
|
|
2341
2686
|
|
|
2342
|
-
const
|
|
2687
|
+
const nativeOidcClientId = 'lvce-editor-native';
|
|
2688
|
+
const webOidcClientId = 'lvce-editor-web';
|
|
2343
2689
|
const oidcScope = 'openid offline_access profile email';
|
|
2690
|
+
const getOidcClientId = platform => {
|
|
2691
|
+
return platform === Electron ? nativeOidcClientId : webOidcClientId;
|
|
2692
|
+
};
|
|
2344
2693
|
|
|
2345
2694
|
const getBackendLoginRequest = async (backendUrl, platform = 0, uid = 0, redirectUri = '', createPkceValuesFn = createPkceValues, createRandomUuid = () => globalThis.crypto.randomUUID()) => {
|
|
2346
2695
|
const effectiveRedirectUri = await getEffectiveRedirectUri(platform, uid, redirectUri);
|
|
@@ -2348,24 +2697,28 @@ const getBackendLoginRequest = async (backendUrl, platform = 0, uid = 0, redirec
|
|
|
2348
2697
|
codeChallenge,
|
|
2349
2698
|
codeVerifier
|
|
2350
2699
|
} = await createPkceValuesFn();
|
|
2700
|
+
const clientId = getOidcClientId(platform);
|
|
2351
2701
|
const nonce = createRandomUuid();
|
|
2702
|
+
const state = createRandomUuid();
|
|
2352
2703
|
const loginUrl = new URL(getBackendAuthUrl(backendUrl, '/oidc/auth'));
|
|
2353
|
-
loginUrl.searchParams.set('client_id',
|
|
2704
|
+
loginUrl.searchParams.set('client_id', clientId);
|
|
2354
2705
|
loginUrl.searchParams.set('code_challenge', codeChallenge);
|
|
2355
2706
|
loginUrl.searchParams.set('code_challenge_method', 'S256');
|
|
2356
2707
|
loginUrl.searchParams.set('nonce', nonce);
|
|
2357
2708
|
loginUrl.searchParams.set('prompt', 'consent');
|
|
2358
2709
|
loginUrl.searchParams.set('response_type', 'code');
|
|
2359
2710
|
loginUrl.searchParams.set('scope', oidcScope);
|
|
2360
|
-
loginUrl.searchParams.set('state',
|
|
2711
|
+
loginUrl.searchParams.set('state', state);
|
|
2361
2712
|
if (effectiveRedirectUri) {
|
|
2362
2713
|
loginUrl.searchParams.set('redirect_uri', effectiveRedirectUri);
|
|
2363
2714
|
}
|
|
2364
2715
|
return {
|
|
2716
|
+
clientId,
|
|
2365
2717
|
codeVerifier,
|
|
2366
2718
|
loginUrl: loginUrl.toString(),
|
|
2367
2719
|
nonce,
|
|
2368
|
-
redirectUri: effectiveRedirectUri
|
|
2720
|
+
redirectUri: effectiveRedirectUri,
|
|
2721
|
+
state
|
|
2369
2722
|
};
|
|
2370
2723
|
};
|
|
2371
2724
|
|
|
@@ -2380,100 +2733,17 @@ const getLoggedInState = (state, response) => {
|
|
|
2380
2733
|
userName: typeof response.userName === 'string' ? response.userName : state.userName,
|
|
2381
2734
|
userState: accessToken ? 'loggedIn' : 'loggedOut',
|
|
2382
2735
|
userSubscriptionPlan: typeof response.subscriptionPlan === 'string' ? response.subscriptionPlan : state.userSubscriptionPlan,
|
|
2736
|
+
userSubscriptionStatus: typeof response.subscriptionStatus === 'string' ? response.subscriptionStatus : state.userSubscriptionStatus,
|
|
2383
2737
|
userUsedTokens: typeof response.usedTokens === 'number' ? response.usedTokens : state.userUsedTokens
|
|
2384
2738
|
};
|
|
2385
2739
|
};
|
|
2386
2740
|
|
|
2387
2741
|
const isLoginResponse = value => {
|
|
2388
|
-
|
|
2389
|
-
return false;
|
|
2390
|
-
}
|
|
2391
|
-
return true;
|
|
2392
|
-
};
|
|
2393
|
-
|
|
2394
|
-
const databaseName = 'auth-worker';
|
|
2395
|
-
const objectStoreName = 'auth';
|
|
2396
|
-
const memoryStorage = new Map();
|
|
2397
|
-
let databasePromise;
|
|
2398
|
-
|
|
2399
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2400
|
-
const transactionToPromise = transaction => {
|
|
2401
|
-
return new Promise((resolve, reject) => {
|
|
2402
|
-
transaction.addEventListener('complete', () => {
|
|
2403
|
-
resolve();
|
|
2404
|
-
});
|
|
2405
|
-
transaction.addEventListener('abort', () => {
|
|
2406
|
-
reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
|
|
2407
|
-
});
|
|
2408
|
-
transaction.addEventListener('error', () => {
|
|
2409
|
-
reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
|
|
2410
|
-
});
|
|
2411
|
-
});
|
|
2412
|
-
};
|
|
2413
|
-
const getDatabase = async () => {
|
|
2414
|
-
if (typeof indexedDB === 'undefined') {
|
|
2415
|
-
return undefined;
|
|
2416
|
-
}
|
|
2417
|
-
if (!databasePromise) {
|
|
2418
|
-
databasePromise = new Promise((resolve, reject) => {
|
|
2419
|
-
const request = indexedDB.open(databaseName, 1);
|
|
2420
|
-
request.addEventListener('upgradeneeded', () => {
|
|
2421
|
-
const database = request.result;
|
|
2422
|
-
if (!database.objectStoreNames.contains(objectStoreName)) {
|
|
2423
|
-
database.createObjectStore(objectStoreName);
|
|
2424
|
-
}
|
|
2425
|
-
});
|
|
2426
|
-
request.addEventListener('success', () => {
|
|
2427
|
-
resolve(request.result);
|
|
2428
|
-
});
|
|
2429
|
-
request.addEventListener('error', () => {
|
|
2430
|
-
reject(request.error ?? new Error('Failed to open persistent auth storage.'));
|
|
2431
|
-
});
|
|
2432
|
-
});
|
|
2433
|
-
}
|
|
2434
|
-
return databasePromise;
|
|
2435
|
-
};
|
|
2436
|
-
const setPersistentAuthValue = async (key, value) => {
|
|
2437
|
-
memoryStorage.set(key, value);
|
|
2438
|
-
const database = await getDatabase();
|
|
2439
|
-
if (!database) {
|
|
2440
|
-
return;
|
|
2441
|
-
}
|
|
2442
|
-
const transaction = database.transaction(objectStoreName, 'readwrite');
|
|
2443
|
-
const objectStore = transaction.objectStore(objectStoreName);
|
|
2444
|
-
objectStore.put(value, key);
|
|
2445
|
-
await transactionToPromise(transaction);
|
|
2446
|
-
};
|
|
2447
|
-
|
|
2448
|
-
const getBackendOidcTokenUrl = backendUrl => {
|
|
2449
|
-
return getBackendAuthUrl(backendUrl, '/oidc/token');
|
|
2742
|
+
return !!value && typeof value === 'object';
|
|
2450
2743
|
};
|
|
2451
2744
|
|
|
2452
|
-
const getAuthorizationServer = backendUrl => {
|
|
2453
|
-
return {
|
|
2454
|
-
issuer: getBackendAuthUrl(backendUrl, '/oidc'),
|
|
2455
|
-
jwks_uri: getBackendAuthUrl(backendUrl, '/oidc/jwks'),
|
|
2456
|
-
token_endpoint: getBackendOidcTokenUrl(backendUrl)
|
|
2457
|
-
};
|
|
2458
|
-
};
|
|
2459
|
-
const getClient = () => {
|
|
2460
|
-
return {
|
|
2461
|
-
client_id: oidcClientId
|
|
2462
|
-
};
|
|
2463
|
-
};
|
|
2464
2745
|
const exchangeElectronAuthorizationCode = async (backendUrl, code, redirectUri, codeVerifier, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
|
|
2465
|
-
|
|
2466
|
-
const client = getClient();
|
|
2467
|
-
const response = await requestTokenEndpoint(authorizationServer, client, None(), 'authorization_code', new URLSearchParams({
|
|
2468
|
-
code,
|
|
2469
|
-
code_verifier: codeVerifier,
|
|
2470
|
-
redirect_uri: redirectUri
|
|
2471
|
-
}));
|
|
2472
|
-
const tokenResponse = await processTokenEndpointResponse(authorizationServer, client, response);
|
|
2473
|
-
return {
|
|
2474
|
-
accessToken: tokenResponse.access_token,
|
|
2475
|
-
refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : ''
|
|
2476
|
-
};
|
|
2746
|
+
return exchangeAuthorizationCode(backendUrl, nativeOidcClientId, code, redirectUri, codeVerifier, requestTokenEndpoint, processTokenEndpointResponse);
|
|
2477
2747
|
};
|
|
2478
2748
|
|
|
2479
2749
|
const hasAuthorizationCode = value => {
|
|
@@ -2500,13 +2770,54 @@ const waitForElectronBackendLogin = async (backendUrl, uid, redirectUri, codeVer
|
|
|
2500
2770
|
return getLoggedOutBackendAuthState('Timed out waiting for backend login.');
|
|
2501
2771
|
};
|
|
2502
2772
|
|
|
2503
|
-
const
|
|
2504
|
-
if (
|
|
2505
|
-
return
|
|
2773
|
+
const getMockLoginResult = async signingInState => {
|
|
2774
|
+
if (!hasPendingMockLoginResponse()) {
|
|
2775
|
+
return undefined;
|
|
2506
2776
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2777
|
+
const response = await consumeNextLoginResponse();
|
|
2778
|
+
if (!isLoginResponse(response)) {
|
|
2779
|
+
return {
|
|
2780
|
+
authErrorMessage: 'Backend returned an invalid login response.',
|
|
2781
|
+
userState: 'loggedOut'
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
if (typeof response.error === 'string' && response.error) {
|
|
2785
|
+
return {
|
|
2786
|
+
authErrorMessage: response.error,
|
|
2787
|
+
userState: 'loggedOut'
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
return persistLoginResult(getLoggedInState(signingInState, response));
|
|
2791
|
+
};
|
|
2792
|
+
const getInteractiveLoginResult = async (backendUrl, platform, authUseRedirect, signingInState) => {
|
|
2793
|
+
const uid = 0;
|
|
2794
|
+
const {
|
|
2795
|
+
clientId,
|
|
2796
|
+
codeVerifier,
|
|
2797
|
+
loginUrl,
|
|
2798
|
+
redirectUri,
|
|
2799
|
+
state
|
|
2800
|
+
} = await getBackendLoginRequest(backendUrl, platform, uid);
|
|
2801
|
+
if (platform !== Electron) {
|
|
2802
|
+
await savePendingOidcAuthState({
|
|
2803
|
+
clientId,
|
|
2804
|
+
codeVerifier,
|
|
2805
|
+
redirectUri,
|
|
2806
|
+
state
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
await invoke$1('Open.openUrl', loginUrl, platform, authUseRedirect);
|
|
2810
|
+
if (platform !== Electron && authUseRedirect) {
|
|
2811
|
+
return signingInState;
|
|
2812
|
+
}
|
|
2813
|
+
const authState = platform === Electron ? await waitForElectronBackendLogin(backendUrl, uid, redirectUri, codeVerifier) : await waitForBackendLogin(backendUrl);
|
|
2814
|
+
if (platform !== Electron) {
|
|
2815
|
+
await clearPendingOidcAuthState();
|
|
2816
|
+
}
|
|
2817
|
+
return persistLoginResult({
|
|
2818
|
+
...authState,
|
|
2819
|
+
authClientId: clientId
|
|
2820
|
+
});
|
|
2510
2821
|
};
|
|
2511
2822
|
const handleClickLogin = async options => {
|
|
2512
2823
|
const {
|
|
@@ -2525,32 +2836,13 @@ const handleClickLogin = async options => {
|
|
|
2525
2836
|
userState: 'loggingIn'
|
|
2526
2837
|
};
|
|
2527
2838
|
try {
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
return {
|
|
2532
|
-
authErrorMessage: 'Backend returned an invalid login response.',
|
|
2533
|
-
userState: 'loggedOut'
|
|
2534
|
-
};
|
|
2535
|
-
}
|
|
2536
|
-
if (typeof response.error === 'string' && response.error) {
|
|
2537
|
-
return {
|
|
2538
|
-
authErrorMessage: response.error,
|
|
2539
|
-
userState: 'loggedOut'
|
|
2540
|
-
};
|
|
2541
|
-
}
|
|
2542
|
-
return persistLoginResult(getLoggedInState(signingInState, response));
|
|
2839
|
+
const mockLoginResult = await getMockLoginResult(signingInState);
|
|
2840
|
+
if (mockLoginResult) {
|
|
2841
|
+
return mockLoginResult;
|
|
2543
2842
|
}
|
|
2544
|
-
|
|
2545
|
-
const {
|
|
2546
|
-
codeVerifier,
|
|
2547
|
-
loginUrl,
|
|
2548
|
-
redirectUri
|
|
2549
|
-
} = await getBackendLoginRequest(backendUrl, platform, uid);
|
|
2550
|
-
await invoke$1('Open.openUrl', loginUrl, platform, authUseRedirect);
|
|
2551
|
-
const authState = platform === Electron ? await waitForElectronBackendLogin(backendUrl, uid, redirectUri, codeVerifier) : await waitForBackendLogin(backendUrl);
|
|
2552
|
-
return persistLoginResult(authState);
|
|
2843
|
+
return getInteractiveLoginResult(backendUrl, platform, authUseRedirect, signingInState);
|
|
2553
2844
|
} catch (error) {
|
|
2845
|
+
await clearPendingOidcAuthState();
|
|
2554
2846
|
const errorMessage = error instanceof Error && error.message ? error.message : 'Backend authentication failed.';
|
|
2555
2847
|
return {
|
|
2556
2848
|
...signingInState,
|
|
@@ -2565,6 +2857,7 @@ const logout = async state => {
|
|
|
2565
2857
|
userState: 'loggingOut'
|
|
2566
2858
|
};
|
|
2567
2859
|
await logoutFromBackend(state.backendUrl);
|
|
2860
|
+
await Promise.all([clearOidcCallbackUrl(), clearPendingOidcAuthState(), clearStoredOidcClientId(), clearPersistentAuthValue('accessToken'), clearPersistentAuthValue('refreshToken')]);
|
|
2568
2861
|
return {
|
|
2569
2862
|
...loggingOutState,
|
|
2570
2863
|
...getLoggedOutBackendAuthState()
|