154 lines
5.2 KiB
JavaScript
154 lines
5.2 KiB
JavaScript
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 };
|
|
}
|