import { INestApplication } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import { NobleCryptoPlugin, ScureBase32Plugin, TOTP } from 'otplib'; import request from 'supertest'; import { App } from 'supertest/types'; import { PrismaService } from '../src/prisma/prisma.service'; import { encryptMfaSecret } from '../src/auth/mfa/mfa-crypto.util'; import { generateTotpSecret } from '../src/auth/mfa/totp.util'; import { createTestApp } from './utils/create-test-app'; /** * Account-level brute-force protection and one-time TOTP codes, against a * real database. Each test uses its own account, because the per-IP * throttle (10/min on login) is shared by the whole suite. */ describe('Account guard (e2e)', () => { let app: INestApplication; let prisma: PrismaService; const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`; const password = 'SuperSecret123!'; const login = (email: string, pw: string) => request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email, password: pw }); beforeAll(async () => { app = await createTestApp(); prisma = app.get(PrismaService); }); afterAll(async () => { await app.close(); }); it('locks an account after five wrong passwords, even for the right one', async () => { const email = `lockout-${suffix}@example.com`; await prisma.user.create({ data: { email, passwordHash: await bcrypt.hash(password, 4), name: 'Lockout', role: { connect: { key: 'READONLY' } }, }, }); for (let i = 0; i < 5; i++) { await login(email, 'wrong-password-123').expect(401); } const locked = await login(email, password).expect(401); const wrong = await login(`nobody-${suffix}@example.com`, password).expect( 401, ); // Same message for "locked" and "no such account". expect((locked.body as { message: string }).message).toBe( (wrong.body as { message: string }).message, ); const stored = await prisma.user.findUniqueOrThrow({ where: { email } }); expect(stored.authLockedUntil?.getTime()).toBeGreaterThan(Date.now()); }); it('accepts a TOTP code only once', async () => { const email = `totp-${suffix}@example.com`; const secret = generateTotpSecret(); await prisma.user.create({ data: { email, passwordHash: await bcrypt.hash(password, 4), name: 'TOTP', mfaEnabled: true, mfaSecret: encryptMfaSecret(secret), role: { connect: { key: 'READONLY' } }, }, }); const code = await new TOTP({ secret, crypto: new NobleCryptoPlugin(), base32: new ScureBase32Plugin(), }).generate(); const first = await login(email, password).expect(200); await request(app.getHttpServer()) .post('/api/v1/auth/mfa/verify') .send({ mfaToken: (first.body as { mfaToken: string }).mfaToken, code }) .expect(200); const second = await login(email, password).expect(200); await request(app.getHttpServer()) .post('/api/v1/auth/mfa/verify') .send({ mfaToken: (second.body as { mfaToken: string }).mfaToken, code }) .expect(401); }); });