DEV Community

Ajit Forger
Ajit Forger

Posted on

Building Authentication Systems in Nest.js: A Complete Guide

Authentication is one of the most critical aspects of any modern web application. Without a solid auth system, your app is like a house with an open door. If you’re building with Nest.js, you already have a framework that prioritizes structure, scalability, and developer experience. Now, let’s see how to implement a robust authentication system in Nest.js — from the fundamentals to advanced practices.


Why Nest.js for Authentication?

Nest.js is built on top of Express (or optionally Fastify) and leverages TypeScript, decorators, and dependency injection. This makes it an excellent candidate for handling authentication because:

  • It integrates well with Passport.js, a popular authentication middleware.
  • Its modular architecture allows you to isolate auth logic in a clean, reusable way.
  • It supports advanced patterns like guards, interceptors, and custom strategies.

Core Building Blocks of Auth in Nest.js

Before diving into code, let’s break down the components involved:

  1. Auth Module – A dedicated module to manage authentication.
  2. User Module – Handles user creation, persistence, and retrieval.
  3. Guards – Control access to routes by verifying authentication.
  4. Strategies – Define how authentication is performed (JWT, sessions, OAuth, etc.).
  5. Decorators – For extracting authenticated user info easily (e.g., @User() decorator).

Step 1: Setting Up the Project

First, create a fresh Nest.js project if you don’t already have one:

npm i -g @nestjs/cli
nest new auth-demo
Enter fullscreen mode Exit fullscreen mode

Then, install the required dependencies for JWT-based authentication:

npm install @nestjs/passport passport passport-local passport-jwt bcrypt @nestjs/jwt
npm install --save-dev @types/passport-local @types/passport-jwt

Enter fullscreen mode Exit fullscreen mode

Step 2: Creating the Auth Module

Nest encourages modular design, so let’s generate an auth module:

nest g module auth
nest g service auth
nest g controller auth
Enter fullscreen mode Exit fullscreen mode

You’ll also need a users module to manage user data:

nest g module users
nest g service users
Enter fullscreen mode Exit fullscreen mode

Step 3: Implementing User Service

Your UserService should handle user lookups and persistence. For simplicity, let’s mock it with in-memory users:

// users/users.service.ts
import { Injectable } from '@nestjs/common';

export type User = any;

@Injectable()
export class UsersService {
  private readonly users = [
    {
      userId: 1,
      username: 'ajit',
      password: 'password123', // in real life -> hashed!
    },
  ];

  async findOne(username: string): Promise<User | undefined> {
    return this.users.find(user => user.username === username);
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Note: Never store plaintext passwords. Always hash using bcrypt.


Step 4: Local Strategy (Login with Username & Password)

Nest.js uses Passport strategies. Let’s implement a Local Strategy:

// auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super(); // default expects 'username' and 'password'
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Auth Service

The AuthService validates users and issues JWTs:

// auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';

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

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: JWT Strategy

To protect routes, we need a JWT strategy:

// auth/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(),
      ignoreExpiration: false,
      secretOrKey: 'SECRET_KEY', // Use env vars in prod!
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Auth Module Configuration

Wire everything up in AuthModule:

// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { AuthController } from './auth.controller';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: 'SECRET_KEY', // move to process.env
      signOptions: { expiresIn: '60m' },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

Step 8: Auth Controller

Expose login and protected routes:

// auth/auth.controller.ts
import { Controller, Request, Post, UseGuards, Get } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './local-auth.guard';
import { JwtAuthGuard } from './jwt-auth.guard';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @UseGuards(LocalAuthGuard)
  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }

  @UseGuards(JwtAuthGuard)
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 9: Guards

Create guards to wrap around the strategies:

// auth/local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
Enter fullscreen mode Exit fullscreen mode
// auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Enter fullscreen mode Exit fullscreen mode

Step 10: Testing

  1. Login:
   POST /auth/login
   { "username": "ajit", "password": "password123" }
Enter fullscreen mode Exit fullscreen mode

→ Returns { "access_token": "..." }

  1. Access Profile:
   GET /auth/profile
   Authorization: Bearer <access_token>
Enter fullscreen mode Exit fullscreen mode

→ Returns { "userId": 1, "username": "ajit" }


Best Practices

  • Never store plaintext passwords — always hash with bcrypt.
  • Use environment variables for secrets and config.
  • Implement refresh tokens for long-lived sessions.
  • Add role-based access control (RBAC) using custom decorators and guards.
  • Consider OAuth2 or SSO for enterprise-level apps.

Conclusion

Nest.js makes authentication clean and maintainable thanks to its modular structure and Passport integration. With just a few steps, we set up a system that supports local login and JWT-protected routes. From here, you can extend it with refresh tokens, roles, or third-party providers like Google or GitHub.

Top comments (0)