Two-Factor Authentication
Overview
Two-Factor Authentication (2FA) adds an extra layer of security to your application by requiring a second verification step beyond just a password. The starter kit implements 2FA using Time-based One-Time Passwords (TOTP).
Features
- QR code generation for easy setup with authenticator apps
- Secret key backup options for recovery
- Toggle 2FA on/off per user
- Remember-device functionality (optional)
Implementation
The 2FA implementation uses the otplib
package which provides TOTP generation compatible with Google Authenticator, Authy, and other authenticator apps.
Dependencies
{
"dependencies": {
"otplib": "^12.0.1",
"qrcode": "^1.5.0"
}
}
Two-Factor Service
@Injectable()
export class TwoFactorService {
constructor(
private configService: ConfigService,
private usersService: UsersService,
) {}
async generateTwoFactorSecret(user: User) {
const secret = authenticator.generateSecret();
const appName = this.configService.get<string>('APP_NAME');
const otpAuthUrl = authenticator.keyuri(
user.email,
appName,
secret,
);
await this.usersService.setTwoFactorSecret(user.id, secret);
return {
secret,
otpAuthUrl,
};
}
async generateQrCodeDataURL(otpAuthUrl: string) {
return toDataURL(otpAuthUrl);
}
verifyTwoFactorCode(twoFactorCode: string, user: User) {
return authenticator.verify({
token: twoFactorCode,
secret: user.twoFactorSecret,
});
}
}
Usage Flow
Step 1: Enable 2FA
@Post('2fa/enable')
@UseGuards(JwtAuthGuard)
async enableTwoFactor(@Request() req) {
const { secret, otpAuthUrl } = await this.twoFactorService
.generateTwoFactorSecret(req.user);
const qrCodeDataURL = await this.twoFactorService
.generateQrCodeDataURL(otpAuthUrl);
return {
secret,
qrCodeDataURL,
};
}
Step 2: Verify and Activate
@Post('2fa/verify')
@UseGuards(JwtAuthGuard)
async verifyAndActivate(
@Request() req,
@Body() body: { twoFactorCode: string },
) {
const isValid = this.twoFactorService.verifyTwoFactorCode(
body.twoFactorCode,
req.user,
);
if (!isValid) {
throw new UnauthorizedException('Invalid two-factor code');
}
await this.usersService.enableTwoFactor(req.user.id);
return { message: 'Two-factor authentication has been enabled' };
}
Step 3: Login with 2FA
@Post('2fa/authenticate')
@UseGuards(JwtTwoFactorGuard)
async authenticate(
@Request() req,
@Body() body: { twoFactorCode: string },
) {
const isValid = this.twoFactorService.verifyTwoFactorCode(
body.twoFactorCode,
req.user,
);
if (!isValid) {
throw new UnauthorizedException('Invalid two-factor code');
}
return this.authService.generateTwoFactorToken(req.user);
}
Security Considerations
- Secret Storage: Store 2FA secrets securely using encryption in your database
- Account Recovery: Implement backup codes or alternate recovery methods
- Rate Limiting: Limit 2FA verification attempts to prevent brute-force attacks
- Trusted Devices: Consider adding "remember this device" functionality for better UX
User Experience Tips
- Provide clear setup instructions with both QR code and manual entry options
- Explain the importance of saving backup codes
- Confirm the user has set up 2FA correctly before fully enabling it
- Offer support for multiple authenticator apps