US07-02: Harden API Authorization and Path Boundaries (#84)
This commit was merged in pull request #84.
This commit is contained in:
@@ -2,14 +2,43 @@
|
||||
// cancellation. Every method accepts an optional { signal } from cancellable().
|
||||
export const BASE = "/api/v1";
|
||||
|
||||
// The API refuses every request without the session cookie, and every mutation
|
||||
// without this token echoed back. The token is readable only same-origin, which is
|
||||
// what makes it proof that the caller is this app and not another page.
|
||||
let csrfToken = null;
|
||||
|
||||
async function session() {
|
||||
if (csrfToken === null) {
|
||||
const response = await fetch(BASE + "/session", { credentials: "same-origin" });
|
||||
const body = await response.json().catch(() => null);
|
||||
csrfToken = (body && body.csrf_token) || null;
|
||||
}
|
||||
return csrfToken || "";
|
||||
}
|
||||
|
||||
async function send(path, { signal, ...options }) {
|
||||
return fetch(BASE + path, {
|
||||
credentials: "same-origin",
|
||||
signal,
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": await session(),
|
||||
...(options.headers || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function request(path, { signal, ...options } = {}) {
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(BASE + path, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
signal,
|
||||
...options,
|
||||
});
|
||||
response = await send(path, { signal, ...options });
|
||||
// A restarted server issues a new session; re-bootstrap once rather than
|
||||
// stranding an open tab on 401.
|
||||
if (response.status === 401) {
|
||||
csrfToken = null;
|
||||
response = await send(path, { signal, ...options });
|
||||
}
|
||||
} catch (error) {
|
||||
// A caller-cancelled fetch is not a failure; tag it so views can ignore it.
|
||||
if (error.name === "AbortError") {
|
||||
|
||||
@@ -29,6 +29,10 @@ function jsonResponse(status, body) {
|
||||
const tick = (ms = 0) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function run() {
|
||||
// The client fetches its CSRF token once, lazily (US07-02). Do that against the
|
||||
// real server first, so the stubbed fetch below only ever sees the call under test.
|
||||
await api.workflow().catch(() => {});
|
||||
|
||||
// ── store ────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const store = createStore({ n: 0 });
|
||||
|
||||
Reference in New Issue
Block a user