Edge-runtime check via auth-cookie.verifySession. /api/* paths get a 401 (no body) when unauthenticated; pages get a 307 to /login with the original path encoded into ?next=. Allowlist explicitly excludes /api/events and /api/qr — both were unauthenticated in v1.1.0 and let an unauthenticated client snoop the entire SSE event stream and enumerate paired account QR codes.
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
import { describe, it, expect, beforeAll } from "vitest";
|
|
import { NextRequest } from "next/server";
|
|
|
|
const SECRET = "test-secret";
|
|
beforeAll(() => {
|
|
process.env.AUTH_SECRET = SECRET;
|
|
process.env.OPERATOR_TOKEN_VERSION = "1";
|
|
});
|
|
|
|
import { signSession } from "./lib/auth-cookie";
|
|
import { middleware } from "./middleware";
|
|
|
|
async function makeReq(path: string, cookie?: string): Promise<NextRequest> {
|
|
const url = new URL(`https://wabot.04080616.xyz${path}`);
|
|
const headers = new Headers();
|
|
if (cookie) headers.set("cookie", `session=${cookie}`);
|
|
return new NextRequest(url, { headers });
|
|
}
|
|
|
|
async function validCookie(): Promise<string> {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
return signSession(
|
|
{
|
|
userId: "00000000-0000-0000-0000-000000000000",
|
|
role: "admin",
|
|
iat: now,
|
|
exp: now + 3600,
|
|
v: 1,
|
|
},
|
|
SECRET,
|
|
);
|
|
}
|
|
|
|
describe("middleware", () => {
|
|
it("page request without a cookie redirects to /login?next=…", async () => {
|
|
const r = await middleware(await makeReq("/dashboard"));
|
|
expect(r.status).toBe(307);
|
|
expect(r.headers.get("location")).toContain("/login");
|
|
expect(r.headers.get("location")).toContain("next=%2Fdashboard");
|
|
});
|
|
|
|
it("/api/* request without a cookie returns 401 with no body", async () => {
|
|
const r = await middleware(await makeReq("/api/events"));
|
|
expect(r.status).toBe(401);
|
|
});
|
|
|
|
it("page request with a valid cookie passes through", async () => {
|
|
const r = await middleware(await makeReq("/dashboard", await validCookie()));
|
|
// NextResponse.next() returns a 200 with the x-middleware-next header.
|
|
expect(r.status).toBe(200);
|
|
});
|
|
|
|
it("page request with a tampered cookie redirects to /login", async () => {
|
|
const cookie = (await validCookie()).slice(0, -4) + "AAAA";
|
|
const r = await middleware(await makeReq("/dashboard", cookie));
|
|
expect(r.status).toBe(307);
|
|
expect(r.headers.get("location")).toContain("/login");
|
|
});
|
|
|
|
it("allowlisted paths bypass auth (login, logout, health, manifest, icons)", async () => {
|
|
for (const path of [
|
|
"/login",
|
|
"/logout",
|
|
"/api/health",
|
|
"/manifest.webmanifest",
|
|
"/icon-192.png",
|
|
"/favicon.ico",
|
|
]) {
|
|
const r = await middleware(await makeReq(path));
|
|
expect(r.status).toBe(200);
|
|
}
|
|
});
|
|
|
|
it("/api/events and /api/qr/<id> are NOT in the allowlist (regression)", async () => {
|
|
expect((await middleware(await makeReq("/api/events"))).status).toBe(401);
|
|
expect(
|
|
(
|
|
await middleware(
|
|
await makeReq("/api/qr/11111111-1111-1111-1111-111111111111"),
|
|
)
|
|
).status,
|
|
).toBe(401);
|
|
});
|
|
});
|