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:

  1. Client-side: Hapus token dari storage (localStorage/memory/cookie)
  2. Token Blacklist: Simpen token yang di-logout ke Redis dengan TTL sesuai expiration time
  3. 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:

  1. Secret key bocor: Jangan commit ke Git, pake environment variable
  2. XSS attack: Attacker bisa curi token dari localStorage. Pake HttpOnly cookie atau simpen di memory
  3. Man-in-the-middle: Pake HTTPS di production
  4. 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.