Adjustment to API authentication with fallback in case the cookie is invalid or missing
This commit is contained in:
146
dist/chromium/background.js
vendored
146
dist/chromium/background.js
vendored
@@ -10,13 +10,17 @@
|
||||
];
|
||||
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
||||
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
||||
const OPPORTUNITIES_LIST_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-list";
|
||||
const COOKIE_REFRESH_TIMEOUT_MS = 15000;
|
||||
const COOKIE_REFRESH_POLL_MS = 250;
|
||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||
|
||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") {
|
||||
if (!message || !message.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.type === "opportunitiesExtension.getXsrfToken") {
|
||||
getXsrfTokenCookie()
|
||||
.then((result) => {
|
||||
sendResponse({
|
||||
@@ -39,7 +43,147 @@
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === "opportunitiesExtension.refreshXsrfCookie") {
|
||||
refreshXsrfCookie(sender)
|
||||
.then(sendResponse)
|
||||
.catch((error) => {
|
||||
sendResponse({
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
error: error.message || "Unable to refresh XSRF cookie."
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function supportsFirefoxCookieStoreTabs() {
|
||||
return Boolean(
|
||||
runtimeApi.runtime
|
||||
&& typeof runtimeApi.runtime.getBrowserInfo === "function"
|
||||
);
|
||||
}
|
||||
|
||||
function tabsCreate(details) {
|
||||
if (runtimeApi.tabs.create.length <= 1) {
|
||||
return runtimeApi.tabs.create(details);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
runtimeApi.tabs.create(details, (tab) => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
reject(new Error(lastError.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(tab);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function tabsRemove(tabId) {
|
||||
if (runtimeApi.tabs.remove.length <= 1) {
|
||||
return runtimeApi.tabs.remove(tabId);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
runtimeApi.tabs.remove(tabId, () => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
reject(new Error(lastError.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function wait(durationMs) {
|
||||
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||
}
|
||||
|
||||
async function getXsrfTokenCookie() {
|
||||
const lookupDetails = [];
|
||||
|
||||
191
dist/chromium/content.js
vendored
191
dist/chromium/content.js
vendored
@@ -502,8 +502,98 @@
|
||||
}
|
||||
|
||||
async function requestTokenRelay() {
|
||||
let refreshResult = null;
|
||||
let refreshAttempted = false;
|
||||
|
||||
while (true) {
|
||||
const xsrfTokenSource = await getXsrfToken();
|
||||
const requestEntry = logRequestStart({
|
||||
|
||||
if (!xsrfTokenSource.token) {
|
||||
if (refreshAttempted) {
|
||||
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||
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);
|
||||
|
||||
try {
|
||||
const response = await fetch(TOKEN_RELAY_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)));
|
||||
|
||||
return responseData.access_token;
|
||||
} catch (error) {
|
||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult) {
|
||||
return logRequestStart({
|
||||
name: "requestTokenRelay",
|
||||
method: "GET",
|
||||
url: TOKEN_RELAY_URL,
|
||||
@@ -519,56 +609,25 @@
|
||||
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
||||
? xsrfTokenSource.lookupDetails.join(" | ")
|
||||
: "(none)",
|
||||
xsrfTokenError: xsrfTokenSource.error || ""
|
||||
xsrfTokenError: xsrfTokenSource.error || "",
|
||||
xsrfRefresh: formatXsrfRefreshResult(refreshResult)
|
||||
}
|
||||
});
|
||||
|
||||
if (!xsrfTokenSource.token) {
|
||||
logRequestFailure(requestEntry, "XSRF token cookie not found.");
|
||||
throw new Error("XSRF token cookie not found.");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(TOKEN_RELAY_URL, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-xsrf-token": xsrfTokenSource.token
|
||||
}
|
||||
});
|
||||
|
||||
const responseBody = await readResponseBodyForDebug(response);
|
||||
|
||||
if (!response.ok) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
throw new Error(`Token relay failed with status ${response.status}.`);
|
||||
function isInvalidXsrfStatus(status) {
|
||||
return status === 400 || status === 401 || status === 403;
|
||||
}
|
||||
|
||||
let responseData;
|
||||
|
||||
try {
|
||||
responseData = await response.json();
|
||||
} catch (error) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
throw error;
|
||||
function formatXsrfRefreshResult(result) {
|
||||
if (!result) {
|
||||
return "not-requested";
|
||||
}
|
||||
|
||||
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
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)));
|
||||
|
||||
return responseData.access_token;
|
||||
} catch (error) {
|
||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||
|
||||
throw error;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
async function requestOpportunitiesForPeriod(period) {
|
||||
@@ -2106,6 +2165,52 @@
|
||||
});
|
||||
}
|
||||
|
||||
function refreshXsrfCookie(reason) {
|
||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||
|
||||
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
reason,
|
||||
error: "Runtime messaging API unavailable."
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
runtimeApi.runtime.sendMessage({
|
||||
type: "opportunitiesExtension.refreshXsrfCookie"
|
||||
}, (response) => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
resolve({
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
reason,
|
||||
error: lastError.message
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({
|
||||
...(response || {
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
error: "Empty refresh response."
|
||||
}),
|
||||
reason
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateAuthBadge(status) {
|
||||
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
||||
|
||||
|
||||
146
dist/firefox/background.js
vendored
146
dist/firefox/background.js
vendored
@@ -10,13 +10,17 @@
|
||||
];
|
||||
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
||||
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
||||
const OPPORTUNITIES_LIST_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-list";
|
||||
const COOKIE_REFRESH_TIMEOUT_MS = 15000;
|
||||
const COOKIE_REFRESH_POLL_MS = 250;
|
||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||
|
||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") {
|
||||
if (!message || !message.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.type === "opportunitiesExtension.getXsrfToken") {
|
||||
getXsrfTokenCookie()
|
||||
.then((result) => {
|
||||
sendResponse({
|
||||
@@ -39,7 +43,147 @@
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === "opportunitiesExtension.refreshXsrfCookie") {
|
||||
refreshXsrfCookie(sender)
|
||||
.then(sendResponse)
|
||||
.catch((error) => {
|
||||
sendResponse({
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
error: error.message || "Unable to refresh XSRF cookie."
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function supportsFirefoxCookieStoreTabs() {
|
||||
return Boolean(
|
||||
runtimeApi.runtime
|
||||
&& typeof runtimeApi.runtime.getBrowserInfo === "function"
|
||||
);
|
||||
}
|
||||
|
||||
function tabsCreate(details) {
|
||||
if (runtimeApi.tabs.create.length <= 1) {
|
||||
return runtimeApi.tabs.create(details);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
runtimeApi.tabs.create(details, (tab) => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
reject(new Error(lastError.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(tab);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function tabsRemove(tabId) {
|
||||
if (runtimeApi.tabs.remove.length <= 1) {
|
||||
return runtimeApi.tabs.remove(tabId);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
runtimeApi.tabs.remove(tabId, () => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
reject(new Error(lastError.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function wait(durationMs) {
|
||||
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||
}
|
||||
|
||||
async function getXsrfTokenCookie() {
|
||||
const lookupDetails = [];
|
||||
|
||||
191
dist/firefox/content.js
vendored
191
dist/firefox/content.js
vendored
@@ -502,8 +502,98 @@
|
||||
}
|
||||
|
||||
async function requestTokenRelay() {
|
||||
let refreshResult = null;
|
||||
let refreshAttempted = false;
|
||||
|
||||
while (true) {
|
||||
const xsrfTokenSource = await getXsrfToken();
|
||||
const requestEntry = logRequestStart({
|
||||
|
||||
if (!xsrfTokenSource.token) {
|
||||
if (refreshAttempted) {
|
||||
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||
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);
|
||||
|
||||
try {
|
||||
const response = await fetch(TOKEN_RELAY_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)));
|
||||
|
||||
return responseData.access_token;
|
||||
} catch (error) {
|
||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult) {
|
||||
return logRequestStart({
|
||||
name: "requestTokenRelay",
|
||||
method: "GET",
|
||||
url: TOKEN_RELAY_URL,
|
||||
@@ -519,56 +609,25 @@
|
||||
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
||||
? xsrfTokenSource.lookupDetails.join(" | ")
|
||||
: "(none)",
|
||||
xsrfTokenError: xsrfTokenSource.error || ""
|
||||
xsrfTokenError: xsrfTokenSource.error || "",
|
||||
xsrfRefresh: formatXsrfRefreshResult(refreshResult)
|
||||
}
|
||||
});
|
||||
|
||||
if (!xsrfTokenSource.token) {
|
||||
logRequestFailure(requestEntry, "XSRF token cookie not found.");
|
||||
throw new Error("XSRF token cookie not found.");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(TOKEN_RELAY_URL, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-xsrf-token": xsrfTokenSource.token
|
||||
}
|
||||
});
|
||||
|
||||
const responseBody = await readResponseBodyForDebug(response);
|
||||
|
||||
if (!response.ok) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
throw new Error(`Token relay failed with status ${response.status}.`);
|
||||
function isInvalidXsrfStatus(status) {
|
||||
return status === 400 || status === 401 || status === 403;
|
||||
}
|
||||
|
||||
let responseData;
|
||||
|
||||
try {
|
||||
responseData = await response.json();
|
||||
} catch (error) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
throw error;
|
||||
function formatXsrfRefreshResult(result) {
|
||||
if (!result) {
|
||||
return "not-requested";
|
||||
}
|
||||
|
||||
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
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)));
|
||||
|
||||
return responseData.access_token;
|
||||
} catch (error) {
|
||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||
|
||||
throw error;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
async function requestOpportunitiesForPeriod(period) {
|
||||
@@ -2106,6 +2165,52 @@
|
||||
});
|
||||
}
|
||||
|
||||
function refreshXsrfCookie(reason) {
|
||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||
|
||||
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
reason,
|
||||
error: "Runtime messaging API unavailable."
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
runtimeApi.runtime.sendMessage({
|
||||
type: "opportunitiesExtension.refreshXsrfCookie"
|
||||
}, (response) => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
resolve({
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
reason,
|
||||
error: lastError.message
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({
|
||||
...(response || {
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
error: "Empty refresh response."
|
||||
}),
|
||||
reason
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateAuthBadge(status) {
|
||||
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
||||
|
||||
|
||||
@@ -10,13 +10,17 @@
|
||||
];
|
||||
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
|
||||
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-";
|
||||
const OPPORTUNITIES_LIST_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-list";
|
||||
const COOKIE_REFRESH_TIMEOUT_MS = 15000;
|
||||
const COOKIE_REFRESH_POLL_MS = 250;
|
||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||
|
||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") {
|
||||
if (!message || !message.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.type === "opportunitiesExtension.getXsrfToken") {
|
||||
getXsrfTokenCookie()
|
||||
.then((result) => {
|
||||
sendResponse({
|
||||
@@ -39,7 +43,147 @@
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === "opportunitiesExtension.refreshXsrfCookie") {
|
||||
refreshXsrfCookie(sender)
|
||||
.then(sendResponse)
|
||||
.catch((error) => {
|
||||
sendResponse({
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
error: error.message || "Unable to refresh XSRF cookie."
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function supportsFirefoxCookieStoreTabs() {
|
||||
return Boolean(
|
||||
runtimeApi.runtime
|
||||
&& typeof runtimeApi.runtime.getBrowserInfo === "function"
|
||||
);
|
||||
}
|
||||
|
||||
function tabsCreate(details) {
|
||||
if (runtimeApi.tabs.create.length <= 1) {
|
||||
return runtimeApi.tabs.create(details);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
runtimeApi.tabs.create(details, (tab) => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
reject(new Error(lastError.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(tab);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function tabsRemove(tabId) {
|
||||
if (runtimeApi.tabs.remove.length <= 1) {
|
||||
return runtimeApi.tabs.remove(tabId);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
runtimeApi.tabs.remove(tabId, () => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
reject(new Error(lastError.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function wait(durationMs) {
|
||||
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||
}
|
||||
|
||||
async function getXsrfTokenCookie() {
|
||||
const lookupDetails = [];
|
||||
|
||||
191
src/content.js
191
src/content.js
@@ -502,8 +502,98 @@
|
||||
}
|
||||
|
||||
async function requestTokenRelay() {
|
||||
let refreshResult = null;
|
||||
let refreshAttempted = false;
|
||||
|
||||
while (true) {
|
||||
const xsrfTokenSource = await getXsrfToken();
|
||||
const requestEntry = logRequestStart({
|
||||
|
||||
if (!xsrfTokenSource.token) {
|
||||
if (refreshAttempted) {
|
||||
const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult);
|
||||
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);
|
||||
|
||||
try {
|
||||
const response = await fetch(TOKEN_RELAY_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)));
|
||||
|
||||
return responseData.access_token;
|
||||
} catch (error) {
|
||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult) {
|
||||
return logRequestStart({
|
||||
name: "requestTokenRelay",
|
||||
method: "GET",
|
||||
url: TOKEN_RELAY_URL,
|
||||
@@ -519,56 +609,25 @@
|
||||
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
|
||||
? xsrfTokenSource.lookupDetails.join(" | ")
|
||||
: "(none)",
|
||||
xsrfTokenError: xsrfTokenSource.error || ""
|
||||
xsrfTokenError: xsrfTokenSource.error || "",
|
||||
xsrfRefresh: formatXsrfRefreshResult(refreshResult)
|
||||
}
|
||||
});
|
||||
|
||||
if (!xsrfTokenSource.token) {
|
||||
logRequestFailure(requestEntry, "XSRF token cookie not found.");
|
||||
throw new Error("XSRF token cookie not found.");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(TOKEN_RELAY_URL, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"x-xsrf-token": xsrfTokenSource.token
|
||||
}
|
||||
});
|
||||
|
||||
const responseBody = await readResponseBodyForDebug(response);
|
||||
|
||||
if (!response.ok) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
throw new Error(`Token relay failed with status ${response.status}.`);
|
||||
function isInvalidXsrfStatus(status) {
|
||||
return status === 400 || status === 401 || status === 403;
|
||||
}
|
||||
|
||||
let responseData;
|
||||
|
||||
try {
|
||||
responseData = await response.json();
|
||||
} catch (error) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
throw error;
|
||||
function formatXsrfRefreshResult(result) {
|
||||
if (!result) {
|
||||
return "not-requested";
|
||||
}
|
||||
|
||||
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) {
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
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)));
|
||||
|
||||
return responseData.access_token;
|
||||
} catch (error) {
|
||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||
|
||||
throw error;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
async function requestOpportunitiesForPeriod(period) {
|
||||
@@ -2106,6 +2165,52 @@
|
||||
});
|
||||
}
|
||||
|
||||
function refreshXsrfCookie(reason) {
|
||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||
|
||||
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
reason,
|
||||
error: "Runtime messaging API unavailable."
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
runtimeApi.runtime.sendMessage({
|
||||
type: "opportunitiesExtension.refreshXsrfCookie"
|
||||
}, (response) => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
resolve({
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
reason,
|
||||
error: lastError.message
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({
|
||||
...(response || {
|
||||
ok: false,
|
||||
refreshed: false,
|
||||
cookieName: "",
|
||||
durationMs: 0,
|
||||
error: "Empty refresh response."
|
||||
}),
|
||||
reason
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateAuthBadge(status) {
|
||||
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user