Implementing 2FA in NestJS: A Step-by-Step Guide for Better Security
Two-factor authentication (2FA) is an important security feature that provides an extra layer of security to user accounts by demanding two forms of identity before giving access. Applications may dramatically improve their security posture and safeguard against illegal access by adding 2FA.
NestJS is a powerful Node.js framework, while Passport is a flexible authentication middleware. Enabling 2FA in a NestJS application requires integrating Google Authenticator, which uses a Time-Based One-Time Password (TOTP) to generate temporary codes.
The process involves configuring modules, implementing endpoints, and protecting sensitive routes. QR codes are also explored for easy setup and integration.
By following this guide, users can enhance their security posture and protect against unauthorized access.
Install necessary packages:
First, you must install the NestJS, Passport, and Google Authenticator packages.
npm install @nestjs/passport passport passport-local passport-jwt passport-google-authenticator
Configure Passport and NestJS modules:
Install the required modules in your NestJS application module. This contains the PassportModule as well as any other modules you may require (e.g., UsersModule, AuthService, and so on).
const { Module } = require('@nestjs/common');
const { PassportModule } = require('@nestjs/passport');
const { JwtModule } = require('@nestjs/jwt');
const { UsersModule } = require('./users/users.module');
const { AuthService } = require('./auth/auth.service');
const { JwtStrategy } = require('./auth/jwt.strategy');
@Module({
imports: [
PassportModule,
UsersModule,
JwtModule.register({
secret: 'your_secret_key',
signOptions: { expiresIn: '1h' }, // JWT token expiration time
}),
],
providers: [AuthService, JwtStrategy],
})
class AppModule {}
module.exports = AppModule;
const { Injectable, UnauthorizedException } = require('@nestjs/common');
const { PassportStrategy } = require('@nestjs/passport');
const { Strategy, ExtractJwt } = require('passport-jwt');
const { AuthService } = require('./auth.service');
const { JwtPayload } = require('./interfaces/jwt-payload.interface');
@Injectable()
class JwtStrategy extends PassportStrategy(Strategy) {
constructor(authService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'your_secret_key', // Replace this with your own secret key (same as used in AppModule)
});
this.authService = authService;
}
async validate(payload) {
const user = await this.authService.validateUser(payload);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
module.exports = JwtStrategy;
Create a 2FA Service
Create TwoFactorAuthenticationService with methods for generating and validating 2FA secrets.
const { Injectable } = require('@nestjs/common');
const { JwtService } = require('@nestjs/jwt');
const { UsersService } = require('../users/users.service');
@Injectable()
class AuthService {
constructor(usersService, jwtService) {
this.usersService = usersService;
this.jwtService = jwtService;
}
async validateUser(payload) {
const user = await this.usersService.findById(payload.sub);
return user;
}
async generateJwtToken(user) {
// Generate a JWT token based on the user's data.
const payload = { sub: user.id, username: user.username };
return this.jwtService.sign(payload);
}
}
module.exports = AuthService;
Create a 2FA Controller
Create a 2FA-related controller for endpoint interaction.
const { Injectable } = require('@nestjs/common');
const speakeasy = require('speakeasy');
@Injectable()
class TwoFactorAuthenticationService {
generateTwoFactorAuthenticationSecret(username) {
const secret = speakeasy.generateSecret({
name: `ByteScrum Custom App:${username}`,
});
return secret.base32;
}
generateTwoFactorAuthenticationToken(secret) {
return speakeasy.totp({
secret,
encoding: 'base32',
});
}
validateTwoFactorAuthenticationToken(token, secret) {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 1,
}
}
module.exports = TwoFactorAuthenticationService;
Generate 2FA Secret and QR Code
Create a TwoFactorAuthenticationController endpoint for 2FA secret generation and QR code URL, enabling user authentication using the Google Authenticator app.
const { Controller, Get, Req, Res } = require('@nestjs/common');
const { TwoFactorAuthenticationService } = require('./two-factor-authentication.service');
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
@Controller('2fa')
class TwoFactorAuthenticationController {
constructor(twoFactorAuthenticationService) {
this.twoFactorAuthenticationService = twoFactorAuthenticationService;
}
@Get('generate')
async generateTwoFactorAuth(req, res) {
const user = req.user;
if (!user) {
return res.status(401).json({ message: 'User not authenticated' });
}
if (user.isTwoFactorAuthenticationEnabled) {
return res.status(400).json({ message: '2FA already enabled!' });
}
const secret = this.twoFactorAuthenticationService.generateTwoFactorAuthenticationSecret(user.username);
const otpAuthUrl = speakeasy.otpauthURL({
secret: secret,
label: `YourApp:${user.username}`,
issuer: 'YourApp',
});
try {
const qrCodeDataURL = await QRCode.toDataURL(otpAuthUrl);
return res.status(200).json({ qrCode: qrCodeDataURL });
} catch (error) {
console.error('Error generating QR code:', error);
return res.status(500).json({ message: 'Error generating QR code' });
}
}
}
module.exports = TwoFactorAuthenticationController;
Enable 2FA for the User
Create TwoFactorAuthenticationController endpoint to enable 2FA for authenticated users, verifying Google Authenticator code.
const { Controller, Post, Body, Req, Res } = require('@nestjs/common');
const { TwoFactorAuthenticationService } = require('./two-factor-authentication.service');
const speakeasy = require('speakeasy');
@Controller('2fa')
class TwoFactorAuthenticationController {
constructor(twoFactorAuthenticationService) {
this.twoFactorAuthenticationService = twoFactorAuthenticationService;
}
@Post('enable')
async enableTwoFactorAuth(@Req() req, @Body() body, @Res() res) {
const user = req.user;
if (!user) {
return res.status(401).json({ message: 'User not authenticated' });
}
if (user.isTwoFactorAuthenticationEnabled) {
return res.status(400).json({ message: '2FA already enabled!' });
}
const { token } = body;
const isValidToken = this.twoFactorAuthenticationService.validateTwoFactorAuthenticationToken(
token,
user.twoFactorAuthenticationSecret
);
if (!isValidToken) {
return res.status(401).json({ message: 'Invalid 2FA token' });
}
return res.status(200).json({ message: '2FA enabled successfully' });
}
}
module.exports = TwoFactorAuthenticationController;
Protect Sensitive Routes with 2FA
Custom Passport strategy protects sensitive routes, checks 2FA enabled, prompts 2FA code, and generates code.
const { Injectable, UnauthorizedException } = require('@nestjs/common');
const { PassportStrategy } = require('@nestjs/passport');
const { Strategy } = require('passport-custom');
const speakeasy = require('speakeasy');
@Injectable()
class GoogleAuthenticatorStrategy extends PassportStrategy(Strategy, 'google-authenticator') {
constructor() {
super();
}
async validate(req) {
const user = req.user;
if (!user) {
throw new UnauthorizedException('User not authenticated');
}
if (!user.isTwoFactorAuthenticationEnabled) {
throw new UnauthorizedException('2FA not enabled');
}
const token = req.body.token;
const isValidToken = this.validateTwoFactorToken(token, user.twoFactorAuthenticationSecret);
if (!isValidToken) {
throw new UnauthorizedException('Invalid 2FA token');
}
return user;
}
validateTwoFactorToken(token, secret) {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 1,
});
}
}
module.exports = GoogleAuthenticatorStrategy;
Validate 2FA Code for Protected Routes
Implement a custom Passport strategy verification process for 2FA code validation.
const { Injectable, UnauthorizedException } = require('@nestjs/common');
const { PassportStrategy } = require('@nestjs/passport');
const { Strategy } = require('passport-custom');
const speakeasy = require('speakeasy');
@Injectable()
class GoogleAuthenticatorStrategy extends PassportStrategy(Strategy, 'google-authenticator') {
constructor() {
super();
}
async validate(req) {
const user = req.user;
if (!user) {
throw new UnauthorizedException('User not authenticated');
}
if (!user.isTwoFactorAuthenticationEnabled) {
throw new UnauthorizedException('2FA not enabled');
}
const token = req.body.token;
const isValidToken = this.validateTwoFactorToken(token, user.twoFactorAuthenticationSecret);
if (!isValidToken) {
throw new UnauthorizedException('Invalid 2FA token');
}
return user;
}
validateTwoFactorToken(token, secret) {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 1,
});
}
}
module.exports = GoogleAuthenticatorStrategy;