diff --git a/packages/auth/test/mfa-login-flow.test.ts b/packages/auth/test/mfa-login-flow.test.ts new file mode 100644 index 00000000..01721a95 --- /dev/null +++ b/packages/auth/test/mfa-login-flow.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from "bun:test"; +import { createContext } from "@wrnexus/core"; +import { createAuthEngine } from "../src/engine.ts"; +import { createAuthHttpHandlers } from "../src/http/index.ts"; +import { MemoryAuthStore } from "../src/stores/memory.ts"; +import { generateTotp } from "../src/totp/index.ts"; + +test("HTTP login creates a usable MFA challenge; expired requests require a new login", async () => { + let time = 1_720_000_000_000; + const engine = createAuthEngine({ + store: new MemoryAuthStore(), + secret: "mfa-full-flow-test-secret-longer-than-thirty-two-characters", + clock: { now: () => time }, + }); + const user = await engine.register({ email: "flow@example.test", password: "StrongPassword123" }); + const setup = await engine.beginTotp(user.user!.id); + expect( + await engine.confirmTotp( + user.user!.id, + setup.credentialId, + await generateTotp(setup.secret, { timestamp: time }), + ), + ).toBe(true); + const handlers = createAuthHttpHandlers({ engine }); + const context = (path: string, body: unknown) => { + const req = new Request(`https://sso.example.test${path}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + return createContext(req, new URL(req.url)); + }; + const login = async () => { + const response = await handlers.login( + context("/api/auth/login", { + identifier: "flow@example.test", + password: "StrongPassword123", + }), + ); + expect(response.status).toBe(401); + const result = await response.json(); + expect(result.code).toBe("mfa-required"); + expect(result.mfaExpiresAt).toBe(time + 300_000); + return result; + }; + const old = await login(); + time += 300_001; + const expired = await handlers.completeMfa( + context("/api/auth/mfa/complete", { + mfaToken: old.mfaToken, + method: "totp", + code: await generateTotp(setup.secret, { timestamp: time }), + }), + ); + expect((await expired.json()).code).toBe("mfa-transaction-expired"); + const fresh = await login(); + const success = await handlers.completeMfa( + context("/api/auth/mfa/complete", { + mfaToken: fresh.mfaToken, + method: "totp", + code: await generateTotp(setup.secret, { timestamp: time }), + }), + ); + expect(success.status).toBe(200); + expect(await success.json()).toMatchObject({ ok: true, user: { id: user.user!.id } }); +});