Back to all posts

Implementing Secure Authentication in NestJS with JWT & Refresh Tokens

S
sonhp
8 min read

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.

S

Written by sonhp

Technical writer and developer passionate about web technologies.

Related Articles