@lvce-editor/auth-worker 1.17.0 → 2.0.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 +715 -349
- package/package.json +1 -1
package/dist/authWorkerMain.js
CHANGED
|
@@ -1071,206 +1071,6 @@ const handleMessagePort = async (port, rpcId) => {
|
|
|
1071
1071
|
}
|
|
1072
1072
|
};
|
|
1073
1073
|
|
|
1074
|
-
const initialize = async platform => {
|
|
1075
|
-
// TODO
|
|
1076
|
-
};
|
|
1077
|
-
|
|
1078
|
-
const trailingSlashesRegex = /\/+$/;
|
|
1079
|
-
const trimTrailingSlashes = value => {
|
|
1080
|
-
return value.replace(trailingSlashesRegex, '');
|
|
1081
|
-
};
|
|
1082
|
-
|
|
1083
|
-
const getBackendAuthUrl = (backendUrl, path) => {
|
|
1084
|
-
return `${trimTrailingSlashes(backendUrl)}${path}`;
|
|
1085
|
-
};
|
|
1086
|
-
|
|
1087
|
-
const getLoggedOutBackendAuthState = (authErrorMessage = '') => {
|
|
1088
|
-
return {
|
|
1089
|
-
authAccessToken: '',
|
|
1090
|
-
authErrorMessage,
|
|
1091
|
-
userState: 'loggedOut'
|
|
1092
|
-
};
|
|
1093
|
-
};
|
|
1094
|
-
|
|
1095
|
-
const getBackendLogoutUrl = backendUrl => {
|
|
1096
|
-
return getBackendAuthUrl(backendUrl, '/auth/logout');
|
|
1097
|
-
};
|
|
1098
|
-
|
|
1099
|
-
const logoutFromBackend = async backendUrl => {
|
|
1100
|
-
if (!backendUrl) {
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
try {
|
|
1104
|
-
await fetch(getBackendLogoutUrl(backendUrl), {
|
|
1105
|
-
credentials: 'include',
|
|
1106
|
-
headers: {
|
|
1107
|
-
Accept: 'application/json'
|
|
1108
|
-
},
|
|
1109
|
-
method: 'POST'
|
|
1110
|
-
});
|
|
1111
|
-
} catch {
|
|
1112
|
-
// Ignore logout failures and still clear local auth state.
|
|
1113
|
-
}
|
|
1114
|
-
};
|
|
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 getUserName = value => {
|
|
1188
|
-
if (typeof value.userName === 'string') {
|
|
1189
|
-
return value.userName;
|
|
1190
|
-
}
|
|
1191
|
-
return getString(value.user?.displayName);
|
|
1192
|
-
};
|
|
1193
|
-
const toBackendAuthState = value => {
|
|
1194
|
-
return {
|
|
1195
|
-
authAccessToken: getString(value.accessToken),
|
|
1196
|
-
authErrorMessage: getString(value.error),
|
|
1197
|
-
authRefreshToken: getString(value.refreshToken),
|
|
1198
|
-
userName: getUserName(value),
|
|
1199
|
-
userState: value.accessToken ? 'loggedIn' : 'loggedOut',
|
|
1200
|
-
userSubscriptionPlan: getString(value.subscriptionPlan),
|
|
1201
|
-
userSubscriptionStatus: getString(value.subscriptionStatus),
|
|
1202
|
-
userUsedTokens: getNumber(value.usedTokens)
|
|
1203
|
-
};
|
|
1204
|
-
};
|
|
1205
|
-
|
|
1206
|
-
const parseBackendAuthResponse = value => {
|
|
1207
|
-
if (!isBackendAuthResponse(value)) {
|
|
1208
|
-
return getLoggedOutBackendAuthState('Backend returned an invalid authentication response.');
|
|
1209
|
-
}
|
|
1210
|
-
return toBackendAuthState(value);
|
|
1211
|
-
};
|
|
1212
|
-
|
|
1213
|
-
const getPayload = async response => {
|
|
1214
|
-
try {
|
|
1215
|
-
return await response.json();
|
|
1216
|
-
} catch {
|
|
1217
|
-
return undefined;
|
|
1218
|
-
}
|
|
1219
|
-
};
|
|
1220
|
-
const syncBackendAuth = async backendUrl => {
|
|
1221
|
-
if (!backendUrl) {
|
|
1222
|
-
return getLoggedOutBackendAuthState('Backend URL is missing.');
|
|
1223
|
-
}
|
|
1224
|
-
try {
|
|
1225
|
-
if (hasPendingMockRefreshResponse()) {
|
|
1226
|
-
const mockResponse = await consumeNextRefreshResponse();
|
|
1227
|
-
return parseBackendAuthResponse(mockResponse);
|
|
1228
|
-
}
|
|
1229
|
-
const response = await fetch(getBackendRefreshUrl(backendUrl), {
|
|
1230
|
-
credentials: 'include',
|
|
1231
|
-
headers: {
|
|
1232
|
-
Accept: 'application/json'
|
|
1233
|
-
},
|
|
1234
|
-
method: 'POST'
|
|
1235
|
-
});
|
|
1236
|
-
if (response.status === 401 || response.status === 403) {
|
|
1237
|
-
return getLoggedOutBackendAuthState();
|
|
1238
|
-
}
|
|
1239
|
-
const payload = await getPayload(response);
|
|
1240
|
-
if (!response.ok) {
|
|
1241
|
-
const parsed = parseBackendAuthResponse(payload);
|
|
1242
|
-
return getLoggedOutBackendAuthState(parsed.authErrorMessage || 'Backend authentication failed.');
|
|
1243
|
-
}
|
|
1244
|
-
const parsed = parseBackendAuthResponse(payload);
|
|
1245
|
-
if (parsed.authErrorMessage) {
|
|
1246
|
-
return getLoggedOutBackendAuthState(parsed.authErrorMessage);
|
|
1247
|
-
}
|
|
1248
|
-
if (!parsed.authAccessToken) {
|
|
1249
|
-
return getLoggedOutBackendAuthState();
|
|
1250
|
-
}
|
|
1251
|
-
return parsed;
|
|
1252
|
-
} catch (error) {
|
|
1253
|
-
const authErrorMessage = error instanceof Error && error.message ? error.message : 'Backend authentication failed.';
|
|
1254
|
-
return getLoggedOutBackendAuthState(authErrorMessage);
|
|
1255
|
-
}
|
|
1256
|
-
};
|
|
1257
|
-
|
|
1258
|
-
const waitForBackendLogin = async (backendUrl, timeoutMs = 30_000, pollIntervalMs = 1000) => {
|
|
1259
|
-
const deadline = Date.now() + timeoutMs;
|
|
1260
|
-
let lastErrorMessage = '';
|
|
1261
|
-
while (Date.now() < deadline) {
|
|
1262
|
-
const authState = await syncBackendAuth(backendUrl);
|
|
1263
|
-
if (authState.userState === 'loggedIn') {
|
|
1264
|
-
return authState;
|
|
1265
|
-
}
|
|
1266
|
-
if (authState.authErrorMessage) {
|
|
1267
|
-
lastErrorMessage = authState.authErrorMessage;
|
|
1268
|
-
}
|
|
1269
|
-
await delay(pollIntervalMs);
|
|
1270
|
-
}
|
|
1271
|
-
return getLoggedOutBackendAuthState(lastErrorMessage);
|
|
1272
|
-
};
|
|
1273
|
-
|
|
1274
1074
|
let USER_AGENT;
|
|
1275
1075
|
if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
|
|
1276
1076
|
const NAME = 'oauth4webapi';
|
|
@@ -2064,14 +1864,43 @@ async function getResponseJsonBody(response, check = assertApplicationJson) {
|
|
|
2064
1864
|
}
|
|
2065
1865
|
const _expectedIssuer = Symbol();
|
|
2066
1866
|
|
|
2067
|
-
|
|
1867
|
+
const trailingSlashesRegex = /\/+$/;
|
|
1868
|
+
const trimTrailingSlashes = value => {
|
|
1869
|
+
return value.replace(trailingSlashesRegex, '');
|
|
1870
|
+
};
|
|
2068
1871
|
|
|
2069
|
-
const
|
|
2070
|
-
|
|
2071
|
-
|
|
1872
|
+
const getBackendAuthUrl = (backendUrl, path) => {
|
|
1873
|
+
return `${trimTrailingSlashes(backendUrl)}${path}`;
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1876
|
+
const getBackendOidcTokenUrl = backendUrl => {
|
|
1877
|
+
return getBackendAuthUrl(backendUrl, '/oidc/token');
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
const getAuthorizationServer$1 = backendUrl => {
|
|
2072
1881
|
return {
|
|
2073
|
-
|
|
2074
|
-
|
|
1882
|
+
issuer: getBackendAuthUrl(backendUrl, '/oidc'),
|
|
1883
|
+
jwks_uri: getBackendAuthUrl(backendUrl, '/oidc/jwks'),
|
|
1884
|
+
token_endpoint: getBackendOidcTokenUrl(backendUrl)
|
|
1885
|
+
};
|
|
1886
|
+
};
|
|
1887
|
+
const getClient$1 = clientId => {
|
|
1888
|
+
return {
|
|
1889
|
+
client_id: clientId
|
|
1890
|
+
};
|
|
1891
|
+
};
|
|
1892
|
+
const exchangeAuthorizationCode = async (backendUrl, clientId, code, redirectUri, codeVerifier, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
|
|
1893
|
+
const authorizationServer = getAuthorizationServer$1(backendUrl);
|
|
1894
|
+
const client = getClient$1(clientId);
|
|
1895
|
+
const response = await requestTokenEndpoint(authorizationServer, client, None(), 'authorization_code', new URLSearchParams({
|
|
1896
|
+
code,
|
|
1897
|
+
code_verifier: codeVerifier,
|
|
1898
|
+
redirect_uri: redirectUri
|
|
1899
|
+
}));
|
|
1900
|
+
const tokenResponse = await processTokenEndpointResponse(authorizationServer, client, response);
|
|
1901
|
+
return {
|
|
1902
|
+
accessToken: tokenResponse.access_token,
|
|
1903
|
+
refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : ''
|
|
2075
1904
|
};
|
|
2076
1905
|
};
|
|
2077
1906
|
|
|
@@ -2087,29 +1916,605 @@ const getCurrentHref = async () => {
|
|
|
2087
1916
|
return globalThis.location.href;
|
|
2088
1917
|
};
|
|
2089
1918
|
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
1919
|
+
const getLoggedOutBackendAuthState = (authErrorMessage = '') => {
|
|
1920
|
+
return {
|
|
1921
|
+
authAccessToken: '',
|
|
1922
|
+
authErrorMessage,
|
|
1923
|
+
userState: 'loggedOut'
|
|
1924
|
+
};
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
const getPayload$1 = async response => {
|
|
1928
|
+
try {
|
|
1929
|
+
return await response.json();
|
|
1930
|
+
} catch {
|
|
1931
|
+
return undefined;
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
const getOidcUserName = async (backendUrl, accessToken, fetchFn = fetch) => {
|
|
1935
|
+
if (!backendUrl || !accessToken) {
|
|
1936
|
+
return '';
|
|
1937
|
+
}
|
|
1938
|
+
const response = await fetchFn(new URL('/account/me', backendUrl), {
|
|
1939
|
+
headers: {
|
|
1940
|
+
Accept: 'application/json',
|
|
1941
|
+
Authorization: `Bearer ${accessToken}`
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
if (!response.ok) {
|
|
1945
|
+
return '';
|
|
1946
|
+
}
|
|
1947
|
+
const payload = await getPayload$1(response);
|
|
1948
|
+
if (!payload || typeof payload !== 'object') {
|
|
1949
|
+
return '';
|
|
1950
|
+
}
|
|
1951
|
+
const displayName = Reflect.get(payload, 'displayName');
|
|
1952
|
+
return typeof displayName === 'string' ? displayName : '';
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
const databaseName = 'auth-worker';
|
|
1956
|
+
const objectStoreName = 'auth';
|
|
1957
|
+
const memoryStorage = new Map();
|
|
1958
|
+
let databasePromise;
|
|
1959
|
+
|
|
1960
|
+
// IndexedDB request objects are mutable browser primitives and cannot satisfy the readonly rule structurally.
|
|
1961
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1962
|
+
const requestToPromise = request => {
|
|
1963
|
+
return new Promise((resolve, reject) => {
|
|
1964
|
+
request.addEventListener('success', () => {
|
|
1965
|
+
resolve(request.result);
|
|
1966
|
+
});
|
|
1967
|
+
request.addEventListener('error', () => {
|
|
1968
|
+
reject(request.error ?? new Error('Persistent storage request failed.'));
|
|
1969
|
+
});
|
|
1970
|
+
});
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1974
|
+
const transactionToPromise = transaction => {
|
|
1975
|
+
return new Promise((resolve, reject) => {
|
|
1976
|
+
transaction.addEventListener('complete', () => {
|
|
1977
|
+
resolve();
|
|
1978
|
+
});
|
|
1979
|
+
transaction.addEventListener('abort', () => {
|
|
1980
|
+
reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
|
|
1981
|
+
});
|
|
1982
|
+
transaction.addEventListener('error', () => {
|
|
1983
|
+
reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
|
|
1984
|
+
});
|
|
1985
|
+
});
|
|
1986
|
+
};
|
|
1987
|
+
const getDatabase = async () => {
|
|
1988
|
+
if (typeof indexedDB === 'undefined') {
|
|
1989
|
+
return undefined;
|
|
1990
|
+
}
|
|
1991
|
+
if (!databasePromise) {
|
|
1992
|
+
databasePromise = new Promise((resolve, reject) => {
|
|
1993
|
+
const request = indexedDB.open(databaseName, 1);
|
|
1994
|
+
request.addEventListener('upgradeneeded', () => {
|
|
1995
|
+
const database = request.result;
|
|
1996
|
+
if (!database.objectStoreNames.contains(objectStoreName)) {
|
|
1997
|
+
database.createObjectStore(objectStoreName);
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
request.addEventListener('success', () => {
|
|
2001
|
+
resolve(request.result);
|
|
2002
|
+
});
|
|
2003
|
+
request.addEventListener('error', () => {
|
|
2004
|
+
reject(request.error ?? new Error('Failed to open persistent auth storage.'));
|
|
2005
|
+
});
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
return databasePromise;
|
|
2009
|
+
};
|
|
2010
|
+
const getPersistentAuthValue = async key => {
|
|
2011
|
+
const database = await getDatabase();
|
|
2012
|
+
if (!database) {
|
|
2013
|
+
return memoryStorage.get(key) ?? '';
|
|
2014
|
+
}
|
|
2015
|
+
const transaction = database.transaction(objectStoreName, 'readonly');
|
|
2016
|
+
const objectStore = transaction.objectStore(objectStoreName);
|
|
2017
|
+
const value = await requestToPromise(objectStore.get(key));
|
|
2018
|
+
return typeof value === 'string' ? value : '';
|
|
2019
|
+
};
|
|
2020
|
+
const setPersistentAuthValue = async (key, value) => {
|
|
2021
|
+
memoryStorage.set(key, value);
|
|
2022
|
+
const database = await getDatabase();
|
|
2023
|
+
if (!database) {
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const transaction = database.transaction(objectStoreName, 'readwrite');
|
|
2027
|
+
const objectStore = transaction.objectStore(objectStoreName);
|
|
2028
|
+
objectStore.put(value, key);
|
|
2029
|
+
await transactionToPromise(transaction);
|
|
2030
|
+
};
|
|
2031
|
+
const clearPersistentAuthValue = async key => {
|
|
2032
|
+
memoryStorage.delete(key);
|
|
2033
|
+
const database = await getDatabase();
|
|
2034
|
+
if (!database) {
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
const transaction = database.transaction(objectStoreName, 'readwrite');
|
|
2038
|
+
const objectStore = transaction.objectStore(objectStoreName);
|
|
2039
|
+
objectStore.delete(key);
|
|
2040
|
+
await transactionToPromise(transaction);
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
const callbackUrlKey = 'oidcCallbackUrl';
|
|
2044
|
+
const clientIdKey = 'oidcClientId';
|
|
2045
|
+
const pendingClientIdKey = 'pendingOidcClientId';
|
|
2046
|
+
const pendingCodeVerifierKey = 'pendingOidcCodeVerifier';
|
|
2047
|
+
const pendingRedirectUriKey = 'pendingOidcRedirectUri';
|
|
2048
|
+
const pendingStateKey = 'pendingOidcState';
|
|
2049
|
+
const clearOidcCallbackUrl = async () => {
|
|
2050
|
+
await clearPersistentAuthValue(callbackUrlKey);
|
|
2051
|
+
};
|
|
2052
|
+
const clearStoredOidcClientId = async () => {
|
|
2053
|
+
await clearPersistentAuthValue(clientIdKey);
|
|
2054
|
+
};
|
|
2055
|
+
const clearPendingOidcAuthState = async () => {
|
|
2056
|
+
await Promise.all([clearPersistentAuthValue(pendingClientIdKey), clearPersistentAuthValue(pendingCodeVerifierKey), clearPersistentAuthValue(pendingRedirectUriKey), clearPersistentAuthValue(pendingStateKey)]);
|
|
2057
|
+
};
|
|
2058
|
+
const getOidcCallbackUrl = async () => {
|
|
2059
|
+
return getPersistentAuthValue(callbackUrlKey);
|
|
2060
|
+
};
|
|
2061
|
+
const getStoredOidcClientId = async () => {
|
|
2062
|
+
return getPersistentAuthValue(clientIdKey);
|
|
2063
|
+
};
|
|
2064
|
+
const loadPendingOidcAuthState = async () => {
|
|
2065
|
+
const [clientId, codeVerifier, redirectUri, state] = await Promise.all([getPersistentAuthValue(pendingClientIdKey), getPersistentAuthValue(pendingCodeVerifierKey), getPersistentAuthValue(pendingRedirectUriKey), getPersistentAuthValue(pendingStateKey)]);
|
|
2066
|
+
if (!clientId || !codeVerifier || !redirectUri || !state) {
|
|
2067
|
+
return undefined;
|
|
2068
|
+
}
|
|
2069
|
+
return {
|
|
2070
|
+
clientId,
|
|
2071
|
+
codeVerifier,
|
|
2072
|
+
redirectUri,
|
|
2073
|
+
state
|
|
2074
|
+
};
|
|
2075
|
+
};
|
|
2076
|
+
const saveOidcClientId = async clientId => {
|
|
2077
|
+
await setPersistentAuthValue(clientIdKey, clientId);
|
|
2078
|
+
};
|
|
2079
|
+
const savePendingOidcAuthState = async value => {
|
|
2080
|
+
await Promise.all([setPersistentAuthValue(pendingClientIdKey, value.clientId), setPersistentAuthValue(pendingCodeVerifierKey, value.codeVerifier), setPersistentAuthValue(pendingRedirectUriKey, value.redirectUri), setPersistentAuthValue(pendingStateKey, value.state)]);
|
|
2081
|
+
};
|
|
2082
|
+
|
|
2083
|
+
const normalizeRedirectUri = value => {
|
|
2084
|
+
const url = new URL(value);
|
|
2085
|
+
url.hash = '';
|
|
2086
|
+
url.search = '';
|
|
2087
|
+
return url.toString();
|
|
2088
|
+
};
|
|
2089
|
+
const getCallbackHref = async () => {
|
|
2090
|
+
const storedCallbackUrl = await getOidcCallbackUrl();
|
|
2091
|
+
if (storedCallbackUrl) {
|
|
2092
|
+
await clearOidcCallbackUrl();
|
|
2093
|
+
return storedCallbackUrl;
|
|
2094
|
+
}
|
|
2095
|
+
return getCurrentHref();
|
|
2096
|
+
};
|
|
2097
|
+
const completeBrowserOidcLogin = async backendUrl => {
|
|
2098
|
+
const href = await getCallbackHref();
|
|
2099
|
+
if (!href) {
|
|
2100
|
+
return undefined;
|
|
2101
|
+
}
|
|
2102
|
+
let url;
|
|
2103
|
+
try {
|
|
2104
|
+
url = new URL(href);
|
|
2105
|
+
} catch {
|
|
2106
|
+
return undefined;
|
|
2107
|
+
}
|
|
2108
|
+
const code = url.searchParams.get('code') || '';
|
|
2109
|
+
const error = url.searchParams.get('error') || '';
|
|
2110
|
+
const errorDescription = url.searchParams.get('error_description') || '';
|
|
2111
|
+
if (!code && !error) {
|
|
2112
|
+
return undefined;
|
|
2113
|
+
}
|
|
2114
|
+
const pendingAuthState = await loadPendingOidcAuthState();
|
|
2115
|
+
if (!pendingAuthState) {
|
|
2116
|
+
await clearPendingOidcAuthState();
|
|
2117
|
+
return getLoggedOutBackendAuthState('Authentication state is missing.');
|
|
2118
|
+
}
|
|
2119
|
+
if (normalizeRedirectUri(href) !== normalizeRedirectUri(pendingAuthState.redirectUri)) {
|
|
2120
|
+
await clearPendingOidcAuthState();
|
|
2121
|
+
return getLoggedOutBackendAuthState('Authentication returned to an unexpected redirect URI.');
|
|
2122
|
+
}
|
|
2123
|
+
if (error) {
|
|
2124
|
+
await clearPendingOidcAuthState();
|
|
2125
|
+
return getLoggedOutBackendAuthState(errorDescription || error);
|
|
2126
|
+
}
|
|
2127
|
+
const returnedState = url.searchParams.get('state') || '';
|
|
2128
|
+
if (!returnedState || returnedState !== pendingAuthState.state) {
|
|
2129
|
+
await clearPendingOidcAuthState();
|
|
2130
|
+
return getLoggedOutBackendAuthState('Authentication state mismatch.');
|
|
2131
|
+
}
|
|
2132
|
+
const exchanged = await exchangeAuthorizationCode(backendUrl, pendingAuthState.clientId, code, pendingAuthState.redirectUri, pendingAuthState.codeVerifier);
|
|
2133
|
+
const userName = await getOidcUserName(backendUrl, exchanged.accessToken);
|
|
2134
|
+
await clearPendingOidcAuthState();
|
|
2135
|
+
return {
|
|
2136
|
+
authAccessToken: exchanged.accessToken,
|
|
2137
|
+
authClientId: pendingAuthState.clientId,
|
|
2138
|
+
authErrorMessage: '',
|
|
2139
|
+
authRefreshToken: exchanged.refreshToken,
|
|
2140
|
+
userName,
|
|
2141
|
+
userState: exchanged.accessToken ? 'loggedIn' : 'loggedOut'
|
|
2142
|
+
};
|
|
2143
|
+
};
|
|
2144
|
+
|
|
2145
|
+
const accessTokenKey = 'accessToken';
|
|
2146
|
+
const refreshTokenKey = 'refreshToken';
|
|
2147
|
+
const userNameKey = 'userName';
|
|
2148
|
+
const userSubscriptionPlanKey = 'userSubscriptionPlan';
|
|
2149
|
+
const userSubscriptionStatusKey = 'userSubscriptionStatus';
|
|
2150
|
+
const userUsedTokensKey = 'userUsedTokens';
|
|
2151
|
+
const toOptionalString = value => {
|
|
2152
|
+
return value || undefined;
|
|
2153
|
+
};
|
|
2154
|
+
const toOptionalNumber = value => {
|
|
2155
|
+
if (!value) {
|
|
2156
|
+
return undefined;
|
|
2157
|
+
}
|
|
2158
|
+
const numberValue = Number(value);
|
|
2159
|
+
return Number.isFinite(numberValue) ? numberValue : undefined;
|
|
2160
|
+
};
|
|
2161
|
+
const clearPersistedAuthSession = async () => {
|
|
2162
|
+
await Promise.all([clearPersistentAuthValue(accessTokenKey), clearPersistentAuthValue(refreshTokenKey), clearStoredOidcClientId(), clearPersistentAuthValue(userNameKey), clearPersistentAuthValue(userSubscriptionPlanKey), clearPersistentAuthValue(userSubscriptionStatusKey), clearPersistentAuthValue(userUsedTokensKey)]);
|
|
2163
|
+
};
|
|
2164
|
+
const getPersistedAuthSession = async () => {
|
|
2165
|
+
const [accessToken, refreshToken, authClientId, userName, userSubscriptionPlan, userSubscriptionStatus, userUsedTokens] = await Promise.all([getPersistentAuthValue(accessTokenKey), getPersistentAuthValue(refreshTokenKey), getStoredOidcClientId(), getPersistentAuthValue(userNameKey), getPersistentAuthValue(userSubscriptionPlanKey), getPersistentAuthValue(userSubscriptionStatusKey), getPersistentAuthValue(userUsedTokensKey)]);
|
|
2166
|
+
if (!accessToken && !refreshToken) {
|
|
2167
|
+
return undefined;
|
|
2168
|
+
}
|
|
2169
|
+
const optionalAuthClientId = toOptionalString(authClientId);
|
|
2170
|
+
const optionalRefreshToken = toOptionalString(refreshToken);
|
|
2171
|
+
const optionalUserName = toOptionalString(userName);
|
|
2172
|
+
const optionalSubscriptionPlan = toOptionalString(userSubscriptionPlan);
|
|
2173
|
+
const optionalSubscriptionStatus = toOptionalString(userSubscriptionStatus);
|
|
2174
|
+
const optionalUsedTokens = toOptionalNumber(userUsedTokens);
|
|
2175
|
+
return {
|
|
2176
|
+
authAccessToken: accessToken,
|
|
2177
|
+
authErrorMessage: '',
|
|
2178
|
+
userState: 'loggedIn',
|
|
2179
|
+
...(optionalAuthClientId ? {
|
|
2180
|
+
authClientId
|
|
2181
|
+
} : {}),
|
|
2182
|
+
...(optionalRefreshToken ? {
|
|
2183
|
+
authRefreshToken: refreshToken
|
|
2184
|
+
} : {}),
|
|
2185
|
+
...(optionalUserName ? {
|
|
2186
|
+
userName
|
|
2187
|
+
} : {}),
|
|
2188
|
+
...(optionalSubscriptionPlan ? {
|
|
2189
|
+
userSubscriptionPlan
|
|
2190
|
+
} : {}),
|
|
2191
|
+
...(optionalSubscriptionStatus ? {
|
|
2192
|
+
userSubscriptionStatus
|
|
2193
|
+
} : {}),
|
|
2194
|
+
...(typeof optionalUsedTokens === 'number' ? {
|
|
2195
|
+
userUsedTokens: optionalUsedTokens
|
|
2196
|
+
} : {})
|
|
2197
|
+
};
|
|
2198
|
+
};
|
|
2199
|
+
const persistAuthSession = async loginResult => {
|
|
2200
|
+
await Promise.all([setPersistentAuthValue(accessTokenKey, loginResult.authAccessToken ?? ''), setPersistentAuthValue(refreshTokenKey, loginResult.authRefreshToken ?? ''), loginResult.authClientId ? saveOidcClientId(loginResult.authClientId) : clearStoredOidcClientId(), setPersistentAuthValue(userNameKey, loginResult.userName ?? ''), setPersistentAuthValue(userSubscriptionPlanKey, loginResult.userSubscriptionPlan ?? ''), setPersistentAuthValue(userSubscriptionStatusKey, loginResult.userSubscriptionStatus ?? ''), setPersistentAuthValue(userUsedTokensKey, typeof loginResult.userUsedTokens === 'number' ? String(loginResult.userUsedTokens) : '')]);
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
const persistLoginResult = async loginResult => {
|
|
2204
|
+
if (loginResult.userState !== 'loggedIn') {
|
|
2205
|
+
await clearPersistedAuthSession();
|
|
2206
|
+
return loginResult;
|
|
2207
|
+
}
|
|
2208
|
+
await persistAuthSession(loginResult);
|
|
2209
|
+
return loginResult;
|
|
2210
|
+
};
|
|
2211
|
+
|
|
2212
|
+
const getBackendUrl = options => {
|
|
2213
|
+
if (typeof options === 'number') {
|
|
2214
|
+
return '';
|
|
2215
|
+
}
|
|
2216
|
+
return options.backendUrl || '';
|
|
2217
|
+
};
|
|
2218
|
+
const initialize = async options => {
|
|
2219
|
+
const backendUrl = getBackendUrl(options);
|
|
2220
|
+
try {
|
|
2221
|
+
if (backendUrl) {
|
|
2222
|
+
const completedBrowserLogin = await completeBrowserOidcLogin(backendUrl);
|
|
2223
|
+
if (completedBrowserLogin) {
|
|
2224
|
+
return persistLoginResult(completedBrowserLogin);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
const persistedAuthSession = await getPersistedAuthSession();
|
|
2228
|
+
if (persistedAuthSession) {
|
|
2229
|
+
return persistedAuthSession;
|
|
2230
|
+
}
|
|
2231
|
+
return getLoggedOutBackendAuthState();
|
|
2232
|
+
} catch (error) {
|
|
2233
|
+
const authErrorMessage = error instanceof Error && error.message ? error.message : 'Backend authentication failed.';
|
|
2234
|
+
return getLoggedOutBackendAuthState(authErrorMessage);
|
|
2235
|
+
}
|
|
2236
|
+
};
|
|
2237
|
+
|
|
2238
|
+
const getBackendLogoutUrl = backendUrl => {
|
|
2239
|
+
return getBackendAuthUrl(backendUrl, '/auth/logout');
|
|
2240
|
+
};
|
|
2241
|
+
|
|
2242
|
+
const logoutFromBackend = async backendUrl => {
|
|
2243
|
+
if (!backendUrl) {
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
try {
|
|
2247
|
+
await fetch(getBackendLogoutUrl(backendUrl), {
|
|
2248
|
+
credentials: 'include',
|
|
2249
|
+
headers: {
|
|
2250
|
+
Accept: 'application/json'
|
|
2251
|
+
},
|
|
2252
|
+
method: 'POST'
|
|
2253
|
+
});
|
|
2254
|
+
} catch {
|
|
2255
|
+
// Ignore logout failures and still clear local auth state.
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
const getBackendRefreshUrl = backendUrl => {
|
|
2260
|
+
return getBackendAuthUrl(backendUrl, '/auth/refresh');
|
|
2261
|
+
};
|
|
2262
|
+
|
|
2263
|
+
const delay = async ms => {
|
|
2264
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
2265
|
+
};
|
|
2266
|
+
|
|
2267
|
+
let nextLoginResponse;
|
|
2268
|
+
let nextRefreshResponse;
|
|
2269
|
+
const setNextLoginResponse = response => {
|
|
2270
|
+
nextLoginResponse = response;
|
|
2271
|
+
};
|
|
2272
|
+
const setNextRefreshResponse = response => {
|
|
2273
|
+
nextRefreshResponse = response;
|
|
2274
|
+
};
|
|
2275
|
+
const clear = () => {
|
|
2276
|
+
nextLoginResponse = undefined;
|
|
2277
|
+
nextRefreshResponse = undefined;
|
|
2278
|
+
};
|
|
2279
|
+
const hasPendingMockLoginResponse = () => {
|
|
2280
|
+
return !!nextLoginResponse;
|
|
2281
|
+
};
|
|
2282
|
+
const hasPendingMockRefreshResponse = () => {
|
|
2283
|
+
return !!nextRefreshResponse;
|
|
2284
|
+
};
|
|
2285
|
+
const consumeNextLoginResponse = async () => {
|
|
2286
|
+
if (!nextLoginResponse) {
|
|
2287
|
+
return undefined;
|
|
2288
|
+
}
|
|
2289
|
+
const response = nextLoginResponse;
|
|
2290
|
+
nextLoginResponse = undefined;
|
|
2291
|
+
if (response.delay > 0) {
|
|
2292
|
+
await delay(response.delay);
|
|
2293
|
+
}
|
|
2294
|
+
if (response.type === 'error') {
|
|
2295
|
+
throw new Error(response.message);
|
|
2296
|
+
}
|
|
2297
|
+
return response.response;
|
|
2298
|
+
};
|
|
2299
|
+
const consumeNextRefreshResponse = async () => {
|
|
2300
|
+
if (!nextRefreshResponse) {
|
|
2301
|
+
return undefined;
|
|
2302
|
+
}
|
|
2303
|
+
const response = nextRefreshResponse;
|
|
2304
|
+
nextRefreshResponse = undefined;
|
|
2305
|
+
if (response.delay > 0) {
|
|
2306
|
+
await new Promise(resolve => setTimeout(resolve, response.delay));
|
|
2307
|
+
}
|
|
2308
|
+
if (response.type === 'error') {
|
|
2309
|
+
throw new Error(response.message);
|
|
2310
|
+
}
|
|
2311
|
+
return response.response;
|
|
2312
|
+
};
|
|
2313
|
+
|
|
2314
|
+
const isObject = value => {
|
|
2315
|
+
return !!value && typeof value === 'object';
|
|
2316
|
+
};
|
|
2317
|
+
|
|
2318
|
+
const isBackendAuthResponse = value => {
|
|
2319
|
+
return isObject(value);
|
|
2320
|
+
};
|
|
2321
|
+
|
|
2322
|
+
const getNumber = (value, fallback = 0) => {
|
|
2323
|
+
return typeof value === 'number' ? value : fallback;
|
|
2324
|
+
};
|
|
2325
|
+
|
|
2326
|
+
const getString = (value, fallback = '') => {
|
|
2327
|
+
return typeof value === 'string' ? value : fallback;
|
|
2328
|
+
};
|
|
2329
|
+
|
|
2330
|
+
const getUserName = value => {
|
|
2331
|
+
if (typeof value.userName === 'string') {
|
|
2332
|
+
return value.userName;
|
|
2333
|
+
}
|
|
2334
|
+
return getString(value.user?.displayName);
|
|
2335
|
+
};
|
|
2336
|
+
const toBackendAuthState = value => {
|
|
2337
|
+
return {
|
|
2338
|
+
authAccessToken: getString(value.accessToken),
|
|
2339
|
+
authErrorMessage: getString(value.error),
|
|
2340
|
+
authRefreshToken: getString(value.refreshToken),
|
|
2341
|
+
userName: getUserName(value),
|
|
2342
|
+
userState: value.accessToken ? 'loggedIn' : 'loggedOut',
|
|
2343
|
+
userSubscriptionPlan: getString(value.subscriptionPlan),
|
|
2344
|
+
userSubscriptionStatus: getString(value.subscriptionStatus),
|
|
2345
|
+
userUsedTokens: getNumber(value.usedTokens)
|
|
2346
|
+
};
|
|
2347
|
+
};
|
|
2348
|
+
|
|
2349
|
+
const parseBackendAuthResponse = value => {
|
|
2350
|
+
if (!isBackendAuthResponse(value)) {
|
|
2351
|
+
return getLoggedOutBackendAuthState('Backend returned an invalid authentication response.');
|
|
2352
|
+
}
|
|
2353
|
+
return toBackendAuthState(value);
|
|
2354
|
+
};
|
|
2355
|
+
|
|
2356
|
+
const getAuthorizationServer = backendUrl => {
|
|
2357
|
+
return {
|
|
2358
|
+
issuer: getBackendAuthUrl(backendUrl, '/oidc'),
|
|
2359
|
+
jwks_uri: getBackendAuthUrl(backendUrl, '/oidc/jwks'),
|
|
2360
|
+
token_endpoint: getBackendOidcTokenUrl(backendUrl)
|
|
2361
|
+
};
|
|
2362
|
+
};
|
|
2363
|
+
const getClient = clientId => {
|
|
2364
|
+
return {
|
|
2365
|
+
client_id: clientId
|
|
2366
|
+
};
|
|
2367
|
+
};
|
|
2368
|
+
const refreshOidcTokens = async (backendUrl, clientId, refreshToken, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
|
|
2369
|
+
const authorizationServer = getAuthorizationServer(backendUrl);
|
|
2370
|
+
const client = getClient(clientId);
|
|
2371
|
+
const response = await requestTokenEndpoint(authorizationServer, client, None(), 'refresh_token', new URLSearchParams({
|
|
2372
|
+
refresh_token: refreshToken
|
|
2373
|
+
}));
|
|
2374
|
+
const tokenResponse = await processTokenEndpointResponse(authorizationServer, client, response);
|
|
2375
|
+
return {
|
|
2376
|
+
accessToken: tokenResponse.access_token,
|
|
2377
|
+
refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : refreshToken
|
|
2378
|
+
};
|
|
2379
|
+
};
|
|
2380
|
+
|
|
2381
|
+
const clearStoredOidcAuth = async () => {
|
|
2382
|
+
await clearPersistedAuthSession();
|
|
2383
|
+
};
|
|
2384
|
+
const toLoginResult = (accessToken, refreshToken, clientId, userName) => {
|
|
2385
|
+
return {
|
|
2386
|
+
authAccessToken: accessToken,
|
|
2387
|
+
authClientId: clientId,
|
|
2388
|
+
authErrorMessage: '',
|
|
2389
|
+
authRefreshToken: refreshToken,
|
|
2390
|
+
userName,
|
|
2391
|
+
userState: accessToken ? 'loggedIn' : 'loggedOut'
|
|
2392
|
+
};
|
|
2393
|
+
};
|
|
2394
|
+
const restoreOidcAuth = async backendUrl => {
|
|
2395
|
+
const [accessToken, refreshToken, clientId] = await Promise.all([getPersistentAuthValue('accessToken'), getPersistentAuthValue('refreshToken'), getStoredOidcClientId()]);
|
|
2396
|
+
if (!refreshToken || !clientId) {
|
|
2397
|
+
return undefined;
|
|
2398
|
+
}
|
|
2399
|
+
try {
|
|
2400
|
+
const refreshedTokens = await refreshOidcTokens(backendUrl, clientId, refreshToken);
|
|
2401
|
+
const userName = await getOidcUserName(backendUrl, refreshedTokens.accessToken);
|
|
2402
|
+
return toLoginResult(refreshedTokens.accessToken, refreshedTokens.refreshToken, clientId, userName);
|
|
2403
|
+
} catch {
|
|
2404
|
+
if (accessToken) {
|
|
2405
|
+
const userName = await getOidcUserName(backendUrl, accessToken);
|
|
2406
|
+
if (userName) {
|
|
2407
|
+
return toLoginResult(accessToken, refreshToken, clientId, userName);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
await clearStoredOidcAuth();
|
|
2411
|
+
return undefined;
|
|
2412
|
+
}
|
|
2413
|
+
};
|
|
2414
|
+
|
|
2415
|
+
const getPayload = async response => {
|
|
2416
|
+
try {
|
|
2417
|
+
return await response.json();
|
|
2418
|
+
} catch {
|
|
2419
|
+
return undefined;
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
const syncBackendAuth = async backendUrl => {
|
|
2423
|
+
if (!backendUrl) {
|
|
2424
|
+
return getLoggedOutBackendAuthState('Backend URL is missing.');
|
|
2425
|
+
}
|
|
2426
|
+
try {
|
|
2427
|
+
const completedBrowserLogin = await completeBrowserOidcLogin(backendUrl);
|
|
2428
|
+
if (completedBrowserLogin) {
|
|
2429
|
+
return persistLoginResult(completedBrowserLogin);
|
|
2430
|
+
}
|
|
2431
|
+
const restoredOidcAuth = await restoreOidcAuth(backendUrl);
|
|
2432
|
+
if (restoredOidcAuth) {
|
|
2433
|
+
return persistLoginResult(restoredOidcAuth);
|
|
2434
|
+
}
|
|
2435
|
+
if (hasPendingMockRefreshResponse()) {
|
|
2436
|
+
const mockResponse = await consumeNextRefreshResponse();
|
|
2437
|
+
return parseBackendAuthResponse(mockResponse);
|
|
2438
|
+
}
|
|
2439
|
+
const response = await fetch(getBackendRefreshUrl(backendUrl), {
|
|
2440
|
+
credentials: 'include',
|
|
2441
|
+
headers: {
|
|
2442
|
+
Accept: 'application/json'
|
|
2443
|
+
},
|
|
2444
|
+
method: 'POST'
|
|
2445
|
+
});
|
|
2446
|
+
if (response.status === 401 || response.status === 403) {
|
|
2447
|
+
return getLoggedOutBackendAuthState();
|
|
2448
|
+
}
|
|
2449
|
+
const payload = await getPayload(response);
|
|
2450
|
+
if (!response.ok) {
|
|
2451
|
+
const parsed = parseBackendAuthResponse(payload);
|
|
2452
|
+
return getLoggedOutBackendAuthState(parsed.authErrorMessage || 'Backend authentication failed.');
|
|
2453
|
+
}
|
|
2454
|
+
const parsed = parseBackendAuthResponse(payload);
|
|
2455
|
+
if (parsed.authErrorMessage) {
|
|
2456
|
+
return getLoggedOutBackendAuthState(parsed.authErrorMessage);
|
|
2457
|
+
}
|
|
2458
|
+
if (!parsed.authAccessToken) {
|
|
2459
|
+
return getLoggedOutBackendAuthState();
|
|
2460
|
+
}
|
|
2461
|
+
return parsed;
|
|
2462
|
+
} catch (error) {
|
|
2463
|
+
const authErrorMessage = error instanceof Error && error.message ? error.message : 'Backend authentication failed.';
|
|
2464
|
+
return getLoggedOutBackendAuthState(authErrorMessage);
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
|
|
2468
|
+
const waitForBackendLogin = async (backendUrl, timeoutMs = 30_000, pollIntervalMs = 1000) => {
|
|
2469
|
+
const deadline = Date.now() + timeoutMs;
|
|
2470
|
+
let lastErrorMessage = '';
|
|
2471
|
+
while (Date.now() < deadline) {
|
|
2472
|
+
const authState = await syncBackendAuth(backendUrl);
|
|
2473
|
+
if (authState.userState === 'loggedIn') {
|
|
2474
|
+
return authState;
|
|
2475
|
+
}
|
|
2476
|
+
if (authState.authErrorMessage) {
|
|
2477
|
+
lastErrorMessage = authState.authErrorMessage;
|
|
2478
|
+
}
|
|
2479
|
+
await delay(pollIntervalMs);
|
|
2480
|
+
}
|
|
2481
|
+
return getLoggedOutBackendAuthState(lastErrorMessage);
|
|
2482
|
+
};
|
|
2483
|
+
|
|
2484
|
+
// cspell:ignore pkce
|
|
2485
|
+
|
|
2486
|
+
const createPkceValues = async () => {
|
|
2487
|
+
const codeVerifier = generateRandomCodeVerifier();
|
|
2488
|
+
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
|
2489
|
+
return {
|
|
2490
|
+
codeChallenge,
|
|
2491
|
+
codeVerifier
|
|
2492
|
+
};
|
|
2493
|
+
};
|
|
2494
|
+
|
|
2495
|
+
const successHtml = `<!doctype html>
|
|
2496
|
+
<html lang="en">
|
|
2497
|
+
<head>
|
|
2498
|
+
<meta charset="utf-8">
|
|
2499
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2500
|
+
<title>Authentication Complete</title>
|
|
2501
|
+
<style>
|
|
2502
|
+
:root {
|
|
2503
|
+
color-scheme: light;
|
|
2504
|
+
--background: linear-gradient(180deg, #f4f7fb 0%, #e9eef8 100%);
|
|
2505
|
+
--panel: rgba(255, 255, 255, 0.92);
|
|
2506
|
+
--panel-border: rgba(33, 52, 88, 0.08);
|
|
2507
|
+
--text: #132238;
|
|
2508
|
+
--muted: #5f6f86;
|
|
2509
|
+
--accent: #1f7a5a;
|
|
2510
|
+
--accent-soft: #e7f6ef;
|
|
2511
|
+
--shadow: 0 24px 60px rgba(44, 65, 98, 0.16);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
* {
|
|
2515
|
+
box-sizing: border-box;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2113
2518
|
html,
|
|
2114
2519
|
body {
|
|
2115
2520
|
margin: 0;
|
|
@@ -2338,6 +2743,18 @@ const getElectronRedirectUri = async uid => {
|
|
|
2338
2743
|
return `http://localhost:${localOauthServerPort}/callback`;
|
|
2339
2744
|
};
|
|
2340
2745
|
|
|
2746
|
+
const getWebRedirectUri = async () => {
|
|
2747
|
+
const href = await getCurrentHref();
|
|
2748
|
+
if (!href) {
|
|
2749
|
+
return '';
|
|
2750
|
+
}
|
|
2751
|
+
try {
|
|
2752
|
+
const url = new URL(href);
|
|
2753
|
+
return `${url.origin}/auth/callback`;
|
|
2754
|
+
} catch {
|
|
2755
|
+
return '';
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2341
2758
|
const getEffectiveRedirectUri = async (platform, uid, redirectUri) => {
|
|
2342
2759
|
if (redirectUri) {
|
|
2343
2760
|
return redirectUri;
|
|
@@ -2345,11 +2762,15 @@ const getEffectiveRedirectUri = async (platform, uid, redirectUri) => {
|
|
|
2345
2762
|
if (platform === Electron) {
|
|
2346
2763
|
return getElectronRedirectUri(uid);
|
|
2347
2764
|
}
|
|
2348
|
-
return
|
|
2765
|
+
return getWebRedirectUri();
|
|
2349
2766
|
};
|
|
2350
2767
|
|
|
2351
|
-
const
|
|
2768
|
+
const nativeOidcClientId = 'lvce-editor-native';
|
|
2769
|
+
const webOidcClientId = 'lvce-editor-web';
|
|
2352
2770
|
const oidcScope = 'openid offline_access profile email';
|
|
2771
|
+
const getOidcClientId = platform => {
|
|
2772
|
+
return platform === Electron ? nativeOidcClientId : webOidcClientId;
|
|
2773
|
+
};
|
|
2353
2774
|
|
|
2354
2775
|
const getBackendLoginRequest = async (backendUrl, platform = 0, uid = 0, redirectUri = '', createPkceValuesFn = createPkceValues, createRandomUuid = () => globalThis.crypto.randomUUID()) => {
|
|
2355
2776
|
const effectiveRedirectUri = await getEffectiveRedirectUri(platform, uid, redirectUri);
|
|
@@ -2357,24 +2778,28 @@ const getBackendLoginRequest = async (backendUrl, platform = 0, uid = 0, redirec
|
|
|
2357
2778
|
codeChallenge,
|
|
2358
2779
|
codeVerifier
|
|
2359
2780
|
} = await createPkceValuesFn();
|
|
2781
|
+
const clientId = getOidcClientId(platform);
|
|
2360
2782
|
const nonce = createRandomUuid();
|
|
2783
|
+
const state = createRandomUuid();
|
|
2361
2784
|
const loginUrl = new URL(getBackendAuthUrl(backendUrl, '/oidc/auth'));
|
|
2362
|
-
loginUrl.searchParams.set('client_id',
|
|
2785
|
+
loginUrl.searchParams.set('client_id', clientId);
|
|
2363
2786
|
loginUrl.searchParams.set('code_challenge', codeChallenge);
|
|
2364
2787
|
loginUrl.searchParams.set('code_challenge_method', 'S256');
|
|
2365
2788
|
loginUrl.searchParams.set('nonce', nonce);
|
|
2366
2789
|
loginUrl.searchParams.set('prompt', 'consent');
|
|
2367
2790
|
loginUrl.searchParams.set('response_type', 'code');
|
|
2368
2791
|
loginUrl.searchParams.set('scope', oidcScope);
|
|
2369
|
-
loginUrl.searchParams.set('state',
|
|
2792
|
+
loginUrl.searchParams.set('state', state);
|
|
2370
2793
|
if (effectiveRedirectUri) {
|
|
2371
2794
|
loginUrl.searchParams.set('redirect_uri', effectiveRedirectUri);
|
|
2372
2795
|
}
|
|
2373
2796
|
return {
|
|
2797
|
+
clientId,
|
|
2374
2798
|
codeVerifier,
|
|
2375
2799
|
loginUrl: loginUrl.toString(),
|
|
2376
2800
|
nonce,
|
|
2377
|
-
redirectUri: effectiveRedirectUri
|
|
2801
|
+
redirectUri: effectiveRedirectUri,
|
|
2802
|
+
state
|
|
2378
2803
|
};
|
|
2379
2804
|
};
|
|
2380
2805
|
|
|
@@ -2398,98 +2823,8 @@ const isLoginResponse = value => {
|
|
|
2398
2823
|
return !!value && typeof value === 'object';
|
|
2399
2824
|
};
|
|
2400
2825
|
|
|
2401
|
-
const databaseName = 'auth-worker';
|
|
2402
|
-
const objectStoreName = 'auth';
|
|
2403
|
-
const memoryStorage = new Map();
|
|
2404
|
-
let databasePromise;
|
|
2405
|
-
|
|
2406
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2407
|
-
const transactionToPromise = transaction => {
|
|
2408
|
-
return new Promise((resolve, reject) => {
|
|
2409
|
-
transaction.addEventListener('complete', () => {
|
|
2410
|
-
resolve();
|
|
2411
|
-
});
|
|
2412
|
-
transaction.addEventListener('abort', () => {
|
|
2413
|
-
reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
|
|
2414
|
-
});
|
|
2415
|
-
transaction.addEventListener('error', () => {
|
|
2416
|
-
reject(transaction.error ?? new Error('Persistent storage transaction failed.'));
|
|
2417
|
-
});
|
|
2418
|
-
});
|
|
2419
|
-
};
|
|
2420
|
-
const getDatabase = async () => {
|
|
2421
|
-
if (typeof indexedDB === 'undefined') {
|
|
2422
|
-
return undefined;
|
|
2423
|
-
}
|
|
2424
|
-
if (!databasePromise) {
|
|
2425
|
-
databasePromise = new Promise((resolve, reject) => {
|
|
2426
|
-
const request = indexedDB.open(databaseName, 1);
|
|
2427
|
-
request.addEventListener('upgradeneeded', () => {
|
|
2428
|
-
const database = request.result;
|
|
2429
|
-
if (!database.objectStoreNames.contains(objectStoreName)) {
|
|
2430
|
-
database.createObjectStore(objectStoreName);
|
|
2431
|
-
}
|
|
2432
|
-
});
|
|
2433
|
-
request.addEventListener('success', () => {
|
|
2434
|
-
resolve(request.result);
|
|
2435
|
-
});
|
|
2436
|
-
request.addEventListener('error', () => {
|
|
2437
|
-
reject(request.error ?? new Error('Failed to open persistent auth storage.'));
|
|
2438
|
-
});
|
|
2439
|
-
});
|
|
2440
|
-
}
|
|
2441
|
-
return databasePromise;
|
|
2442
|
-
};
|
|
2443
|
-
const setPersistentAuthValue = async (key, value) => {
|
|
2444
|
-
memoryStorage.set(key, value);
|
|
2445
|
-
const database = await getDatabase();
|
|
2446
|
-
if (!database) {
|
|
2447
|
-
return;
|
|
2448
|
-
}
|
|
2449
|
-
const transaction = database.transaction(objectStoreName, 'readwrite');
|
|
2450
|
-
const objectStore = transaction.objectStore(objectStoreName);
|
|
2451
|
-
objectStore.put(value, key);
|
|
2452
|
-
await transactionToPromise(transaction);
|
|
2453
|
-
};
|
|
2454
|
-
|
|
2455
|
-
const persistLoginResult = async loginResult => {
|
|
2456
|
-
if (loginResult.userState !== 'loggedIn') {
|
|
2457
|
-
return loginResult;
|
|
2458
|
-
}
|
|
2459
|
-
await setPersistentAuthValue('accessToken', loginResult.authAccessToken ?? '');
|
|
2460
|
-
await setPersistentAuthValue('refreshToken', loginResult.authRefreshToken ?? '');
|
|
2461
|
-
return loginResult;
|
|
2462
|
-
};
|
|
2463
|
-
|
|
2464
|
-
const getBackendOidcTokenUrl = backendUrl => {
|
|
2465
|
-
return getBackendAuthUrl(backendUrl, '/oidc/token');
|
|
2466
|
-
};
|
|
2467
|
-
|
|
2468
|
-
const getAuthorizationServer = backendUrl => {
|
|
2469
|
-
return {
|
|
2470
|
-
issuer: getBackendAuthUrl(backendUrl, '/oidc'),
|
|
2471
|
-
jwks_uri: getBackendAuthUrl(backendUrl, '/oidc/jwks'),
|
|
2472
|
-
token_endpoint: getBackendOidcTokenUrl(backendUrl)
|
|
2473
|
-
};
|
|
2474
|
-
};
|
|
2475
|
-
const getClient = () => {
|
|
2476
|
-
return {
|
|
2477
|
-
client_id: oidcClientId
|
|
2478
|
-
};
|
|
2479
|
-
};
|
|
2480
2826
|
const exchangeElectronAuthorizationCode = async (backendUrl, code, redirectUri, codeVerifier, requestTokenEndpoint = genericTokenEndpointRequest, processTokenEndpointResponse = processGenericTokenEndpointResponse) => {
|
|
2481
|
-
|
|
2482
|
-
const client = getClient();
|
|
2483
|
-
const response = await requestTokenEndpoint(authorizationServer, client, None(), 'authorization_code', new URLSearchParams({
|
|
2484
|
-
code,
|
|
2485
|
-
code_verifier: codeVerifier,
|
|
2486
|
-
redirect_uri: redirectUri
|
|
2487
|
-
}));
|
|
2488
|
-
const tokenResponse = await processTokenEndpointResponse(authorizationServer, client, response);
|
|
2489
|
-
return {
|
|
2490
|
-
accessToken: tokenResponse.access_token,
|
|
2491
|
-
refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : ''
|
|
2492
|
-
};
|
|
2827
|
+
return exchangeAuthorizationCode(backendUrl, nativeOidcClientId, code, redirectUri, codeVerifier, requestTokenEndpoint, processTokenEndpointResponse);
|
|
2493
2828
|
};
|
|
2494
2829
|
|
|
2495
2830
|
const hasAuthorizationCode = value => {
|
|
@@ -2516,6 +2851,55 @@ const waitForElectronBackendLogin = async (backendUrl, uid, redirectUri, codeVer
|
|
|
2516
2851
|
return getLoggedOutBackendAuthState('Timed out waiting for backend login.');
|
|
2517
2852
|
};
|
|
2518
2853
|
|
|
2854
|
+
const getMockLoginResult = async signingInState => {
|
|
2855
|
+
if (!hasPendingMockLoginResponse()) {
|
|
2856
|
+
return undefined;
|
|
2857
|
+
}
|
|
2858
|
+
const response = await consumeNextLoginResponse();
|
|
2859
|
+
if (!isLoginResponse(response)) {
|
|
2860
|
+
return {
|
|
2861
|
+
authErrorMessage: 'Backend returned an invalid login response.',
|
|
2862
|
+
userState: 'loggedOut'
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
if (typeof response.error === 'string' && response.error) {
|
|
2866
|
+
return {
|
|
2867
|
+
authErrorMessage: response.error,
|
|
2868
|
+
userState: 'loggedOut'
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
return persistLoginResult(getLoggedInState(signingInState, response));
|
|
2872
|
+
};
|
|
2873
|
+
const getInteractiveLoginResult = async (backendUrl, platform, authUseRedirect, signingInState) => {
|
|
2874
|
+
const uid = 0;
|
|
2875
|
+
const {
|
|
2876
|
+
clientId,
|
|
2877
|
+
codeVerifier,
|
|
2878
|
+
loginUrl,
|
|
2879
|
+
redirectUri,
|
|
2880
|
+
state
|
|
2881
|
+
} = await getBackendLoginRequest(backendUrl, platform, uid);
|
|
2882
|
+
if (platform !== Electron) {
|
|
2883
|
+
await savePendingOidcAuthState({
|
|
2884
|
+
clientId,
|
|
2885
|
+
codeVerifier,
|
|
2886
|
+
redirectUri,
|
|
2887
|
+
state
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
await invoke$1('Open.openUrl', loginUrl, platform, authUseRedirect);
|
|
2891
|
+
if (platform !== Electron && authUseRedirect) {
|
|
2892
|
+
return signingInState;
|
|
2893
|
+
}
|
|
2894
|
+
const authState = platform === Electron ? await waitForElectronBackendLogin(backendUrl, uid, redirectUri, codeVerifier) : await waitForBackendLogin(backendUrl);
|
|
2895
|
+
if (platform !== Electron) {
|
|
2896
|
+
await clearPendingOidcAuthState();
|
|
2897
|
+
}
|
|
2898
|
+
return persistLoginResult({
|
|
2899
|
+
...authState,
|
|
2900
|
+
authClientId: clientId
|
|
2901
|
+
});
|
|
2902
|
+
};
|
|
2519
2903
|
const handleClickLogin = async options => {
|
|
2520
2904
|
const {
|
|
2521
2905
|
authUseRedirect,
|
|
@@ -2533,32 +2917,13 @@ const handleClickLogin = async options => {
|
|
|
2533
2917
|
userState: 'loggingIn'
|
|
2534
2918
|
};
|
|
2535
2919
|
try {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
return {
|
|
2540
|
-
authErrorMessage: 'Backend returned an invalid login response.',
|
|
2541
|
-
userState: 'loggedOut'
|
|
2542
|
-
};
|
|
2543
|
-
}
|
|
2544
|
-
if (typeof response.error === 'string' && response.error) {
|
|
2545
|
-
return {
|
|
2546
|
-
authErrorMessage: response.error,
|
|
2547
|
-
userState: 'loggedOut'
|
|
2548
|
-
};
|
|
2549
|
-
}
|
|
2550
|
-
return persistLoginResult(getLoggedInState(signingInState, response));
|
|
2920
|
+
const mockLoginResult = await getMockLoginResult(signingInState);
|
|
2921
|
+
if (mockLoginResult) {
|
|
2922
|
+
return mockLoginResult;
|
|
2551
2923
|
}
|
|
2552
|
-
|
|
2553
|
-
const {
|
|
2554
|
-
codeVerifier,
|
|
2555
|
-
loginUrl,
|
|
2556
|
-
redirectUri
|
|
2557
|
-
} = await getBackendLoginRequest(backendUrl, platform, uid);
|
|
2558
|
-
await invoke$1('Open.openUrl', loginUrl, platform, authUseRedirect);
|
|
2559
|
-
const authState = platform === Electron ? await waitForElectronBackendLogin(backendUrl, uid, redirectUri, codeVerifier) : await waitForBackendLogin(backendUrl);
|
|
2560
|
-
return persistLoginResult(authState);
|
|
2924
|
+
return getInteractiveLoginResult(backendUrl, platform, authUseRedirect, signingInState);
|
|
2561
2925
|
} catch (error) {
|
|
2926
|
+
await clearPendingOidcAuthState();
|
|
2562
2927
|
const errorMessage = error instanceof Error && error.message ? error.message : 'Backend authentication failed.';
|
|
2563
2928
|
return {
|
|
2564
2929
|
...signingInState,
|
|
@@ -2573,6 +2938,7 @@ const logout = async state => {
|
|
|
2573
2938
|
userState: 'loggingOut'
|
|
2574
2939
|
};
|
|
2575
2940
|
await logoutFromBackend(state.backendUrl);
|
|
2941
|
+
await Promise.all([clearOidcCallbackUrl(), clearPendingOidcAuthState(), clearPersistedAuthSession()]);
|
|
2576
2942
|
return {
|
|
2577
2943
|
...loggingOutState,
|
|
2578
2944
|
...getLoggedOutBackendAuthState()
|