Mini Project: JWT Auth di NestJS (Access + Refresh Token + Logout)

🔑 Intro

JWT itu udah jadi “standar anak muda” buat handle login di aplikasi web & mobile. Biasanya kita pake Access Token buat akses cepat, terus Refresh Token biar user nggak perlu login tiap 5 menit. Nah, jangan lupa juga ada fitur Logout biar token lama nggak bisa dipake lagi.

Di artikel ini, kita bakal bikin sistem JWT Auth di NestJS lengkap dengan:

  • Access Token (buat akses API sehari-hari)
  • Refresh Token (buat perpanjang session tanpa login ulang)
  • Logout (biar token invalid kalau user keluar)

Simple, aman, dan pastinya gampang diikutin step by step. Cocok banget buat kamu yang lagi belajar bikin backend auth di NestJS. 🔥

1️⃣ Install Package

Kalau belum:

npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt

2️⃣ Users Service (Dummy Data + Refresh Token)

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  private users = [
    { id: 1, username: 'alice', password: '1234', role: 'user', refreshToken: null },
    { id: 2, username: 'bob', password: '1234', role: 'admin', refreshToken: null },
  ];

  async findOne(username: string) {
    return this.users.find(user => user.username === username);
  }

  async saveRefreshToken(userId: number, refreshToken: string) {
    const user = this.users.find(u => u.id === userId);
    if (user) {
      user.refreshToken = await bcrypt.hash(refreshToken, 10);
    }
  }

  async removeRefreshToken(userId: number) {
    const user = this.users.find(u => u.id === userId);
    if (user) user.refreshToken = null;
  }

  async validateRefreshToken(userId: number, refreshToken: string) {
    const user = this.users.find(u => u.id === userId);
    if (!user || !user.refreshToken) return false;
    return bcrypt.compare(refreshToken, user.refreshToken);
  }
}

3️⃣ Auth Service

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

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

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

  async login(user: any) {
    const payload = { username: user.username, sub: user.id, role: user.role };

    const accessToken = this.jwtService.sign(payload, {
      secret: process.env.JWT_SECRET || 'superSecretKey',
      expiresIn: '15m',
    });

    const refreshToken = this.jwtService.sign(payload, {
      secret: process.env.JWT_REFRESH_SECRET || 'refreshSecretKey',
      expiresIn: '7d',
    });

    await this.usersService.saveRefreshToken(user.id, refreshToken);

    return {
      access_token: accessToken,
      refresh_token: refreshToken,
    };
  }

  async refreshToken(userId: number, refreshToken: string) {
    const isValid = await this.usersService.validateRefreshToken(userId, refreshToken);
    if (!isValid) throw new UnauthorizedException('Invalid refresh token');

    const user = await this.usersService.findOne(
      (await this.usersService.findOne('alice'))?.username ?? '',
    );

    const payload = { username: user.username, sub: user.id, role: user.role };

    return {
      access_token: this.jwtService.sign(payload, {
        secret: process.env.JWT_SECRET || 'superSecretKey',
        expiresIn: '15m',
      }),
    };
  }

  async logout(userId: number) {
    await this.usersService.removeRefreshToken(userId);
    return { message: 'Logged out successfully' };
  }
}

4️⃣ Auth Controller

// src/auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';

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

  @Post('login')
  async login(@Body() body: { username: string; password: string }) {
    const user = await this.authService.validateUser(body.username, body.password);
    return this.authService.login(user);
  }

  @Post('refresh')
  async refresh(@Body() body: { userId: number; refreshToken: string }) {
    return this.authService.refreshToken(body.userId, body.refreshToken);
  }

  @Post('logout')
  async logout(@Body() body: { userId: number }) {
    return this.authService.logout(body.userId);
  }
}

5️⃣ JWT Strategy (Access Token)

// src/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, 'jwt') {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET || 'superSecretKey',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username, role: payload.role };
  }
}

6️⃣ App Module

// src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [AuthModule],
})
export class AppModule {}

7️⃣ Testing Flow

1. Login

POST /auth/login
{ "username": "alice", "password": "1234" }

→ return access_token (15m) + refresh_token (7d)

2. Akses API pakai access_token

GET /profile
Authorization: Bearer <access_token>

3. Kalau access_token expired → refresh token

POST /auth/refresh
{ "userId": 1, "refreshToken": "<refresh_token>" }

→ return access_token baru

4. Logout

POST /auth/logout
{ "userId": 1 }

→ refresh token dihapus, user gak bisa refresh lagi

🎯 Kesimpulan

  • Access Token → buat akses API, expired cepat (aman).
  • Refresh Token → buat dapet Access Token baru, expired lebih lama.
  • Logout → hapus Refresh Token, biar user bener-bener gak bisa akses lagi.
Abdan Zam Zam Ramadhan
Abdan Zam Zam Ramadhan

Senior Software Engineer @ PT. Astra Internasional, Tbk.

Articles: 11