Adjustment to API authentication with fallback in case the cookie is invalid or missing

This commit is contained in:
2026-09-02 14:04:33 -03:00
parent dfc84c64ee
commit 9cb58809c8
6 changed files with 951 additions and 204 deletions

View File

@@ -10,13 +10,17 @@
]; ];
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F"; const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-"; 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; const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => { runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") { if (!message || !message.type) {
return false; return false;
} }
if (message.type === "opportunitiesExtension.getXsrfToken") {
getXsrfTokenCookie() getXsrfTokenCookie()
.then((result) => { .then((result) => {
sendResponse({ sendResponse({
@@ -39,7 +43,147 @@
}); });
return true; 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() { async function getXsrfTokenCookie() {
const lookupDetails = []; const lookupDetails = [];

View File

@@ -502,8 +502,98 @@
} }
async function requestTokenRelay() { async function requestTokenRelay() {
let refreshResult = null;
let refreshAttempted = false;
while (true) {
const xsrfTokenSource = await getXsrfToken(); 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", name: "requestTokenRelay",
method: "GET", method: "GET",
url: TOKEN_RELAY_URL, url: TOKEN_RELAY_URL,
@@ -519,56 +609,25 @@
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
? xsrfTokenSource.lookupDetails.join(" | ") ? xsrfTokenSource.lookupDetails.join(" | ")
: "(none)", : "(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 { function isInvalidXsrfStatus(status) {
const response = await fetch(TOKEN_RELAY_URL, { return status === 400 || status === 401 || status === 403;
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}.`);
} }
let responseData; function formatXsrfRefreshResult(result) {
if (!result) {
try { return "not-requested";
responseData = await response.json();
} catch (error) {
logRequestSuccess(requestEntry, response, responseBody);
throw error;
} }
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) { const outcome = result.refreshed ? "refreshed" : "not-refreshed";
logRequestSuccess(requestEntry, response, responseBody); const cookieName = result.cookieName || "(none)";
throw new Error("Token relay response does not contain access_token."); const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown";
} return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim();
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;
}
} }
async function requestOpportunitiesForPeriod(period) { 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) { function updateAuthBadge(status) {
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`); const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);

View File

@@ -10,13 +10,17 @@
]; ];
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F"; const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-"; 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; const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => { runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") { if (!message || !message.type) {
return false; return false;
} }
if (message.type === "opportunitiesExtension.getXsrfToken") {
getXsrfTokenCookie() getXsrfTokenCookie()
.then((result) => { .then((result) => {
sendResponse({ sendResponse({
@@ -39,7 +43,147 @@
}); });
return true; 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() { async function getXsrfTokenCookie() {
const lookupDetails = []; const lookupDetails = [];

View File

@@ -502,8 +502,98 @@
} }
async function requestTokenRelay() { async function requestTokenRelay() {
let refreshResult = null;
let refreshAttempted = false;
while (true) {
const xsrfTokenSource = await getXsrfToken(); 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", name: "requestTokenRelay",
method: "GET", method: "GET",
url: TOKEN_RELAY_URL, url: TOKEN_RELAY_URL,
@@ -519,56 +609,25 @@
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
? xsrfTokenSource.lookupDetails.join(" | ") ? xsrfTokenSource.lookupDetails.join(" | ")
: "(none)", : "(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 { function isInvalidXsrfStatus(status) {
const response = await fetch(TOKEN_RELAY_URL, { return status === 400 || status === 401 || status === 403;
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}.`);
} }
let responseData; function formatXsrfRefreshResult(result) {
if (!result) {
try { return "not-requested";
responseData = await response.json();
} catch (error) {
logRequestSuccess(requestEntry, response, responseBody);
throw error;
} }
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) { const outcome = result.refreshed ? "refreshed" : "not-refreshed";
logRequestSuccess(requestEntry, response, responseBody); const cookieName = result.cookieName || "(none)";
throw new Error("Token relay response does not contain access_token."); const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown";
} return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim();
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;
}
} }
async function requestOpportunitiesForPeriod(period) { 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) { function updateAuthBadge(status) {
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`); const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);

View File

@@ -10,13 +10,17 @@
]; ];
const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F"; const XSRF_COOKIE_NAME = "XSRF-TOKEN-US2DZ2V_F";
const XSRF_COOKIE_PREFIX = "XSRF-TOKEN-"; 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; const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => { runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!message || message.type !== "opportunitiesExtension.getXsrfToken") { if (!message || !message.type) {
return false; return false;
} }
if (message.type === "opportunitiesExtension.getXsrfToken") {
getXsrfTokenCookie() getXsrfTokenCookie()
.then((result) => { .then((result) => {
sendResponse({ sendResponse({
@@ -39,7 +43,147 @@
}); });
return true; 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() { async function getXsrfTokenCookie() {
const lookupDetails = []; const lookupDetails = [];

View File

@@ -502,8 +502,98 @@
} }
async function requestTokenRelay() { async function requestTokenRelay() {
let refreshResult = null;
let refreshAttempted = false;
while (true) {
const xsrfTokenSource = await getXsrfToken(); 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", name: "requestTokenRelay",
method: "GET", method: "GET",
url: TOKEN_RELAY_URL, url: TOKEN_RELAY_URL,
@@ -519,56 +609,25 @@
xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length
? xsrfTokenSource.lookupDetails.join(" | ") ? xsrfTokenSource.lookupDetails.join(" | ")
: "(none)", : "(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 { function isInvalidXsrfStatus(status) {
const response = await fetch(TOKEN_RELAY_URL, { return status === 400 || status === 401 || status === 403;
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}.`);
} }
let responseData; function formatXsrfRefreshResult(result) {
if (!result) {
try { return "not-requested";
responseData = await response.json();
} catch (error) {
logRequestSuccess(requestEntry, response, responseBody);
throw error;
} }
if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) { const outcome = result.refreshed ? "refreshed" : "not-refreshed";
logRequestSuccess(requestEntry, response, responseBody); const cookieName = result.cookieName || "(none)";
throw new Error("Token relay response does not contain access_token."); const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown";
} return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim();
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;
}
} }
async function requestOpportunitiesForPeriod(period) { 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) { function updateAuthBadge(status) {
const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`); const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`);