import { INestApplication } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcryptjs'; 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'; interface MfaRequiredBody { mfaRequired: true; mfaToken: string; } /** * All ApiCore JWTs share one secret. Only an access token may open a * session - the helper tokens (MFA challenge after the password step, MFA * setup, SSO completion) and hand-made tokens without the pinned claims * must be refused everywhere. */ describe('Token confusion (e2e)', () => { let app: INestApplication; let prisma: PrismaService; let userId: string; const email = `token-confusion-${Date.now()}@example.com`; const password = 'SuperSecret123!'; const me = (token: string) => request(app.getHttpServer()) .get('/api/v1/auth/me') .set('Authorization', `Bearer ${token}`); beforeAll(async () => { app = await createTestApp(); prisma = app.get(PrismaService); const user = await prisma.user.create({ data: { email, passwordHash: await bcrypt.hash(password, 12), name: 'MFA user', mfaEnabled: true, mfaSecret: encryptMfaSecret(generateTotpSecret()), role: { connect: { key: 'ADMIN' } }, }, }); userId = user.id; }); afterAll(async () => { await app.close(); }); it('does not accept the MFA challenge token as an access token', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email, password }) .expect(200); const body = response.body as MfaRequiredBody; expect(body.mfaRequired).toBe(true); await me(body.mfaToken).expect(401); await request(app.getHttpServer()) .get('/api/v1/users') .set('Authorization', `Bearer ${body.mfaToken}`) .expect(401); }); it('does not accept a token without the pinned audience and issuer', async () => { const legacy = new JwtService().sign( { sub: userId, email }, { secret: process.env.JWT_ACCESS_SECRET, expiresIn: 60 }, ); await me(legacy).expect(401); }); it('does not accept a helper token even with the access audience', async () => { const forged = new JwtService().sign( { sub: userId, email, purpose: 'mfa-challenge' }, { secret: process.env.JWT_ACCESS_SECRET, expiresIn: 60, issuer: 'apicore', audience: 'apicore:access', }, ); await me(forged).expect(401); }); });