fix
This commit is contained in:
+45
-9
@@ -137,37 +137,73 @@ function csrfCookieSecure(): boolean {
|
||||
|
||||
/** Dedup concurrent seed GETs (login submit + parallel mutations). */
|
||||
let csrfSeedInflight: Promise<string | null> | null = null;
|
||||
/** Cross-origin: API host-only cookies are invisible to document.cookie — cache header. */
|
||||
let csrfTokenMemory: string | null = null;
|
||||
|
||||
function isCrossOriginApi(apiBase: string): boolean {
|
||||
if (!apiBase || typeof location === "undefined") return false;
|
||||
try {
|
||||
return new URL(apiBase, location.href).origin !== location.origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Double-submit CSRF: cookie value must equal X-CSRF-Token on mutating calls.
|
||||
* Proven pattern (browser + curl): GET /api/auth/me seeds descrybe_csrf (401 ok when
|
||||
* logged out), then POST with X-CSRF-Token matching that cookie. Prefer API-issued
|
||||
* cookie over local mint so the jar matches what credentialed fetch sends.
|
||||
* Seed with GET /api/auth/me (401 ok when logged out). Prefer the API's
|
||||
* X-CSRF-Token response header so cross-origin SPAs (app host → api.*) work —
|
||||
* document.cookie cannot read the API host-only cookie. Same-origin may still
|
||||
* mint locally when the seed header is unavailable.
|
||||
*/
|
||||
async function ensureCsrfCookie(apiBase: string): Promise<string | null> {
|
||||
let token = readCookie(CSRF_COOKIE_NAME);
|
||||
if (token) return token;
|
||||
if (csrfTokenMemory) return csrfTokenMemory;
|
||||
|
||||
const crossOrigin = isCrossOriginApi(apiBase);
|
||||
// Same-origin only: a cookie on the page host is the API cookie.
|
||||
// Cross-origin: page-host cookies are the wrong jar and cause mismatches.
|
||||
if (!crossOrigin) {
|
||||
const token = readCookie(CSRF_COOKIE_NAME);
|
||||
if (token) {
|
||||
csrfTokenMemory = token;
|
||||
return token;
|
||||
}
|
||||
}
|
||||
if (typeof document === "undefined") return null;
|
||||
|
||||
if (!csrfSeedInflight) {
|
||||
csrfSeedInflight = (async () => {
|
||||
let fromHeader: string | null = null;
|
||||
try {
|
||||
const seedPath = "/api/auth/me";
|
||||
const seedUrl = apiBase ? `${apiBase}${seedPath}` : seedPath;
|
||||
await fetch(seedUrl, {
|
||||
const res = await fetch(seedUrl, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: { Accept: "application/json" }
|
||||
});
|
||||
const header = res.headers.get("X-CSRF-Token");
|
||||
if (header && header.trim()) fromHeader = header.trim();
|
||||
} catch {
|
||||
/* network — fall through to mint */
|
||||
/* network — fall through */
|
||||
}
|
||||
if (fromHeader) {
|
||||
csrfTokenMemory = fromHeader;
|
||||
return fromHeader;
|
||||
}
|
||||
if (crossOrigin) {
|
||||
// Do not mint on the page host — that cookie is never sent to api.*.
|
||||
return null;
|
||||
}
|
||||
const seeded = readCookie(CSRF_COOKIE_NAME);
|
||||
if (seeded) return seeded;
|
||||
// Same-host mint fallback (loopback twin already aligned via apiBase()).
|
||||
if (seeded) {
|
||||
csrfTokenMemory = seeded;
|
||||
return seeded;
|
||||
}
|
||||
const minted = mintCsrfToken();
|
||||
const secure = csrfCookieSecure() ? "; Secure" : "";
|
||||
document.cookie = `${CSRF_COOKIE_NAME}=${encodeURIComponent(minted)}; Path=/; SameSite=Lax; Max-Age=${CSRF_MAX_AGE_SEC}${secure}`;
|
||||
csrfTokenMemory = minted;
|
||||
return minted;
|
||||
})().finally(() => {
|
||||
csrfSeedInflight = null;
|
||||
|
||||
Reference in New Issue
Block a user