🔑 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.

