import { createHmac } from "node:crypto"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import request from "supertest"; import type { FastifyInstance } from "fastify"; import { buildApp } from "../src/server"; const JWT_SECRET = "test-jwt-secret-32-chars-min-abcdef"; const COOKIE_SECRET = "test-cookie-secret-32-chars-abcdef"; let app: FastifyInstance; function base64UrlEncode(value: string): string { return Buffer.from(value) .toString("base64") .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); } function signHs256Token(payload: Record, secret: string): string { const header = { alg: "HS256", typ: "JWT" }; const encodedHeader = base64UrlEncode(JSON.stringify(header)); const encodedPayload = base64UrlEncode(JSON.stringify(payload)); const signingInput = `${encodedHeader}.${encodedPayload}`; const signature = createHmac("sha256", secret) .update(signingInput) .digest("base64") .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); return `${signingInput}.${signature}`; } beforeAll(async () => { app = await buildApp({ NODE_ENV: "test", AUTH_DISABLED: false, SEED_DEFAULT_BUDGET: false, DATABASE_URL: "postgres://app:app@127.0.0.1:5432/skymoney", JWT_SECRET, COOKIE_SECRET, JWT_ISSUER: "skymoney-api", JWT_AUDIENCE: "skymoney-web", APP_ORIGIN: "http://localhost:5173", }); await app.ready(); // Keep JWT verification behavior under test without requiring DB connectivity. (app as any).ensureUser = async () => undefined; }); afterAll(async () => { if (app) await app.close(); }); describe("A04 Cryptographic Failures (runtime adversarial checks)", () => { const csrf = "runtime-test-csrf-token"; it("rejects token with wrong issuer", async () => { const nowSeconds = Math.floor(Date.now() / 1000); const token = signHs256Token( { sub: `wrong-iss-${Date.now()}`, iss: "attacker-issuer", aud: "skymoney-web", iat: nowSeconds, exp: nowSeconds + 600, }, JWT_SECRET ); const res = await request(app.server) .post("/auth/refresh") .set("x-csrf-token", csrf) .set("Cookie", [`csrf=${csrf}`, `session=${token}`]); expect(res.status).toBe(401); expect(res.body.code).toBe("UNAUTHENTICATED"); }); it("rejects token with wrong audience", async () => { const nowSeconds = Math.floor(Date.now() / 1000); const token = signHs256Token( { sub: `wrong-aud-${Date.now()}`, iss: "skymoney-api", aud: "attacker-app", iat: nowSeconds, exp: nowSeconds + 600, }, JWT_SECRET ); const res = await request(app.server) .post("/auth/refresh") .set("x-csrf-token", csrf) .set("Cookie", [`csrf=${csrf}`, `session=${token}`]); expect(res.status).toBe(401); expect(res.body.code).toBe("UNAUTHENTICATED"); }); it("rejects token with alg=none (unsigned token)", async () => { const nowSeconds = Math.floor(Date.now() / 1000); const encodedHeader = base64UrlEncode(JSON.stringify({ alg: "none", typ: "JWT" })); const encodedPayload = base64UrlEncode( JSON.stringify({ sub: `none-alg-${Date.now()}`, iss: "skymoney-api", aud: "skymoney-web", iat: nowSeconds, exp: nowSeconds + 600, }) ); const token = `${encodedHeader}.${encodedPayload}.`; const res = await request(app.server) .post("/auth/refresh") .set("x-csrf-token", csrf) .set("Cookie", [`csrf=${csrf}`, `session=${token}`]); expect(res.status).toBe(401); expect(res.body.code).toBe("UNAUTHENTICATED"); }); it("accepts token with correct signature, issuer, and audience", async () => { const userId = `valid-${Date.now()}`; const findUniqueMock = vi .spyOn((app as any).prisma.user, "findUnique") .mockResolvedValue({ id: userId, passwordChangedAt: null }); const nowSeconds = Math.floor(Date.now() / 1000); const token = signHs256Token( { sub: userId, iss: "skymoney-api", aud: "skymoney-web", iat: nowSeconds, exp: nowSeconds + 600, }, JWT_SECRET ); const res = await request(app.server) .post("/auth/refresh") .set("x-csrf-token", csrf) .set("Cookie", [`csrf=${csrf}`, `session=${token}`]); expect(res.status).toBe(200); expect(res.body.ok).toBe(true); findUniqueMock.mockRestore(); }); });