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:
- Auth Module – A dedicated module to manage authentication.
- User Module – Handles user creation, persistence, and retrieval.
- Guards – Control access to routes by verifying authentication.
- Strategies – Define how authentication is performed (JWT, sessions, OAuth, etc.).
-
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
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
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
You’ll also need a users module to manage user data:
nest g module users
nest g service users
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);
}
}
⚠️ 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;
}
}
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),
};
}
}
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 };
}
}
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 {}
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;
}
}
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') {}
// auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Step 10: Testing
- Login:
POST /auth/login
{ "username": "ajit", "password": "password123" }
→ Returns { "access_token": "..." }
- Access Profile:
GET /auth/profile
Authorization: Bearer <access_token>
→ 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)