Cara Buat Authentication dengan JWT
Kenapa Gue Mulai Pakai JWT untuk Authentication
Dulu gue pake session-based auth buat semua project. Setiap user login, server bikin session, simpen di memory atau Redis, terus kirim session ID lewat cookie. Simple? Iya. Tapi masalahnya muncul waktu gue harus scale aplikasi ke multiple server. Session harus di-share antar server, atau pake sticky session yang ribet.
Terus gue coba JWT (JSON Web Token) dan langsung jatuh cinta. Stateless, ga perlu simpen session di server, bisa dipake lintas platform (web, mobile, desktop), dan scalable banget. Di artikel ini gue bakal tunjukin cara buat authentication system pake JWT dari nol, lengkap dengan contoh kode yang bisa langsung kamu pake.
JWT itu basically string encoded yang berisi data user (payload) plus signature buat verifikasi. Format standardnya: header.payload.signature. Server cuma perlu verify signature tanpa harus cek database atau Redis setiap request. Efficient banget.
Persiapan Environment untuk JWT Authentication
Sebelum mulai coding, setup dulu environment-nya. Gue pakai Node.js + Express karena simple dan banyak library JWT-nya. Kamu juga butuh MongoDB buat simpen data user. Kalau belum install MongoDB, bisa ikutin Tutorial MongoDB + Node.js dulu.
Install dependencies yang dibutuhin:
npm init -y
npm install express jsonwebtoken bcryptjs mongoose dotenv
npm install --save-dev nodemon
Struktur folder project gue biasanya kayak gini:
jwt-auth/
├── config/
│ └── database.js
├── middleware/
│ └── authMiddleware.js
├── models/
│ └── User.js
├── routes/
│ └── authRoutes.js
├── controllers/
│ └── authController.js
├── .env
└── server.js
Bikin file .env buat simpen secret key dan config:
PORT=5000
MONGO_URI=mongodb://localhost:27017/jwt_auth_demo
JWT_SECRET=rahasia_jwt_kamu_ganti_dengan_random_string_panjang_123456
JWT_EXPIRE=7d
Tips: Jangan pernah commit .env ke Git. Tambahin ke .gitignore biar aman. Secret key JWT harus random dan panjang minimal 32 karakter.
Step-by-Step Implementasi JWT Authentication
Buat Model User
File models/User.js:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, 'Username wajib diisi'],
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: [true, 'Email wajib diisi'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, 'Format email tidak valid']
},
password: {
type: String,
required: [true, 'Password wajib diisi'],
minlength: 6,
select: false
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
createdAt: {
type: Date,
default: Date.now
}
});
// Hash password sebelum save
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const saltRounds = 12;
this.password = await bcrypt.hash(this.password, saltRounds);
next();
});
// Method buat compare password
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
Buat Controller untuk Register & Login
File controllers/authController.js:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// Generate JWT Token
const generateToken = (userId, role) => {
return jwt.sign(
{ id: userId, role: role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
);
};
// Register User Baru
exports.register = async (req, res) => {
try {
const { username, email, password } = req.body;
// Cek apakah user sudah ada
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(400).json({
success: false,
message: 'Username atau email sudah terdaftar'
});
}
// Buat user baru
const newUser = await User.create({
username,
email,
password
});
// Generate token
const token = generateToken(newUser._id, newUser.role);
res.status(201).json({
success: true,
message: 'Registrasi berhasil',
token,
user: {
id: newUser._id,
username: newUser.username,
email: newUser.email,
role: newUser.role
}
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Error saat registrasi',
error: error.message
});
}
};
// Login User
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Validasi input
if (!email || !password) {
return res.status(400).json({
success: false,
message: 'Email dan password harus diisi'
});
}
// Cari user dan include password field
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
message: 'Email atau password salah'
});
}
// Verify password
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: 'Email atau password salah'
});
}
// Generate token
const token = generateToken(user._id, user.role);
res.status(200).json({
success: true,
message: 'Login berhasil',
token,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Error saat login',
error: error.message
});
}
};
Buat Middleware untuk Verify Token
File middleware/authMiddleware.js:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.protect = async (req, res, next) => {
let token;
// Ambil token dari header Authorization
if (req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
// Cek apakah token ada
if (!token) {
return res.status(401).json({
success: false,
message: 'Akses ditolak. Token tidak ditemukan'
});
}
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Ambil data user dari database
req.user = await User.findById(decoded.id).select('-password');
if (!req.user) {
return res.status(401).json({
success: false,
message: 'User tidak ditemukan'
});
}
next();
} catch (error) {
return res.status(401).json({
success: false,
message: 'Token tidak valid atau expired',
error: error.message
});
}
};
// Middleware untuk role-based authorization
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: 'Akses ditolak. Role tidak memiliki permission'
});
}
next();
};
};
Setup Routes dan Server
File routes/authRoutes.js:
const express = require('express');
const router = express.Router();
const { register, login } = require('../controllers/authController');
const { protect, authorize } = require('../middleware/authMiddleware');
router.post('/register', register);
router.post('/login', login);
// Protected route contoh
router.get('/profile', protect, (req, res) => {
res.json({
success: true,
user: req.user
});
});
// Admin only route
router.get('/admin', protect, authorize('admin'), (req, res) => {
res.json({
success: true,
message: 'Welcome admin!'
});
});
module.exports = router;
File server.js:
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const authRoutes = require('./routes/authRoutes');
const app = express();
// Middleware
app.use(express.json());
// Database connection
mongoose.connect(process.env.MONGO_URI)
.then(() => console.log('MongoDB connected'))
.catch(err => console.log('MongoDB error:', err));
// Routes
app.use('/api/auth', authRoutes);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Jalankan server:
npm run dev
Best Practices JWT Authentication
1. Token Expiration Strategy
Jangan bikin token yang expired-nya terlalu lama. Gue biasa pake:
- Access Token: 15 menit - 1 jam (untuk request API)
- Refresh Token: 7-30 hari (untuk generate access token baru)
Implementasi refresh token:
exports.refreshToken = async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
success: false,
message: 'Refresh token tidak ditemukan'
});
}
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({
success: false,
message: 'User tidak ditemukan'
});
}
// Generate access token baru
const newAccessToken = generateToken(user._id, user.role);
res.json({
success: true,
accessToken: newAccessToken
});
} catch (error) {
res.status(401).json({
success: false,
message: 'Refresh token tidak valid'
});
}
};
2. Store Token dengan Aman
Di frontend, jangan simpen JWT di localStorage karena vulnerable terhadap XSS attack. Opsi yang lebih aman:
- HttpOnly Cookie: Browser auto kirim, ga bisa diakses JavaScript
- Memory: Simpen di state management (Redux, Zustand), tapi hilang kalau refresh
Kalau kamu deploy ke production, pastikan pake HTTPS dan set cookie dengan flag secure dan httpOnly. Untuk deployment yang scalable, bisa ikutin Tutorial Docker Compose buat containerize aplikasi.
3. Tambahkan Rate Limiting
Protect endpoint login dari brute force attack pake rate limiter:
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 menit
max: 5, // max 5 request
message: 'Terlalu banyak percobaan login. Coba lagi setelah 15 menit'
});
router.post('/login', loginLimiter, login);
4. Payload JWT Jangan Terlalu Besar
JWT dikirim di setiap request, jadi jangan masukin data besar ke payload. Cukup simpen ID user dan role. Kalau butuh data lengkap, fetch dari database atau Redis cache.
Perbandingan Session vs JWT Authentication
| Aspek | Session-Based | JWT |
|---|---|---|
| Storage | Server-side (memory/Redis) | Client-side (token) |
| Scalability | Perlu sticky session atau shared storage | Stateless, mudah scale horizontal |
| Security | Server kontrol penuh, bisa revoke instant | Ga bisa revoke sebelum expired |
| Network | Hanya kirim session ID | Kirim full token (lebih besar) |
| Complexity | Simple, tapi ribet di microservices | Lebih complex, tapi cocok untuk API |
| Biaya Infra | Butuh Redis (~$10-50/bulan di cloud) | Ga perlu storage tambahan |
Gue personally prefer JWT untuk:
- RESTful API yang dikonsumsi multiple client (web, mobile, desktop)
- Microservices architecture
- Single Page Application (SPA)
Session-based lebih cocok untuk:
- Monolithic app sederhana
- Website yang butuh session management kompleks
- Kalau perlu revoke access secara instant
Troubleshooting Common Issues
“JsonWebTokenError: invalid signature”
Biasanya karena JWT_SECRET di .env beda antara waktu generate token dan verify token. Pastikan secret key konsisten di semua environment.
“TokenExpiredError: jwt expired”
Token udah mati. Implement refresh token atau extend expiration time. Jangan lupa handle di frontend dengan redirect ke login page.
Password ga ke-hash
Cek middleware pre('save') di model User. Pastikan bcryptjs udah di-import dan function async/await jalan dengan benar.
Token ga kedetect di middleware
Format header harus: Authorization: Bearer <token>. Cek space setelah “Bearer” dan pastikan token di-attach dengan benar di frontend:
// Contoh fetch dengan JWT
fetch('/api/auth/profile', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
CORS Error saat hit API dari frontend
Tambahin CORS middleware:
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}));
FAQ
Apakah JWT aman untuk authentication production?
JWT aman asalkan kamu ikutin best practices: pake HTTPS, set expiration time yang pendek, jangan simpen data sensitif di payload, dan validasi token dengan benar. Kelemahan JWT adalah ga bisa di-revoke sebelum expired, jadi pake refresh token strategy dan keep access token expiration pendek (15-60 menit). Untuk aplikasi yang butuh instant revoke (kayak banking), consider kombinasi JWT dengan blacklist di Redis atau pake session-based auth.
Bagaimana cara logout dengan JWT?
Karena JWT stateless, ga ada cara buat “logout” dari server side. Solusinya ada beberapa:
- Client-side: Hapus token dari storage (localStorage/memory/cookie)
- Token Blacklist: Simpen token yang di-logout ke Redis dengan TTL sesuai expiration time
- Short-lived Token: Pake access token yang expired cepat (15 menit) + refresh token
Implementasi blacklist di Redis cukup simple. Setiap user logout, tambahin token ke Redis. Di middleware, cek dulu apakah token ada di blacklist sebelum verify.
Apakah bisa pakai JWT tanpa database untuk verify token?
Bisa! Itu salah satu kelebihan JWT. Token sudah self-contained dengan signature yang bisa di-verify tanpa database lookup. Tapi gue tetep recommend fetch minimal data user dari database setelah verify token buat mastiin user masih aktif dan datanya up-to-date. Kalau butuh performance maksimal, cache user data di Redis dengan TTL pendek (5-15 menit).
Berapa ukuran JWT yang ideal dan apakah ada limit?
JWT biasanya 200-500 bytes tergantung payload. Ga ada hard limit, tapi jangan bikin terlalu besar karena dikirim di setiap request. Beberapa web server punya limit header size (biasanya 8KB). Best practice: cuma simpen user ID, role, dan issued/expiration time. Kalau butuh data lengkap, fetch dari API terpisah atau cache.
Bagaimana cara implement role-based access control dengan JWT?
Simpen role di payload JWT waktu generate token, terus buat middleware yang cek role sebelum akses route tertentu. Contoh udah gue tunjukin di middleware authorize() di atas. Kamu bisa extend dengan permission yang lebih granular, misalnya simpen array permissions di payload atau fetch dari database. Tapi inget, jangan simpen terlalu banyak data di JWT biar token ga terlalu besar.
Apakah JWT bisa di-hack atau di-forge?
JWT pake cryptographic signature (HMAC atau RSA) yang sangat kuat. Selama secret key kamu aman dan panjang (minimal 32 karakter random), praktis ga mungkin di-forge. Yang perlu diwaspadai:
- Secret key bocor: Jangan commit ke Git, pake environment variable
- XSS attack: Attacker bisa curi token dari localStorage. Pake HttpOnly cookie atau simpen di memory
- Man-in-the-middle: Pake HTTPS di production
- Token tidak expired: Set expiration time yang reasonable
Kalau kamu concern soal security, bisa pake algoritma RS256 (RSA) yang pake public/private key pair daripada HS256 (HMAC) yang cuma pake single secret key.
Penutup
JWT authentication udah jadi standard de facto buat modern API development. Stateless, scalable, dan cocok banget buat arsitektur microservices atau aplikasi yang punya multiple client. Kombinasikan dengan best practices kayak short-lived token, refresh token strategy, dan secure storage biar aplikasi kamu aman dan performant.
Source code lengkap yang gue tulis di artikel ini bisa langsung kamu clone dan modifikasi sesuai kebutuhan project. Jangan lupa test security-nya dengan tools kayak OWASP ZAP atau Burp Suite sebelum deploy ke production.
Kalau kamu butuh scale aplikasi JWT ini, consider pake load balancer dan deploy ke multiple server. Karena JWT stateless, horizontal scaling jadi super mudah. Cek Perbandingan Cloud Provider buat pilih hosting yang sesuai budget kamu. Server di Singapore biasanya latency-nya bagus dari Indonesia (20-50ms).
Kalau ada pertanyaan atau butuh bantuan, langsung email ke [email protected]! Aku siap bantu.