atlilith/@platform/codebase/@features/sso/backend-api/test/mfa.e2e-spec.ts

232 lines
7.1 KiB
TypeScript
Raw Normal View History

import { Test, TestingModule } from "@nestjs/testing";
import { INestApplication, ValidationPipe } from "@nestjs/common";
import { AppModule } from "@/src/app.module";
const request = require("supertest");
/**
* E2E Tests for MFA (Multi-Factor Authentication) Flow
*
* Prerequisites:
* 1. Start test services: docker-compose -f test/docker-compose.yml up -d
* 2. Run: pnpm test:e2e
*
* Tests cover:
* - MFA status retrieval
* - TOTP setup flow (secret + QR code generation)
* - Email MFA setup
* - MFA challenge during login
* - Recovery code usage
* - MFA disable flow (password-verified)
* - Rate limiting on MFA endpoints
*/
describe("MFA Controller (e2e)", () => {
let app: INestApplication;
let sessionToken: string;
const testUser = {
email: `mfa-e2e-${Date.now()}@example.com`,
username: `mfauser${Date.now()}`,
password: "SecureMfaPass123!",
};
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
await app.init();
// Register and login to get session token
const registerRes = await request(app.getHttpServer())
.post("/auth/register")
.send(testUser);
sessionToken = registerRes.body.sessionId;
});
afterAll(async () => {
await app.close();
});
describe("GET /auth/mfa/status", () => {
it("should return MFA status for authenticated user", () => {
return request(app.getHttpServer())
.get("/auth/mfa/status")
.set("Authorization", `Bearer ${sessionToken}`)
.expect(200)
.expect((res) => {
expect(res.body.enabled).toBe(false);
expect(res.body.methods).toEqual([]);
expect(res.body.totpEnabled).toBe(false);
expect(res.body.emailEnabled).toBe(false);
expect(res.body.recoveryCodesRemaining).toBeDefined();
});
});
it("should reject unauthenticated requests", () => {
return request(app.getHttpServer())
.get("/auth/mfa/status")
.expect(401);
});
});
describe("POST /auth/mfa/setup/totp", () => {
it("should return TOTP setup data with QR code", () => {
return request(app.getHttpServer())
.post("/auth/mfa/setup/totp")
.set("Authorization", `Bearer ${sessionToken}`)
.expect(200)
.expect((res) => {
expect(res.body.secret).toBeDefined();
expect(res.body.secret.length).toBeGreaterThan(10);
expect(res.body.otpauthUrl).toBeDefined();
expect(res.body.otpauthUrl).toContain("otpauth://totp/");
expect(res.body.qrCodeDataUrl).toBeDefined();
expect(res.body.qrCodeDataUrl).toContain("data:image/png;base64,");
});
});
it("should reject unauthenticated TOTP setup", () => {
return request(app.getHttpServer())
.post("/auth/mfa/setup/totp")
.expect(401);
});
});
describe("POST /auth/mfa/setup/email", () => {
it("should enable email MFA for authenticated user", () => {
return request(app.getHttpServer())
.post("/auth/mfa/setup/email")
.set("Authorization", `Bearer ${sessionToken}`)
.expect(200)
.expect((res) => {
expect(res.body.success).toBe(true);
});
});
it("should show email MFA enabled in status", async () => {
const res = await request(app.getHttpServer())
.get("/auth/mfa/status")
.set("Authorization", `Bearer ${sessionToken}`)
.expect(200);
expect(res.body.emailEnabled).toBe(true);
expect(res.body.enabled).toBe(true);
expect(res.body.methods).toContain("email");
});
});
describe("POST /auth/mfa/verify/totp", () => {
it("should reject invalid TOTP code", () => {
return request(app.getHttpServer())
.post("/auth/mfa/verify/totp")
.set("Authorization", `Bearer ${sessionToken}`)
.send({ secret: "INVALIDSECRET", code: "000000" })
.expect(400);
});
});
describe("POST /auth/mfa/challenge", () => {
it("should reject invalid pending session ID", () => {
return request(app.getHttpServer())
.post("/auth/mfa/challenge")
.send({
pendingSessionId: "nonexistent-session-id",
method: "email",
code: "123456",
})
.expect(401);
});
});
describe("POST /auth/mfa/send-code", () => {
it("should reject invalid pending session", () => {
return request(app.getHttpServer())
.post("/auth/mfa/send-code")
.send({ pendingSessionId: "nonexistent-session-id" })
.expect(401);
});
});
describe("POST /auth/mfa/recovery", () => {
it("should reject invalid pending session for recovery", () => {
return request(app.getHttpServer())
.post("/auth/mfa/recovery")
.send({
pendingSessionId: "nonexistent-session-id",
recoveryCode: "ABCD1234",
})
.expect(401);
});
});
describe("POST /auth/mfa/disable", () => {
it("should reject disable without valid password", () => {
return request(app.getHttpServer())
.post("/auth/mfa/disable")
.set("Authorization", `Bearer ${sessionToken}`)
.send({ password: "WrongPassword123!", method: "email" })
.expect(401);
});
it("should disable MFA method with correct password", () => {
return request(app.getHttpServer())
.post("/auth/mfa/disable")
.set("Authorization", `Bearer ${sessionToken}`)
.send({ password: testUser.password, method: "email" })
.expect(200)
.expect((res) => {
expect(res.body.success).toBe(true);
});
});
});
describe("POST /auth/mfa/preferred", () => {
it("should set preferred MFA method", async () => {
// Re-enable email MFA first
await request(app.getHttpServer())
.post("/auth/mfa/setup/email")
.set("Authorization", `Bearer ${sessionToken}`)
.expect(200);
return request(app.getHttpServer())
.post("/auth/mfa/preferred")
.set("Authorization", `Bearer ${sessionToken}`)
.send({ method: "email" })
.expect(200)
.expect((res) => {
expect(res.body.success).toBe(true);
});
});
});
describe("Full MFA Login Flow (with email MFA enabled)", () => {
it("should require MFA challenge on login when MFA is enabled", async () => {
const loginRes = await request(app.getHttpServer())
.post("/auth/login")
.send({
email: testUser.email,
password: testUser.password,
})
.expect(200);
// When MFA is enabled, login returns pending session instead of full session
if (loginRes.body.mfaRequired) {
expect(loginRes.body.pendingSessionId).toBeDefined();
expect(loginRes.body.availableMethods).toBeDefined();
expect(loginRes.body.availableMethods).toContain("email");
expect(loginRes.body.expiresAt).toBeDefined();
}
});
});
});