Introduction
Adding Two-Factor Authentication (2FA) enhances the security of user accounts by mandating a secondary form of verification alongside the password. This guide will demonstrate how to incorporate 2FA into a NestJS application using the@nestjs/passport
module and the
speakeasy
library for creating and authenticating one-time passwords (OTPs).
Setting Up the NestJS Project
Let's start by setting up a new NestJS project and installing all the required dependencies:
$ nest new nest-2fa-example
$ cd nest-2fa-example
$ npm install @nestjs/passport passport passport-local speakeasy
Creating the User Entity
We'll create a User
entity with fields for the user's email, password hash, and secret key for 2FA.
// user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column({ nullable: true })
secretKey: string; // Secret key for 2FA
}
Implementing Authentication Strategies
We'll use Passport.js with the @nestjs/passport
module to implement local and 2FA authentication strategies.
Local Strategy
// local.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(email: string, password: string): Promise<any> {
return this.authService.validateUser(email, password);
}
}
2FA Strategy
// twofa.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './auth.service';
@Injectable()
export class TwoFaStrategy extends PassportStrategy(Strategy, '2fa') {
constructor(private authService: AuthService) {
super();
}
async validate(email: string, token: string): Promise<any> {
return this.authService.validateTwoFa(email, token);
}
}
Implementing Authentication Service
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from './users.service';
import { speakeasy } from 'speakeasy';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async validateUser(email: string, password: string): Promise<any> {
const user = await this.usersService.findByEmail(email);
if (user && user.password === password) {
return user;
}
throw new UnauthorizedException();
}
async generateTwoFaSecret(email: string): Promise<string> {
const secret = speakeasy.generateSecret({ length: 20 }).base32;
await this.usersService.updateTwoFaSecret(email, secret);
return secret;
}
async validateTwoFa(email: string, token: string): Promise<any> {
const user = await this.usersService.findByEmail(email);
if (!user || !user.secretKey) {
throw new UnauthorizedException();
}
const isValidToken = speakeasy.totp.verify({
secret: user.secretKey,
encoding: 'base32',
token,
window: 1,
});
if (isValidToken) {
return user;
}
throw new UnauthorizedException();
}
}
Controller Endpoints
Below is an example of how you can implement controller endpoints for user registration, login, and enabling/disabling 2FA in a NestJS application:
// auth.controller.ts
import { Controller, Post, Body, Req, UseGuards, Get } from '@nestjs/common';
import { AuthService } from './auth.service';
import { CreateUserDto } from './dto/create-user.dto';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { Request } from 'express';
import { TwoFaAuthGuard } from './guards/twofa-auth.guard';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
async register(@Body() createUserDto: CreateUserDto) {
return this.authService.register(createUserDto);
}
@Post('login')
@UseGuards(LocalAuthGuard)
async login(@Req() req: Request) {
return this.authService.login(req.user);
}
@Post('twofa/enable')
async enableTwoFa(@Req() req: Request) {
return this.authService.enableTwoFa(req.user.email);
}
@Post('twofa/disable')
@UseGuards(TwoFaAuthGuard)
async disableTwoFa(@Req() req: Request) {
return this.authService.disableTwoFa(req.user.email);
}
@Post('twofa/verify')
@UseGuards(TwoFaAuthGuard)
async verifyTwoFa(@Req() req: Request) {
return this.authService.verifyTwoFa(req.user.email, req.body.token);
}
@Get('twofa/secret')
@UseGuards(TwoFaAuthGuard)
async getTwoFaSecret(@Req() req: Request) {
return this.authService.getTwoFaSecret(req.user.email);
}
}
In this example:
-
/auth/register
: This endpoint allows users to register by providing their email and password. -
/auth/login
: This endpoint allows users to log in using their email and password. -
/auth/twofa/enable
: This endpoint enables 2FA for the authenticated user. -
/auth/twofa/disable
: This endpoint disables 2FA for the authenticated user. -
/auth/twofa/verify
: This endpoint verifies the 2FA token provided by the user during login. -
/auth/twofa/secret
: This endpoint retrieves the secret key for 2FA, which can be used for setting up authenticator apps like Google Authenticator.
Ensure that you have corresponding DTOs (such as CreateUserDto
) and guards (such as LocalAuthGuard
and TwoFaAuthGuard
) defined to handle input validation and authentication logic properly. Also, make sure to implement the necessary service methods in AuthService
to handle user registration, login, and 2FA-related operations.
DTOs
DTO stands for Data Transfer Object. In NestJS, DTOs are plain JavaScript/TypeScript objects used to define the structure of data being transferred between the client and server. They help ensure type safety, validate input data, and provide a clear contract between the client and server.
Here's an example of a DTO for user registration:
// create-user.dto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@IsString()
@MinLength(6)
password: string;
}
In this DTO:
-
@IsNotEmpty()
: Ensures that the email and password fields are not empty. -
@IsEmail()
: Validates that the email field is a valid email address. -
@IsString()
: Validates that the password field is a string. -
@MinLength(6)
: Specifies that the password must be at least 6 characters long.
You can define similar DTOs for other operations such as login, enabling/disabling 2FA, etc.
Summary:
Exploring the implementation of Two-Factor Authentication (2FA) in a NestJS application can be a valuable addition to your security measures. By utilizing the @nestjs/passport
module in conjunction with the speakeasy
library for generating and verifying one-time passwords (OTPs), you can bolster the protection of user accounts and fortify your application against potential security breaches. Incorporating 2FA
not only enhances the overall security posture of your application but also instills a sense of trust and confidence among your users.
Hope you find this helpful!!!!
Do comment your suggestions and questions....
Two-Factor Authentication (2FA) in NestJS: A Comprehensive Guide