Security centurion! Architect Vitruvius has decided to strengthen the Empire's defense with another layer. One guard at the gate is not enough - it's time for a double guard! Two-Factor Authentication (2FA) is a system where a legionary must pass through two independent checkpoints to get into the fort.
Two-Factor Authentication requires two independent verification factors:
Even if the enemy intercepts a legionary's password, without the second factor they won't enter the fort!
TOTP is an algorithm that generates one-time codes based on time. Each code is valid for 30 seconds and cannot be reused.
1// How TOTP works:
2// 1. Server generates a random secret (key)
3// 2. User scans the QR code with the secret in the authenticator app (Google Authenticator)
4// 3. The app generates a 6-digit code every 30 seconds
5// 4. Server verifies the code using the same secret and time1# otplib - modern TOTP library
2npm install otplib
3# qrcode - generating QR codes
4npm install qrcode
5npm install -D @types/qrcode1// auth/two-factor.service.ts
2import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
3import { authenticator } from 'otplib';
4import * as qrcode from 'qrcode';
5import { InjectRepository } from '@nestjs/typeorm';
6import { Repository } from 'typeorm';
7import { User } from '../entities/user.entity';
8
9@Injectable()
10export class TwoFactorService {
11 constructor(
12 @InjectRepository(User)
13 private usersRepository: Repository<User>,
14 ) {
15 // TOTP configuration
16 authenticator.options = {
17 digits: 6, // 6-digit code
18 step: 30, // New code every 30 seconds
19 window: 1, // Accept code from +/- 1 step (tolerance)
20 };
21 }
22
23 // Step 1: Generate secret and QR code
24 async generateSecret(userId: number): Promise<{
25 secret: string;
26 qrCodeUrl: string;
27 backupCodes: string[];
28 }> {
29 const user = await this.usersRepository.findOne({ where: { id: userId } });
30
31 if (!user) {
32 throw new BadRequestException('Legionary not found!');
33 }
34
35 if (user.twoFactorEnabled) {
36 throw new BadRequestException('2FA is already active!');
37 }
38
39 // Generate random secret
40 const secret = authenticator.generateSecret();
41
42 // Generate URI for the authenticator app
43 const otpauthUrl = authenticator.keyuri(
44 user.email,
45 'Imperium Romanum', // Application name
46 secret,
47 );
48
49 // Generate QR code as data URL
50 const qrCodeUrl = await qrcode.toDataURL(otpauthUrl);
51
52 // Generate backup codes
53 const backupCodes = this.generateBackupCodes(8);
54
55 // Save secret temporarily (don't activate yet!)
56 await this.usersRepository.update(userId, {
57 twoFactorSecret: secret,
58 twoFactorBackupCodes: JSON.stringify(
59 backupCodes.map(code => ({ code, used: false }))
60 ),
61 });
62
63 return { secret, qrCodeUrl, backupCodes };
64 }
65
66 // Step 2: Verify and activate 2FA
67 async enableTwoFactor(userId: number, code: string): Promise<boolean> {
68 const user = await this.usersRepository.findOne({ where: { id: userId } });
69
70 if (!user || !user.twoFactorSecret) {
71 throw new BadRequestException('Generate the secret first!');
72 }
73
74 // Verify the code from the app
75 const isValid = authenticator.verify({
76 token: code,
77 secret: user.twoFactorSecret,
78 });
79
80 if (!isValid) {
81 throw new UnauthorizedException('Invalid 2FA code!');
82 }
83
84 // Activate 2FA
85 await this.usersRepository.update(userId, {
86 twoFactorEnabled: true,
87 });
88
89 return true;
90 }
91
92 // Step 3: Verify code during login
93 async verifyCode(userId: number, code: string): Promise<boolean> {
94 const user = await this.usersRepository.findOne({ where: { id: userId } });
95
96 if (!user || !user.twoFactorSecret) {
97 throw new UnauthorizedException('2FA is not configured!');
98 }
99
100 // Check if it's a backup code
101 if (code.length === 8) {
102 return this.verifyBackupCode(user, code);
103 }
104
105 // Verify TOTP code
106 return authenticator.verify({
107 token: code,
108 secret: user.twoFactorSecret,
109 });
110 }
111
112 // Backup code verification
113 private async verifyBackupCode(user: User, code: string): Promise<boolean> {
114 const backupCodes = JSON.parse(user.twoFactorBackupCodes || '[]');
115 const codeEntry = backupCodes.find(
116 (bc: any) => bc.code === code && !bc.used
117 );
118
119 if (!codeEntry) return false;
120
121 // Mark as used
122 codeEntry.used = true;
123 await this.usersRepository.update(user.id, {
124 twoFactorBackupCodes: JSON.stringify(backupCodes),
125 });
126
127 return true;
128 }
129
130 // Generate backup codes
131 private generateBackupCodes(count: number): string[] {
132 const codes: string[] = [];
133 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
134
135 for (let i = 0; i < count; i++) {
136 let code = '';
137 for (let j = 0; j < 8; j++) {
138 code += chars.charAt(Math.floor(Math.random() * chars.length));
139 }
140 codes.push(code);
141 }
142
143 return codes;
144 }
145
146 // Deactivate 2FA
147 async disableTwoFactor(userId: number, code: string): Promise<void> {
148 const isValid = await this.verifyCode(userId, code);
149
150 if (!isValid) {
151 throw new UnauthorizedException('Invalid code - cannot disable 2FA!');
152 }
153
154 await this.usersRepository.update(userId, {
155 twoFactorEnabled: false,
156 twoFactorSecret: null,
157 twoFactorBackupCodes: null,
158 });
159 }
160}1// auth/two-factor.controller.ts
2import { Controller, Post, Get, Body, UseGuards, Req } from '@nestjs/common';
3import { TwoFactorService } from './two-factor.service';
4import { JwtAuthGuard } from './guards/jwt-auth.guard';
5
6@Controller('auth/2fa')
7@UseGuards(JwtAuthGuard)
8export class TwoFactorController {
9 constructor(private twoFactorService: TwoFactorService) {}
10
11 // Step 1: Start 2FA configuration
12 @Post('setup')
13 async setup(@Req() req) {
14 const result = await this.twoFactorService.generateSecret(req.user.id);
15
16 return {
17 message: 'Scan the QR code in the authenticator app',
18 qrCodeUrl: result.qrCodeUrl,
19 backupCodes: result.backupCodes,
20 warning: 'Save the backup codes in a safe place!',
21 };
22 }
23
24 // Step 2: Confirm configuration with a code from the app
25 @Post('verify')
26 async verify(@Req() req, @Body('code') code: string) {
27 await this.twoFactorService.enableTwoFactor(req.user.id, code);
28
29 return {
30 message: '2FA has been activated! Double guard protects your account.',
31 enabled: true,
32 };
33 }
34
35 // Disable 2FA
36 @Post('disable')
37 async disable(@Req() req, @Body('code') code: string) {
38 await this.twoFactorService.disableTwoFactor(req.user.id, code);
39
40 return {
41 message: '2FA has been deactivated.',
42 enabled: false,
43 };
44 }
45}1// auth/guards/two-factor.guard.ts
2import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
3import { UnauthorizedException } from '@nestjs/common';
4import { TwoFactorService } from '../two-factor.service';
5
6@Injectable()
7export class TwoFactorGuard implements CanActivate {
8 constructor(private twoFactorService: TwoFactorService) {}
9
10 async canActivate(context: ExecutionContext): Promise<boolean> {
11 const request = context.switchToHttp().getRequest();
12 const user = request.user;
13
14 // If 2FA is not enabled, let through
15 if (!user.twoFactorEnabled) {
16 return true;
17 }
18
19 // Check if the user already passed 2FA in this session
20 if (user.twoFactorVerified) {
21 return true;
22 }
23
24 throw new UnauthorizedException(
25 '2FA verification required! Enter the code from your authenticator app.'
26 );
27 }
28}The entire login process with 2FA looks as follows:
1// Example flow in AuthService
2async loginWith2FA(loginDto: LoginDto): Promise<any> {
3 // Step 1: Verify password
4 const user = await this.validateUser(loginDto.username, loginDto.password);
5
6 if (!user) {
7 throw new UnauthorizedException('Invalid credentials!');
8 }
9
10 // Step 2: Check if 2FA is active
11 if (user.twoFactorEnabled) {
12 if (!loginDto.twoFactorCode) {
13 // Return temporary token requiring 2FA
14 const tempToken = this.jwtService.sign(
15 { sub: user.id, requiresTwoFactor: true },
16 { expiresIn: '5m' },
17 );
18 return { requiresTwoFactor: true, tempToken };
19 }
20
21 // Step 3: Verify 2FA code
22 const is2FAValid = await this.twoFactorService.verifyCode(
23 user.id,
24 loginDto.twoFactorCode,
25 );
26
27 if (!is2FAValid) {
28 throw new UnauthorizedException('Invalid 2FA code!');
29 }
30 }
31
32 // Step 4: Issue full token
33 return this.generateTokenPair(user);
34}Two-Factor Authentication is a powerful weapon in the Empire's security arsenal. A double guard at the gate means that even if the enemy obtains a password, they won't enter the fort without the second key!