fix: bootstrap admin CSRF tokens
This commit is contained in:
1 parent
a57c73055f
commit
b3245a18ea
8 files changed
+270
-45
No files matched your search
+86
-24
@@ -1,31 +1,93 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { adminFetch } from "./admin-fetch";
|
||||
|
||||
describe("admin CSRF wiring", () => {
|
||||
it("defaults CSRF on for mutating withAdmin handlers", () => {
|
||||
const source = readFileSync(
|
||||
resolve(process.cwd(), "src/lib/api-handler.ts"),
|
||||
"utf8",
|
||||
);
|
||||
expect(source).toContain("options.requireCsrf !== false");
|
||||
const TOKEN = "a".repeat(64);
|
||||
const REFRESHED_TOKEN = "b".repeat(64);
|
||||
|
||||
function installDocument(initialToken: string | null) {
|
||||
let token = initialToken;
|
||||
const meta = {
|
||||
getAttribute: (name: string) => (name === "content" ? token : null),
|
||||
setAttribute: (name: string, value: string) => {
|
||||
if (name === "content") token = value;
|
||||
},
|
||||
};
|
||||
|
||||
vi.stubGlobal("document", {
|
||||
querySelector: () => (token ? meta : null),
|
||||
createElement: () => meta,
|
||||
head: { appendChild: vi.fn() },
|
||||
});
|
||||
}
|
||||
|
||||
function jsonResponse(body: unknown, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
describe("adminFetch CSRF handling", () => {
|
||||
beforeEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("issues a csrf meta tag from the admin layout", () => {
|
||||
const source = readFileSync(
|
||||
resolve(process.cwd(), "src/app/admin/layout.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
expect(source).toContain("setCsrfCookie");
|
||||
expect(source).toContain('meta name="csrf-token"');
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("provides adminFetch helper that sets x-csrf-token", () => {
|
||||
const source = readFileSync(
|
||||
resolve(process.cwd(), "src/lib/admin-fetch.ts"),
|
||||
"utf8",
|
||||
);
|
||||
expect(source).toContain("x-csrf-token");
|
||||
expect(source).toContain("getCsrfToken");
|
||||
it("bootstraps a missing CSRF token before a mutating request", async () => {
|
||||
installDocument(null);
|
||||
const fetchMock = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(jsonResponse({ ok: true, token: TOKEN }))
|
||||
.mockResolvedValueOnce(jsonResponse({ ok: true }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await adminFetch("/api/admin/import/furni", {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(fetchMock.mock.calls[0]?.[0]).toBe("/api/admin/csrf");
|
||||
const requestHeaders = new Headers(fetchMock.mock.calls[1]?.[1]?.headers);
|
||||
expect(requestHeaders.get("x-csrf-token")).toBe(TOKEN);
|
||||
});
|
||||
|
||||
it("refreshes and retries once when the server rejects a stale token", async () => {
|
||||
installDocument(TOKEN);
|
||||
const fetchMock = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse(
|
||||
{ ok: false, error: "Invalid or missing CSRF token" },
|
||||
403,
|
||||
),
|
||||
)
|
||||
.mockResolvedValueOnce(jsonResponse({ ok: true, token: REFRESHED_TOKEN }))
|
||||
.mockResolvedValueOnce(jsonResponse({ ok: true, imported: 1 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await adminFetch("/api/admin/import/furni", {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(await response.json()).toEqual({ ok: true, imported: 1 });
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
const retryHeaders = new Headers(fetchMock.mock.calls[2]?.[1]?.headers);
|
||||
expect(retryHeaders.get("x-csrf-token")).toBe(REFRESHED_TOKEN);
|
||||
});
|
||||
|
||||
it("does not bootstrap CSRF for read-only requests", async () => {
|
||||
installDocument(null);
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse([]));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await adminFetch("/api/admin/import/furni");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
expect(fetchMock.mock.calls[0]?.[0]).toBe("/api/admin/import/furni");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user