Implementing Secure Authentication in NestJS with JWT & Refresh Tokens
Authentication is a critical aspect of any application. In this guide, we'll walk through implementing a secure authentication system in NestJS using JWT (JSON Web Tokens) and refresh tokens.
Why Use Refresh Tokens?
A common authentication pattern involves:
✅ Short-lived access tokens (e.g., 15 minutes) to reduce security risks.
✅ Long-lived refresh tokens (e.g., 7 days) to renew access without re-authentication.
✅ Automatic token rotation to prevent stolen refresh tokens from being misused.
1. Setting Up JWT in NestJS
Installing Required Dependencies
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install --save-dev @types/passport-jwt @types/bcrypt
2. Creating the Authentication Module
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '../user/user.module';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET || 'supersecretkey',
signOptions: { expiresIn: '15m' }, // Access token expires in 15 min
}),
UserModule,
],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
3. Implementing JWT Strategy
Create a JWT strategy for validating tokens:
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 || 'supersecretkey',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
4. Generating JWT Access & Refresh Tokens
Modify AuthService
to generate both access and refresh tokens:
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
async generateTokens(user: any) {
const payload = { username: user.username, sub: user.id };
const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
return { accessToken, refreshToken };
}
async hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
async comparePasswords(
password: string,
hashedPassword: string,
): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}
}
5. Handling Refresh Tokens Securely
To refresh access tokens, store refresh tokens securely and rotate them on every refresh request.
Modify AuthService
to include refresh token handling:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
async refreshAccessToken(refreshToken: string) {
try {
const decoded = this.jwtService.verify(refreshToken);
return this.jwtService.sign(
{ username: decoded.username, sub: decoded.sub },
{ expiresIn: '15m' },
);
} catch (error) {
throw new UnauthorizedException('Invalid refresh token');
}
}
}
6. Implementing Authentication Endpoints
Modify AuthController
to handle login, token refresh, and logout:
import {
Controller,
Post,
Body,
Res,
HttpCode,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
@HttpCode(200)
async login(
@Body() userCredentials: { username: string; password: string },
@Res() res,
) {
// Validate user credentials (mock example)
const user = { id: 1, username: userCredentials.username }; // Replace with real DB check
const { accessToken, refreshToken } =
await this.authService.generateTokens(user);
return res.json({ accessToken, refreshToken });
}
@Post('refresh')
@HttpCode(200)
async refreshToken(@Body('refreshToken') refreshToken: string, @Res() res) {
if (!refreshToken) {
throw new UnauthorizedException('Refresh token required');
}
const newAccessToken =
await this.authService.refreshAccessToken(refreshToken);
return res.json({ accessToken: newAccessToken });
}
}
7. Enhancing Security
✅ Store refresh tokens securely – Save in an HTTP-only cookie instead of local storage.
✅ Use token rotation – Issue a new refresh token every time the old one is used.
✅ Invalidate old refresh tokens – Store refresh tokens in the database and revoke them if needed.
Conclusion
Implementing JWT authentication in NestJS ensures secure and scalable user authentication. By leveraging access and refresh tokens, we can:
✔ Reduce security risks with short-lived access tokens.
✔ Enhance user experience with refresh token-based re-authentication.
✔ Improve security with token rotation and revocation mechanisms.