DEV Community

Khelan Mehta
Khelan Mehta

Posted on

Authentication System with Next.js and Nest.js

Image description
Image description
Image description


Building a Secure Authentication System with Next.js and Nest.js

Security is a top priority in modern web applications, and a well-architected authentication system is key to protecting user data. In this guide, we’ll build a secure authentication system using Next.js and Nest.js, implementing Crypto.js, bcrypt, JWT, and role-based authentication. Additionally, we'll cover how to integrate NodeMailer for password reset functionality with OTP-based sessions.

Why This Architecture?

Our system follows a modular and layered security approach:

Crypto.js (Frontend Encryption) – Encrypts credentials before transmission.

bcrypt (Backend Encryption) – Hashes passwords before storing them in the database.

JWT Authentication – Issues secure access tokens for API requests.

Role-Based Access Control (RBAC) – Restricts users based on roles.

OTP and NodeMailer for Password Reset – Provides secure session-based password recovery.


Step 1: Setting Up the Backend with Nest.js

1.1 Install Dependencies

In your Nest.js backend, install the required packages:

npm install @nestjs/jwt @nestjs/passport bcryptjs passport passport-jwt crypto-js nodemailer
Enter fullscreen mode Exit fullscreen mode

1.2 User Entity & Authentication Service

Define the user schema in TypeORM:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  password: string;

  @Column({ default: 'user' }) // Default role is 'user'
  role: string;
}
Enter fullscreen mode Exit fullscreen mode

Now, create the authentication service:

import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(private jwtService: JwtService) {}

  async hashPassword(password: string): Promise<string> {
    return bcrypt.hash(password, 10);
  }

  async comparePasswords(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
  }

  async generateJWT(email: string, role: string) {
    return this.jwtService.sign({ email, role });
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implementing Crypto.js in the Frontend (Next.js)

To enhance security, we encrypt credentials before sending them to the backend.

2.1 Install Crypto.js

npm install crypto-js
Enter fullscreen mode Exit fullscreen mode

2.2 Encrypt User Credentials in Next.js

Modify the login page (pages/login.js):

import CryptoJS from 'crypto-js';

async function login(email, password) {
  const encryptedPassword = CryptoJS.AES.encrypt(password, process.env.NEXT_PUBLIC_SECRET).toString();

  const response = await fetch('/api/auth/login', {
    method: 'POST',
    body: JSON.stringify({ email, password: encryptedPassword }),
  });

  const data = await response.json();
  console.log(data);
}
Enter fullscreen mode Exit fullscreen mode

Step 3: JWT-Based Authentication & Role-Based Access

3.1 Protect Routes Using JWT

Middleware for JWT authentication (jwt.strategy.ts):

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload) {
    return { email: payload.email, role: payload.role };
  }
}
Enter fullscreen mode Exit fullscreen mode

3.2 Role-Based Authorization Middleware

To allow access based on user roles, create a decorator:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return request.user.role === 'admin'; // Only admins can access
  }
}
Enter fullscreen mode Exit fullscreen mode

Apply the RolesGuard to protected routes:

@UseGuards(JwtAuthGuard, RolesGuard)
@Get('admin')
adminAccess() {
  return "You have admin privileges!";
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Implementing Forgot Password with OTP & NodeMailer

4.1 Generate OTP and Send Email

Install NodeMailer:

npm install nodemailer
Enter fullscreen mode Exit fullscreen mode

Create the email service:

import * as nodemailer from 'nodemailer';

@Injectable()
export class MailService {
  private transporter = nodemailer.createTransport({
    service: 'Gmail',
    auth: {
      user: process.env.EMAIL_USER,
      pass: process.env.EMAIL_PASS,
    },
  });

  async sendOTP(email: string, otp: string) {
    await this.transporter.sendMail({
      from: '"Security Team" <no-reply@example.com>',
      to: email,
      subject: 'Password Reset OTP',
      text: `Your OTP is ${otp}`,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

4.2 Store OTP in Database and Verify

Modify the AuthService:

import { v4 as uuidv4 } from 'uuid';

async requestPasswordReset(email: string) {
  const otp = uuidv4().substring(0, 6); // Generate a 6-digit OTP
  await this.userRepository.update({ email }, { otp });
  await this.mailService.sendOTP(email, otp);
}

async verifyOTP(email: string, otp: string, newPassword: string) {
  const user = await this.userRepository.findOne({ email });

  if (user.otp === otp) {
    user.password = await this.hashPassword(newPassword);
    user.otp = null;
    await this.userRepository.save(user);
    return 'Password reset successfully';
  } else {
    throw new Error('Invalid OTP');
  }
}
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

This authentication system ensures security at every layer:

🔒 Crypto.js encrypts credentials on the frontend.

🔒 bcrypt hashes passwords before storing in the database.

🔒 JWT tokens provide secure access.

🔒 Role-based access restricts sensitive data.

🔒 OTP-based password reset improves security.

With Next.js and Nest.js, we achieve a highly scalable, secure authentication system. 🚀


Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs