Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Two-Factor Authentication (2FA) - podwojna straz Imperium

Centurionie bezpieczenstwa! Architekt Vitruvius postanowil wzmocnic obrone Imperium o kolejna warstwe. Jeden straznik przy bramie to za malo - czas na podwojna straz! Two-Factor Authentication (2FA) to system, w ktorym legionariusz musi przejsc przez dwie niezalezne bramki, zeby dostac sie do fortu.

Czym jest 2FA?

Two-Factor Authentication wymaga dwoch niezaleznych czynnikow weryfikacji:

  • Cos co wiesz - haslo (pierwszy faktor)
  • Cos co masz - telefon z aplikacja generujaca kody (drugi faktor)

Nawet jesli wrog przechwyci haslo legionariusza, bez drugiego faktora nie wejdzie do fortu!

TOTP - Time-based One-Time Password

TOTP to algorytm generujacy jednorazowe kody na podstawie czasu. Kazdy kod jest wazny przez 30 sekund i nie mozna go uzyc ponownie.

1// Jak dziala TOTP:
2// 1. Serwer generuje losowy secret (klucz)
3// 2. Uzytkownik skanuje QR kod z secretem w aplikacji (Google Authenticator)
4// 3. Aplikacja generuje 6-cyfrowy kod co 30 sekund
5// 4. Serwer weryfikuje kod uzywajac tego samego secretu i czasu

Instalacja bibliotek

1# otplib - nowoczesna biblioteka TOTP
2npm install otplib
3# qrcode - generowanie kodow QR
4npm install qrcode
5npm install -D @types/qrcode

TwoFactor Service

1// 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 // Konfiguracja TOTP
16 authenticator.options = {
17 digits: 6,      // 6-cyfrowy kod
18 step: 30,       // Nowy kod co 30 sekund
19 window: 1,      // Akceptuj kod z +/- 1 kroku (tolerancja)
20 };
21 }
22
23 // Krok 1: Generowanie secretu i QR kodu
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('Legionariusz nie znaleziony!');
33 }
34
35 if (user.twoFactorEnabled) {
36 throw new BadRequestException('2FA jest juz aktywne!');
37 }
38
39 // Generuj losowy secret
40 const secret = authenticator.generateSecret();
41
42 // Generuj URI dla aplikacji authenticator
43 const otpauthUrl = authenticator.keyuri(
44 user.email,
45 'Imperium Romanum',  // Nazwa aplikacji
46 secret,
47 );
48
49 // Generuj QR kod jako data URL
50 const qrCodeUrl = await qrcode.toDataURL(otpauthUrl);
51
52 // Generuj backup codes (kody zapasowe)
53 const backupCodes = this.generateBackupCodes(8);
54
55 // Zapisz secret tymczasowo (nie aktywuj jeszcze!)
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 // Krok 2: Weryfikacja i aktywacja 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('Najpierw wygeneruj secret!');
72 }
73
74 // Zweryfikuj kod z aplikacji
75 const isValid = authenticator.verify({
76 token: code,
77 secret: user.twoFactorSecret,
78 });
79
80 if (!isValid) {
81 throw new UnauthorizedException('Nieprawidlowy kod 2FA!');
82 }
83
84 // Aktywuj 2FA
85 await this.usersRepository.update(userId, {
86 twoFactorEnabled: true,
87 });
88
89 return true;
90 }
91
92 // Krok 3: Weryfikacja kodu przy logowaniu
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 nie jest skonfigurowane!');
98 }
99
100 // Sprawdz czy to backup code
101 if (code.length === 8) {
102 return this.verifyBackupCode(user, code);
103 }
104
105 // Zweryfikuj TOTP kod
106 return authenticator.verify({
107 token: code,
108 secret: user.twoFactorSecret,
109 });
110 }
111
112 // Weryfikacja backup code
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 // Oznacz jako uzyty
122 codeEntry.used = true;
123 await this.usersRepository.update(user.id, {
124 twoFactorBackupCodes: JSON.stringify(backupCodes),
125 });
126
127 return true;
128 }
129
130 // Generowanie 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 // Dezaktywacja 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('Nieprawidlowy kod - nie mozna wylaczyc 2FA!');
152 }
153
154 await this.usersRepository.update(userId, {
155 twoFactorEnabled: false,
156 twoFactorSecret: null,
157 twoFactorBackupCodes: null,
158 });
159 }
160}

2FA Controller

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 // Krok 1: Rozpocznij konfiguracje 2FA
12 @Post('setup')
13 async setup(@Req() req) {
14 const result = await this.twoFactorService.generateSecret(req.user.id);
15
16 return {
17 message: 'Zeskanuj QR kod w aplikacji authenticator',
18 qrCodeUrl: result.qrCodeUrl,
19 backupCodes: result.backupCodes,
20 warning: 'Zapisz backup codes w bezpiecznym miejscu!',
21 };
22 }
23
24 // Krok 2: Potwierdz konfiguracje kodem z aplikacji
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 zostalo aktywowane! Podwojna straz chroni twoje konto.',
31 enabled: true,
32 };
33 }
34
35 // Wylacz 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 zostalo dezaktywowane.',
42 enabled: false,
43 };
44 }
45}

2FA Guard - straznik drugiej bramy

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 // Jesli 2FA nie jest wlaczone, przepusc
15 if (!user.twoFactorEnabled) {
16 return true;
17 }
18
19 // Sprawdz czy uzytkownik juz przeszedl 2FA w tej sesji
20 if (user.twoFactorVerified) {
21 return true;
22 }
23
24 throw new UnauthorizedException(
25 'Wymagana weryfikacja 2FA! Podaj kod z aplikacji authenticator.'
26 );
27 }
28}

Flow logowania z 2FA

Caly proces logowania z 2FA wyglada nastepujaco:

  1. Legionariusz podaje username + password (pierwszy faktor)
  2. Serwer weryfikuje dane i sprawdza czy 2FA jest aktywne
  3. Jesli tak - zwraca tymczasowy token i wymaga kodu 2FA
  4. Legionariusz otwiera aplikacje authenticator i podaje 6-cyfrowy kod
  5. Serwer weryfikuje kod TOTP i wydaje pelny access token
  6. Legionariusz ma dostep do fortu!
1// Przyklad flow w AuthService
2async loginWith2FA(loginDto: LoginDto): Promise<any> {
3 // Krok 1: Weryfikuj haslo
4 const user = await this.validateUser(loginDto.username, loginDto.password);
5
6 if (!user) {
7 throw new UnauthorizedException('Nieprawidlowe dane!');
8 }
9
10 // Krok 2: Sprawdz czy 2FA aktywne
11 if (user.twoFactorEnabled) {
12 if (!loginDto.twoFactorCode) {
13 // Zwroc tymczasowy token wymagajacy 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 // Krok 3: Zweryfikuj kod 2FA
22 const is2FAValid = await this.twoFactorService.verifyCode(
23 user.id,
24 loginDto.twoFactorCode,
25 );
26
27 if (!is2FAValid) {
28 throw new UnauthorizedException('Nieprawidlowy kod 2FA!');
29 }
30 }
31
32 // Krok 4: Wydaj pelny token
33 return this.generateTokenPair(user);
34}

Two-Factor Authentication to potezna broń w arsenale bezpieczenstwa Imperium. Podwojna straz przy bramie sprawia, ze nawet jesli wrog zdobedzie haslo, nie wejdzie do fortu bez drugiego klucza!

Vai a CodeWorlds