169 lines
6.0 KiB
JavaScript
169 lines
6.0 KiB
JavaScript
function createAuthTokenRelay({
|
|
TOKEN_RELAY_URL,
|
|
authSessionState,
|
|
createDebugBody,
|
|
getXsrfToken,
|
|
logRequestFailure,
|
|
logRequestStart,
|
|
logRequestSuccess,
|
|
maskToken,
|
|
readResponseBodyForDebug,
|
|
refreshXsrfCookie,
|
|
renderDebugPanel,
|
|
environment
|
|
}) {
|
|
const { fetch } = environment;
|
|
async function requestTokenRelay({ name = "requestTokenRelay", url = TOKEN_RELAY_URL, includeExpiry = false } = {}) {
|
|
if (url === TOKEN_RELAY_URL) authSessionState.principal = "";
|
|
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);
|
|
const controller = environment.AbortController ? new environment.AbortController() : null;
|
|
let timeoutId;
|
|
const timeout = new Promise((_, reject) => {
|
|
if (!environment.setTimeout) return;
|
|
timeoutId = environment.setTimeout(() => {
|
|
reject(new Error("Token relay timed out after 30 seconds. Please try again."));
|
|
controller?.abort();
|
|
}, 30000);
|
|
});
|
|
|
|
try {
|
|
const response = await Promise.race([fetch(url, {
|
|
method: "GET",
|
|
credentials: "include",
|
|
...(controller ? { signal: controller.signal } : {}),
|
|
headers: {
|
|
"x-xsrf-token": xsrfTokenSource.token
|
|
}
|
|
}), timeout]);
|
|
|
|
const responseBody = await Promise.race([readResponseBodyForDebug(response), timeout]);
|
|
|
|
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 Promise.race([response.json(), timeout]);
|
|
} catch (error) {
|
|
if (controller?.signal.aborted) throw 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, responseBody);
|
|
|
|
if (url === TOKEN_RELAY_URL) {
|
|
authSessionState.principal = typeof responseData.principal === "string" ? responseData.principal.trim() : "";
|
|
}
|
|
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;
|
|
} finally {
|
|
if (timeoutId !== undefined) environment.clearTimeout?.(timeoutId);
|
|
}
|
|
}
|
|
}
|
|
|
|
function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult, name = "requestTokenRelay", url = TOKEN_RELAY_URL) {
|
|
return logRequestStart({
|
|
name,
|
|
method: "GET",
|
|
url,
|
|
headers: {
|
|
"x-xsrf-token": 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 };
|
|
}
|