feat: implement TASK-WP1-04 admin security
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { createHmac, randomUUID } from "node:crypto";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { createApp } from "../../apps/api/src/app.js";
|
||||
import { RegistrationService } from "../../apps/api/src/registration.js";
|
||||
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
const services: RegistrationService[] = [];
|
||||
const now = Date.parse("2026-07-28T12:00:00.000Z");
|
||||
const adminPepper = Buffer.alloc(32, 0xb1);
|
||||
const headers = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
|
||||
|
||||
function createHarness() {
|
||||
const root = mkdtempSync(join(tmpdir(), "dada-wp1-04-api-"));
|
||||
roots.push(root);
|
||||
const resend = new MockResendAdapter();
|
||||
const registration = new RegistrationService({
|
||||
adminAllowlistPepper: adminPepper,
|
||||
challengePepper: Buffer.alloc(32, 0xb2),
|
||||
clock: () => now,
|
||||
codeGenerator: () => "418205",
|
||||
currentPrivacyNoticeVersion: "p0a-registration-notice-v1",
|
||||
databasePath: join(root, "dada.sqlite3"),
|
||||
invitePepper: Buffer.alloc(32, 0xb3),
|
||||
resend,
|
||||
sessionPepper: Buffer.alloc(32, 0xb4),
|
||||
});
|
||||
services.push(registration);
|
||||
return { registration, resend };
|
||||
}
|
||||
|
||||
function cookieValue(setCookie: string | string[] | undefined, name: string) {
|
||||
const entries = Array.isArray(setCookie) ? setCookie : [setCookie ?? ""];
|
||||
const match = entries.find((entry) => entry.startsWith(`${name}=`));
|
||||
if (!match) throw new Error(`Cookie ${name} was not returned.`);
|
||||
return match.split(";", 1)[0];
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const service of services.splice(0)) service.close();
|
||||
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
describe("TASK-WP1-04 admin auth API", () => {
|
||||
it("rejects before Resend, creates an admin session, and isolates both audiences", async () => {
|
||||
const { registration, resend } = createHarness();
|
||||
const email = "api-admin@example.invalid";
|
||||
registration.applySecureConfig({
|
||||
adminAllowlistHashes: [createHmac("sha256", adminPepper).update(email).digest("hex").toUpperCase()],
|
||||
adminRecoveryHashes: [],
|
||||
secureConfigRevision: 1,
|
||||
});
|
||||
const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration });
|
||||
|
||||
const blocked = await app.inject({
|
||||
headers,
|
||||
method: "POST",
|
||||
payload: { email: "blocked@example.invalid" },
|
||||
url: "/api/v1/admin-auth/login/send",
|
||||
});
|
||||
expect(blocked.statusCode).toBe(409);
|
||||
expect(blocked.json()).toMatchObject({ error: { details: { field_errors: [{ message_key: "admin.auth.not_allowed" }] } } });
|
||||
expect(resend.calls).toHaveLength(0);
|
||||
|
||||
const sent = await app.inject({
|
||||
headers,
|
||||
method: "POST",
|
||||
payload: { email },
|
||||
url: "/api/v1/admin-auth/login/send",
|
||||
});
|
||||
expect(sent.statusCode).toBe(200);
|
||||
const flowCookie = cookieValue(sent.headers["set-cookie"], "dada_admin_auth_flow");
|
||||
const code = resend.readLatestCode(email);
|
||||
const completed = await app.inject({
|
||||
headers: { ...headers, cookie: flowCookie, "idempotency-key": "wp1-04-api-admin-complete-000000000001" },
|
||||
method: "POST",
|
||||
payload: { registration_id: sent.json().registration_id, verification_code: code },
|
||||
url: "/api/v1/admin-auth/login/complete",
|
||||
});
|
||||
expect(completed.statusCode).toBe(200);
|
||||
expect(completed.json()).toMatchObject({ audience: "admin", status: "authenticated", admin: { role: "super_admin" } });
|
||||
const adminCookie = cookieValue(completed.headers["set-cookie"], "dada_admin_session");
|
||||
|
||||
const session = await app.inject({ headers: { ...headers, cookie: adminCookie }, method: "GET", url: "/api/v1/admin-auth/session" });
|
||||
expect(session.statusCode).toBe(200);
|
||||
expect(session.json()).toMatchObject({ audience: "admin", authenticated: true, notice_acknowledged: false });
|
||||
|
||||
const ordinaryUserId = randomUUID();
|
||||
registration.database.prepare(`
|
||||
INSERT INTO users (
|
||||
user_id, normalized_email, role, status, counts_toward_stage_limit,
|
||||
registration_id, created_at
|
||||
) VALUES (?, 'api-user@example.invalid', 'user', 'active', 1, ?, ?)
|
||||
`).run(ordinaryUserId, randomUUID(), now);
|
||||
const ordinary = registration.issueAuthenticatedSession(ordinaryUserId, "user");
|
||||
const userAtAdmin = await app.inject({
|
||||
headers: { ...headers, cookie: `dada_admin_session=${ordinary.sessionToken}` },
|
||||
method: "GET",
|
||||
url: "/api/v1/admin-auth/session",
|
||||
});
|
||||
expect(userAtAdmin.statusCode).toBe(401);
|
||||
const adminAtUser = await app.inject({ headers: { ...headers, cookie: adminCookie }, method: "GET", url: "/api/v1/auth/session" });
|
||||
expect(adminAtUser.statusCode).toBe(401);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user