General source code refactoring
This commit is contained in:
222
src/auth/consumer-session.js
Normal file
222
src/auth/consumer-session.js
Normal file
@@ -0,0 +1,222 @@
|
||||
function createAuthConsumerSession({
|
||||
CUSTOMER_TOKEN_MAX_ATTEMPTS,
|
||||
CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS,
|
||||
CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS,
|
||||
CUSTOMER_TOKEN_SERVICE_URL,
|
||||
CUSTOMER_TOKEN_SESSION_URL,
|
||||
extractTokenServiceAuthorization,
|
||||
fetchJson,
|
||||
supportsFirefoxCookieStoreTabs,
|
||||
tabsCreate,
|
||||
tabsRemove,
|
||||
wait
|
||||
}) {
|
||||
async function requestConsumerTokenService(sender) {
|
||||
const debugRequests = [];
|
||||
|
||||
try {
|
||||
const tokenResponse = await fetchConsumerTokenServiceUntilJson(sender, debugRequests);
|
||||
const accountSummaryAuthorization = extractTokenServiceAuthorization(
|
||||
tokenResponse,
|
||||
"ACCP-TS",
|
||||
"account-ti-authorization"
|
||||
);
|
||||
const accountPlansAuthorization = extractTokenServiceAuthorization(
|
||||
tokenResponse,
|
||||
"OCICONS-TS",
|
||||
"g2m-authorization"
|
||||
);
|
||||
const tokenRequestDebugEntry = debugRequests[debugRequests.length - 1];
|
||||
|
||||
if (tokenRequestDebugEntry) {
|
||||
tokenRequestDebugEntry.metadata = {
|
||||
...tokenRequestDebugEntry.metadata,
|
||||
accountSummaryTokenService: "ACCP-TS",
|
||||
accountSummaryAuthorizationHeader: accountSummaryAuthorization.name || "(missing)",
|
||||
accountSummaryAuthorizationValue: accountSummaryAuthorization.value
|
||||
? `(present, ${accountSummaryAuthorization.value.length} characters)`
|
||||
: "(missing)",
|
||||
accountSummaryAuthorizationExpires: accountSummaryAuthorization.expires || "(not provided)",
|
||||
accountPlansTokenService: "OCICONS-TS",
|
||||
accountPlansAuthorizationHeader: accountPlansAuthorization.name || "(missing)",
|
||||
accountPlansAuthorizationValue: accountPlansAuthorization.value
|
||||
? `(present, ${accountPlansAuthorization.value.length} characters)`
|
||||
: "(missing)",
|
||||
accountPlansAuthorizationExpires: accountPlansAuthorization.expires || "(not provided)"
|
||||
};
|
||||
}
|
||||
|
||||
if (!accountSummaryAuthorization.value) {
|
||||
throw new Error("ACCP-TS uiaasHeader.value was not found in requestConsumerTokenService.");
|
||||
}
|
||||
|
||||
if (!accountPlansAuthorization.value) {
|
||||
throw new Error("OCICONS-TS uiaasHeader.value was not found in requestConsumerTokenService.");
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
authorizations: {
|
||||
accountSummary: accountSummaryAuthorization,
|
||||
accountPlans: accountPlansAuthorization
|
||||
},
|
||||
debugRequests
|
||||
};
|
||||
} catch (error) {
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchConsumerTokenServiceUntilJson(sender, debugRequests) {
|
||||
let attempt = 0;
|
||||
let sessionTab = null;
|
||||
let receivedValidJson = false;
|
||||
|
||||
try {
|
||||
while (attempt < CUSTOMER_TOKEN_MAX_ATTEMPTS) {
|
||||
attempt += 1;
|
||||
|
||||
try {
|
||||
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Cache-Control": "no-cache, no-store, max-age=0",
|
||||
Pragma: "no-cache"
|
||||
}
|
||||
}, "requestConsumerTokenService", debugRequests);
|
||||
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||
|
||||
if (debugEntry) {
|
||||
debugEntry.metadata.attempt = String(attempt);
|
||||
}
|
||||
|
||||
receivedValidJson = true;
|
||||
return tokenResponse;
|
||||
} catch (error) {
|
||||
const isHtmlFallback = error
|
||||
&& error.code === "INVALID_JSON_RESPONSE"
|
||||
&& error.httpStatus === 200;
|
||||
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||
|
||||
if (!isHtmlFallback || attempt >= CUSTOMER_TOKEN_MAX_ATTEMPTS) {
|
||||
if (debugEntry) {
|
||||
debugEntry.metadata.attempt = String(attempt);
|
||||
if (isHtmlFallback) {
|
||||
debugEntry.metadata.retryLimitReached = "true";
|
||||
debugEntry.error = `requestConsumerTokenService returned invalid JSON after ${CUSTOMER_TOKEN_MAX_ATTEMPTS} attempts.`;
|
||||
error.message = debugEntry.error;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!sessionTab) {
|
||||
try {
|
||||
sessionTab = await openConsumerTokenSessionTabWithRetry(sender, debugEntry);
|
||||
if (debugEntry) {
|
||||
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
|
||||
debugEntry.metadata.sessionBootstrapState = "opened-inactive-tab";
|
||||
}
|
||||
} catch (sessionError) {
|
||||
if (debugEntry) {
|
||||
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
|
||||
debugEntry.metadata.sessionBootstrapState = `tab-open-failed: ${sessionError.message || "unknown error"}`;
|
||||
}
|
||||
}
|
||||
} else if (debugEntry) {
|
||||
debugEntry.metadata.sessionBootstrapState = "waiting-for-valid-session";
|
||||
}
|
||||
|
||||
const retryDelayMs = getConsumerTokenRetryDelay(attempt);
|
||||
if (debugEntry) {
|
||||
debugEntry.metadata.attempt = String(attempt);
|
||||
debugEntry.metadata.retryReason = "HTTP 200 response did not contain valid JSON";
|
||||
debugEntry.metadata.nextRetryDelayMs = String(retryDelayMs);
|
||||
}
|
||||
|
||||
await wait(retryDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("requestConsumerTokenService retry loop ended unexpectedly.");
|
||||
} finally {
|
||||
if (sessionTab && Number.isInteger(sessionTab.id)) {
|
||||
const debugEntry = debugRequests[debugRequests.length - 1];
|
||||
|
||||
try {
|
||||
await tabsRemove(sessionTab.id);
|
||||
if (debugEntry) {
|
||||
debugEntry.metadata.sessionBootstrapState = receivedValidJson
|
||||
? "closed-after-valid-json"
|
||||
: "closed-after-retry-finished";
|
||||
}
|
||||
} catch (error) {
|
||||
if (debugEntry) {
|
||||
debugEntry.metadata.sessionBootstrapState = `tab-close-failed: ${error.message || "unknown error"}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getConsumerTokenRetryDelay(attempt) {
|
||||
return Math.min(
|
||||
CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS * (2 ** Math.max(attempt - 1, 0)),
|
||||
CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS
|
||||
);
|
||||
}
|
||||
|
||||
async function openConsumerTokenSessionTabWithRetry(sender, debugEntry) {
|
||||
let attempt = 0;
|
||||
|
||||
// Opening failures must not consume the token service's five attempts.
|
||||
while (true) {
|
||||
attempt += 1;
|
||||
if (debugEntry) {
|
||||
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
|
||||
debugEntry.metadata.sessionTabOpenAttempt = String(attempt);
|
||||
}
|
||||
|
||||
try {
|
||||
const tab = await openConsumerTokenSessionTab(sender);
|
||||
if (debugEntry) {
|
||||
debugEntry.metadata.sessionBootstrapState = "opened-inactive-tab";
|
||||
delete debugEntry.metadata.sessionTabNextRetryDelayMs;
|
||||
}
|
||||
return tab;
|
||||
} catch (error) {
|
||||
const retryDelayMs = getConsumerTokenRetryDelay(Math.min(attempt, 5));
|
||||
if (debugEntry) {
|
||||
debugEntry.metadata.sessionBootstrapState = "retrying-tab-open";
|
||||
debugEntry.metadata.sessionTabLastOpenError = error.message || "unknown error";
|
||||
debugEntry.metadata.sessionTabNextRetryDelayMs = String(retryDelayMs);
|
||||
}
|
||||
await wait(retryDelayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openConsumerTokenSessionTab(sender) {
|
||||
const details = {
|
||||
url: CUSTOMER_TOKEN_SESSION_URL,
|
||||
active: false
|
||||
};
|
||||
|
||||
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||
details.windowId = sender.tab.windowId;
|
||||
}
|
||||
|
||||
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||
details.cookieStoreId = sender.tab.cookieStoreId;
|
||||
}
|
||||
|
||||
return tabsCreate(details);
|
||||
}
|
||||
|
||||
return { requestConsumerTokenService, fetchConsumerTokenServiceUntilJson, getConsumerTokenRetryDelay, openConsumerTokenSessionTabWithRetry, openConsumerTokenSessionTab };
|
||||
}
|
||||
57
src/auth/content-session.js
Normal file
57
src/auth/content-session.js
Normal file
@@ -0,0 +1,57 @@
|
||||
function createAuthContentSession({
|
||||
messageTypes,
|
||||
AUTH_STATUS,
|
||||
appendBackgroundRequestLog,
|
||||
authSessionState,
|
||||
requestTokenRelay,
|
||||
sendRuntimeMessage,
|
||||
updateAuthBadge
|
||||
}) {
|
||||
function requestTokenRelayOnce() {
|
||||
if (!authSessionState.tokenRelayRequest) {
|
||||
authSessionState.authStatus = AUTH_STATUS.pending;
|
||||
updateAuthBadge(authSessionState.authStatus);
|
||||
authSessionState.tokenRelayRequest = requestTokenRelay()
|
||||
.then((token) => {
|
||||
authSessionState.accessToken = token;
|
||||
authSessionState.authStatus = AUTH_STATUS.authenticated;
|
||||
requestConsumerTokenServiceOnce().catch(() => {});
|
||||
return token;
|
||||
})
|
||||
.catch((error) => {
|
||||
authSessionState.accessToken = "";
|
||||
authSessionState.authStatus = AUTH_STATUS.unauthenticated;
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
updateAuthBadge(authSessionState.authStatus);
|
||||
});
|
||||
} else {
|
||||
updateAuthBadge(authSessionState.authStatus);
|
||||
}
|
||||
|
||||
return authSessionState.tokenRelayRequest;
|
||||
}
|
||||
|
||||
function requestConsumerTokenServiceOnce() {
|
||||
if (!authSessionState.consumerTokenServiceRequest) {
|
||||
authSessionState.consumerTokenServiceRequest = sendRuntimeMessage({
|
||||
type: messageTypes.requestConsumerTokenService
|
||||
}).then((response) => {
|
||||
appendBackgroundRequestLog(response && response.debugRequests);
|
||||
|
||||
if (!response || !response.ok) {
|
||||
throw new Error(response && response.error
|
||||
? response.error
|
||||
: "Unable to load consumer token service.");
|
||||
}
|
||||
|
||||
return response.authorizations;
|
||||
});
|
||||
}
|
||||
|
||||
return authSessionState.consumerTokenServiceRequest;
|
||||
}
|
||||
|
||||
return { requestTokenRelayOnce, requestConsumerTokenServiceOnce };
|
||||
}
|
||||
7
src/auth/sales-common-state.js
Normal file
7
src/auth/sales-common-state.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/** @returns {{salesCommonToken: null | {access_token: string, expiresAt: number}, salesCommonTokenRequest: null | Promise<string>}} */
|
||||
function createAuthSalesCommonState() {
|
||||
return {
|
||||
salesCommonToken: null,
|
||||
salesCommonTokenRequest: null
|
||||
};
|
||||
}
|
||||
33
src/auth/sales-common.js
Normal file
33
src/auth/sales-common.js
Normal file
@@ -0,0 +1,33 @@
|
||||
function createAuthSalesCommon({
|
||||
TOKEN_RELAY_URL,
|
||||
authSalesCommonState,
|
||||
requestTokenRelay
|
||||
}) {
|
||||
function requestTokenRelaySalesCommon() {
|
||||
if (authSalesCommonState.salesCommonToken && Date.now() < authSalesCommonState.salesCommonToken.expiresAt) {
|
||||
return Promise.resolve(authSalesCommonState.salesCommonToken.access_token);
|
||||
}
|
||||
if (authSalesCommonState.salesCommonTokenRequest) {
|
||||
return authSalesCommonState.salesCommonTokenRequest;
|
||||
}
|
||||
authSalesCommonState.salesCommonTokenRequest = requestTokenRelay({
|
||||
name: "requestTokenRelaySalesCommon",
|
||||
url: `${TOKEN_RELAY_URL}?scopesfq=urn%3Aopc%3Aresource%3Afusion%3Aeeho%3Asales-common%2F`,
|
||||
includeExpiry: true
|
||||
}).then((token) => {
|
||||
authSalesCommonState.salesCommonToken = token;
|
||||
return token.access_token;
|
||||
}).finally(() => {
|
||||
authSalesCommonState.salesCommonTokenRequest = null;
|
||||
});
|
||||
return authSalesCommonState.salesCommonTokenRequest;
|
||||
}
|
||||
|
||||
function invalidateSalesCommonToken(rejectedToken) {
|
||||
if (authSalesCommonState.salesCommonToken && authSalesCommonState.salesCommonToken.access_token === rejectedToken) {
|
||||
authSalesCommonState.salesCommonToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
return { requestTokenRelaySalesCommon, invalidateSalesCommonToken };
|
||||
}
|
||||
8
src/auth/session-state.js
Normal file
8
src/auth/session-state.js
Normal file
@@ -0,0 +1,8 @@
|
||||
function createAuthSessionState({ AUTH_STATUS } = {}) {
|
||||
return {
|
||||
tokenRelayRequest: null,
|
||||
consumerTokenServiceRequest: null,
|
||||
accessToken: "",
|
||||
authStatus: AUTH_STATUS.idle
|
||||
};
|
||||
}
|
||||
40
src/auth/spa-credentials.js
Normal file
40
src/auth/spa-credentials.js
Normal file
@@ -0,0 +1,40 @@
|
||||
function createAuthSpaCredentials({
|
||||
findNestedObject
|
||||
}) {
|
||||
function normalizeMessageAuthorization(authorization, fallbackHeaderName) {
|
||||
const headerName = authorization && typeof authorization.name === "string"
|
||||
? authorization.name.trim()
|
||||
: "";
|
||||
const headerValue = authorization && typeof authorization.value === "string"
|
||||
? authorization.value.trim()
|
||||
: "";
|
||||
|
||||
return {
|
||||
name: headerName || fallbackHeaderName,
|
||||
value: headerValue
|
||||
};
|
||||
}
|
||||
|
||||
function extractTokenServiceAuthorization(response, tokenServiceName, fallbackHeaderName) {
|
||||
const tokenService = Array.isArray(response)
|
||||
? response.find((entry) => entry && entry.tokenService === tokenServiceName)
|
||||
: findNestedObject(response, (entry) => entry.tokenService === tokenServiceName);
|
||||
const authorizationHeader = tokenService && tokenService.uiaasHeader;
|
||||
const headerName = authorizationHeader && typeof authorizationHeader.label === "string"
|
||||
? authorizationHeader.label.trim()
|
||||
: "";
|
||||
const headerValue = authorizationHeader && typeof authorizationHeader.value === "string"
|
||||
? authorizationHeader.value.trim()
|
||||
: "";
|
||||
|
||||
return {
|
||||
name: headerName || fallbackHeaderName,
|
||||
value: headerValue,
|
||||
expires: authorizationHeader && authorizationHeader.expires
|
||||
? String(authorizationHeader.expires)
|
||||
: ""
|
||||
};
|
||||
}
|
||||
|
||||
return { normalizeMessageAuthorization, extractTokenServiceAuthorization };
|
||||
}
|
||||
153
src/auth/token-relay.js
Normal file
153
src/auth/token-relay.js
Normal file
@@ -0,0 +1,153 @@
|
||||
function createAuthTokenRelay({
|
||||
TOKEN_RELAY_URL,
|
||||
createDebugBody,
|
||||
getXsrfToken,
|
||||
logRequestFailure,
|
||||
logRequestStart,
|
||||
logRequestSuccess,
|
||||
maskToken,
|
||||
readResponseBodyForDebug,
|
||||
refreshXsrfCookie,
|
||||
renderDebugPanel,
|
||||
environment
|
||||
}) {
|
||||
const { fetch } = environment;
|
||||
async function requestTokenRelay({ name = "requestTokenRelay", url = TOKEN_RELAY_URL, includeExpiry = false } = {}) {
|
||||
let refreshResult = null;
|
||||
let refreshAttempted = false;
|
||||
|
||||
while (true) {
|
||||
const xsrfTokenSource = await getXsrfToken();
|
||||
|
||||
if (!xsrfTokenSource.token) {
|
||||
if (refreshAttempted) {
|
||||
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult, name, url);
|
||||
logRequestFailure(requestEntry, "XSRF token cookie not found after automatic refresh.");
|
||||
throw new Error("XSRF token cookie not found after automatic refresh.");
|
||||
}
|
||||
|
||||
refreshResult = await refreshXsrfCookie("missing-cookie");
|
||||
refreshAttempted = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult, name, url);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-xsrf-token": xsrfTokenSource.token
|
||||
}
|
||||
});
|
||||
|
||||
const responseBody = await readResponseBodyForDebug(response);
|
||||
|
||||
if (!response.ok) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
|
||||
if (!refreshAttempted && isInvalidXsrfStatus(response.status)) {
|
||||
refreshResult = await refreshXsrfCookie(`tokenrelay-${response.status}`);
|
||||
refreshAttempted = true;
|
||||
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||
renderDebugPanel();
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Token relay failed with status ${response.status}.`);
|
||||
}
|
||||
|
||||
let responseData;
|
||||
|
||||
try {
|
||||
responseData = await response.json();
|
||||
} catch (error) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
|
||||
if (!refreshAttempted) {
|
||||
refreshResult = await refreshXsrfCookie("tokenrelay-invalid-response");
|
||||
refreshAttempted = true;
|
||||
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||
renderDebugPanel();
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
|
||||
if (!refreshAttempted) {
|
||||
refreshResult = await refreshXsrfCookie("tokenrelay-missing-access-token");
|
||||
refreshAttempted = true;
|
||||
requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult);
|
||||
renderDebugPanel();
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error("Token relay response does not contain access_token.");
|
||||
}
|
||||
|
||||
logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({
|
||||
...responseData,
|
||||
access_token: maskToken(responseData.access_token)
|
||||
}, null, 2)));
|
||||
|
||||
if (includeExpiry) {
|
||||
const lifetime = Number(responseData.expires_in);
|
||||
return {
|
||||
access_token: responseData.access_token,
|
||||
expiresAt: Number.isFinite(lifetime) && lifetime > 0
|
||||
? requestEntry.startedAt.getTime() + lifetime * 1000 : 0
|
||||
};
|
||||
}
|
||||
return responseData.access_token;
|
||||
} catch (error) {
|
||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult, name = "requestTokenRelay", url = TOKEN_RELAY_URL) {
|
||||
return logRequestStart({
|
||||
name,
|
||||
method: "GET",
|
||||
url,
|
||||
headers: {
|
||||
"x-xsrf-token": xsrfTokenSource.token ? maskToken(xsrfTokenSource.token) : "(missing)"
|
||||
},
|
||||
metadata: {
|
||||
xsrfCookieName: xsrfTokenSource.cookieName || "(missing)",
|
||||
xsrfTokenSource: xsrfTokenSource.source,
|
||||
xsrfMatchedCookies: xsrfTokenSource.matchedCookieNames && xsrfTokenSource.matchedCookieNames.length
|
||||
? xsrfTokenSource.matchedCookieNames.join(", ")
|
||||
: "(none)",
|
||||
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
||||
? xsrfTokenSource.lookupDetails.join(" | ")
|
||||
: "(none)",
|
||||
xsrfTokenError: xsrfTokenSource.error || "",
|
||||
xsrfRefresh: formatXsrfRefreshResult(refreshResult)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isInvalidXsrfStatus(status) {
|
||||
return status === 400 || status === 401 || status === 403;
|
||||
}
|
||||
|
||||
function formatXsrfRefreshResult(result) {
|
||||
if (!result) {
|
||||
return "not-requested";
|
||||
}
|
||||
|
||||
const outcome = result.refreshed ? "refreshed" : "not-refreshed";
|
||||
const cookieName = result.cookieName || "(none)";
|
||||
const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown";
|
||||
return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim();
|
||||
}
|
||||
|
||||
return { requestTokenRelay, createTokenRelayRequestEntry, isInvalidXsrfStatus, formatXsrfRefreshResult };
|
||||
}
|
||||
42
src/auth/xsrf-content.js
Normal file
42
src/auth/xsrf-content.js
Normal file
@@ -0,0 +1,42 @@
|
||||
function createAuthXsrfContent({
|
||||
getXsrfTokenFromExtensionCookies,
|
||||
environment
|
||||
}) {
|
||||
const { document } = environment;
|
||||
async function getXsrfToken() {
|
||||
const extensionCookie = await getXsrfTokenFromExtensionCookies();
|
||||
|
||||
if (extensionCookie.token) {
|
||||
return extensionCookie;
|
||||
}
|
||||
|
||||
const xsrfCookie = document.cookie
|
||||
.split(";")
|
||||
.map((cookie) => cookie.trim())
|
||||
.find((cookie) => cookie.startsWith("XSRF-TOKEN-"));
|
||||
|
||||
if (!xsrfCookie) {
|
||||
return {
|
||||
token: "",
|
||||
cookieName: "",
|
||||
source: extensionCookie.error ? "document.cookie fallback after cookies API error" : "document.cookie fallback",
|
||||
matchedCookieNames: extensionCookie.matchedCookieNames || [],
|
||||
lookupDetails: extensionCookie.lookupDetails || [],
|
||||
error: extensionCookie.error || ""
|
||||
};
|
||||
}
|
||||
|
||||
const cookieName = xsrfCookie.slice(0, xsrfCookie.indexOf("="));
|
||||
const tokenValue = xsrfCookie.slice(xsrfCookie.indexOf("=") + 1);
|
||||
return {
|
||||
token: decodeURIComponent(tokenValue),
|
||||
cookieName,
|
||||
source: "document.cookie",
|
||||
matchedCookieNames: extensionCookie.matchedCookieNames || [],
|
||||
lookupDetails: extensionCookie.lookupDetails || [],
|
||||
error: ""
|
||||
};
|
||||
}
|
||||
|
||||
return { getXsrfToken };
|
||||
}
|
||||
116
src/auth/xsrf-cookies.js
Normal file
116
src/auth/xsrf-cookies.js
Normal file
@@ -0,0 +1,116 @@
|
||||
function createAuthXsrfCookies({
|
||||
ORACLE_COOKIE_URLS,
|
||||
ORACLE_DOMAIN,
|
||||
XSRF_COOKIE_NAME,
|
||||
XSRF_COOKIE_PREFIX,
|
||||
cookiesGet,
|
||||
cookiesGetAll,
|
||||
cookiesGetAllCookieStores,
|
||||
removeUndefinedValues
|
||||
}) {
|
||||
async function getXsrfTokenCookie() {
|
||||
const lookupDetails = [];
|
||||
const allCookies = [];
|
||||
const stores = await getCookieStores(lookupDetails);
|
||||
|
||||
for (const store of stores) {
|
||||
const exactCookie = await getExactCookieFromUrls(store.id, lookupDetails);
|
||||
|
||||
if (exactCookie) {
|
||||
allCookies.push(exactCookie);
|
||||
}
|
||||
}
|
||||
|
||||
for (const store of stores) {
|
||||
await collectCookies({
|
||||
name: XSRF_COOKIE_NAME,
|
||||
storeId: store.id
|
||||
}, allCookies, lookupDetails);
|
||||
}
|
||||
|
||||
for (const store of stores) {
|
||||
await collectCookies({
|
||||
domain: ORACLE_DOMAIN,
|
||||
storeId: store.id
|
||||
}, allCookies, lookupDetails);
|
||||
|
||||
await collectCookies({
|
||||
domain: `.${ORACLE_DOMAIN}`,
|
||||
storeId: store.id
|
||||
}, allCookies, lookupDetails);
|
||||
}
|
||||
|
||||
for (const store of stores) {
|
||||
for (const url of ORACLE_COOKIE_URLS) {
|
||||
await collectCookies({
|
||||
url,
|
||||
storeId: store.id
|
||||
}, allCookies, lookupDetails);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueCookies = dedupeCookies(allCookies);
|
||||
const xsrfCookies = uniqueCookies.filter((cookie) => cookie.name.startsWith(XSRF_COOKIE_PREFIX));
|
||||
const exactCookie = xsrfCookies.find((cookie) => cookie.name === XSRF_COOKIE_NAME);
|
||||
const hostCookie = xsrfCookies.find((cookie) => cookie.domain === ORACLE_DOMAIN || cookie.domain === `.${ORACLE_DOMAIN}`);
|
||||
|
||||
return {
|
||||
cookie: exactCookie || hostCookie || xsrfCookies[0] || null,
|
||||
matchedCookieNames: xsrfCookies.map((cookie) => `${cookie.name} (${cookie.domain}${cookie.path})`),
|
||||
lookupDetails
|
||||
};
|
||||
}
|
||||
|
||||
async function getExactCookieFromUrls(storeId, lookupDetails) {
|
||||
for (const url of ORACLE_COOKIE_URLS) {
|
||||
const cookie = await cookiesGet({
|
||||
url,
|
||||
name: XSRF_COOKIE_NAME,
|
||||
storeId
|
||||
});
|
||||
|
||||
lookupDetails.push(`${JSON.stringify({ url, name: XSRF_COOKIE_NAME, storeId })} => ${cookie ? "found" : "not found"}`);
|
||||
|
||||
if (cookie) {
|
||||
return cookie;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getCookieStores(lookupDetails) {
|
||||
try {
|
||||
const stores = await cookiesGetAllCookieStores();
|
||||
lookupDetails.push(`getAllCookieStores => ${stores.length} store(s)`);
|
||||
return stores.length ? stores : [{ id: undefined }];
|
||||
} catch (error) {
|
||||
lookupDetails.push(`getAllCookieStores => unavailable (${error.message || "unknown error"})`);
|
||||
return [{ id: undefined }];
|
||||
}
|
||||
}
|
||||
|
||||
async function collectCookies(details, target, lookupDetails) {
|
||||
const cleanDetails = removeUndefinedValues(details);
|
||||
const cookies = await cookiesGetAll(cleanDetails);
|
||||
target.push(...cookies);
|
||||
lookupDetails.push(`${JSON.stringify(cleanDetails)} => ${cookies.length} cookie(s)`);
|
||||
}
|
||||
|
||||
function dedupeCookies(cookies) {
|
||||
const seen = new Set();
|
||||
|
||||
return cookies.filter((cookie) => {
|
||||
const key = `${cookie.name}|${cookie.domain}|${cookie.path}|${cookie.storeId || ""}`;
|
||||
|
||||
if (seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return { getXsrfTokenCookie, getExactCookieFromUrls, getCookieStores, collectCookies, dedupeCookies };
|
||||
}
|
||||
84
src/auth/xsrf-recovery.js
Normal file
84
src/auth/xsrf-recovery.js
Normal file
@@ -0,0 +1,84 @@
|
||||
function createAuthXsrfRecovery({
|
||||
COOKIE_REFRESH_POLL_MS,
|
||||
COOKIE_REFRESH_TIMEOUT_MS,
|
||||
OPPORTUNITIES_LIST_URL,
|
||||
getXsrfTokenCookie,
|
||||
supportsFirefoxCookieStoreTabs,
|
||||
tabsCreate,
|
||||
tabsRemove,
|
||||
wait
|
||||
}) {
|
||||
async function refreshXsrfCookie(sender) {
|
||||
const startedAt = Date.now();
|
||||
const previous = (await getXsrfTokenCookie()).cookie;
|
||||
const createProperties = {
|
||||
url: OPPORTUNITIES_LIST_URL,
|
||||
active: false
|
||||
};
|
||||
|
||||
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
|
||||
createProperties.windowId = sender.tab.windowId;
|
||||
}
|
||||
|
||||
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
|
||||
createProperties.cookieStoreId = sender.tab.cookieStoreId;
|
||||
}
|
||||
|
||||
let refreshTab;
|
||||
|
||||
try {
|
||||
refreshTab = await tabsCreate(createProperties);
|
||||
const current = await waitForXsrfCookieChange(previous);
|
||||
const refreshed = hasCookieChanged(previous, current);
|
||||
|
||||
return {
|
||||
ok: Boolean(current),
|
||||
refreshed,
|
||||
cookieName: current ? current.name : "",
|
||||
durationMs: Date.now() - startedAt,
|
||||
error: refreshed ? "" : "XSRF cookie was not created or changed before timeout."
|
||||
};
|
||||
} finally {
|
||||
if (refreshTab && Number.isInteger(refreshTab.id)) {
|
||||
try {
|
||||
await tabsRemove(refreshTab.id);
|
||||
} catch (error) {
|
||||
// The temporary tab may already have been closed.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForXsrfCookieChange(previousCookie) {
|
||||
const deadline = Date.now() + COOKIE_REFRESH_TIMEOUT_MS;
|
||||
let currentCookie = null;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
currentCookie = (await getXsrfTokenCookie()).cookie;
|
||||
|
||||
if (hasCookieChanged(previousCookie, currentCookie)) {
|
||||
return currentCookie;
|
||||
}
|
||||
|
||||
await wait(COOKIE_REFRESH_POLL_MS);
|
||||
}
|
||||
|
||||
return currentCookie;
|
||||
}
|
||||
|
||||
function hasCookieChanged(previousCookie, currentCookie) {
|
||||
if (!currentCookie || !currentCookie.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!previousCookie) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return previousCookie.name !== currentCookie.name
|
||||
|| previousCookie.value !== currentCookie.value
|
||||
|| previousCookie.expirationDate !== currentCookie.expirationDate;
|
||||
}
|
||||
|
||||
return { refreshXsrfCookie, waitForXsrfCookieChange, hasCookieChanged };
|
||||
}
|
||||
Reference in New Issue
Block a user