SavefileArchive
USD/IDR ...
|
BTC ...
|
ETH ...
|
GOLD/gram ...
Terbaru
SavefileArchive — Tutorial coding, tips programming, dan dunia musik untuk developer & pecinta musik Indonesia
JWT Error: JsonWebTokenError, TokenExpiredError, dan Cara Fixnya

JWT Error: JsonWebTokenError, TokenExpiredError, dan Cara Fixnya

JWT Error: JsonWebTokenError, TokenExpiredError, dan Cara Fixnya

Ilustrasi JWT Token Error

JWT (JSON Web Token) adalah standar autentikasi yang dipakai hampir semua aplikasi modern. Tapi error JWT bisa sangat membingungkan — terutama karena pesan error-nya sering tidak menjelaskan mengapa token tidak valid. Artikel ini membahas semua error JWT yang umum dan cara mengatasinya.


1. Jenis-jenis JWT Error

JsonWebTokenError: invalid signature

// Penyebab paling umum:
// 1. JWT_SECRET berbeda antara yang dipakai saat sign dan verify
// 2. Token dimodifikasi setelah dibuat
// 3. Token dari environment berbeda (dev token dipakai di production)

// Debug: cek secret yang dipakai
console.log('Secret length:', process.env.JWT_SECRET?.length);
// Jika undefined atau berbeda antara service → ini penyebabnya

// Fix: pastikan JWT_SECRET sama di semua service yang perlu verify token
// Gunakan secret yang sama dari environment variable, bukan hardcode

// ❌ Jangan hardcode secret
const token = jwt.sign(payload, 'mysecret123');

// ✅ Selalu dari environment variable
const token = jwt.sign(payload, process.env.JWT_SECRET);

TokenExpiredError: jwt expired

// Token sudah melewati waktu expiry-nya
// Ini adalah behavior yang BENAR — bukan bug

// Cara handle dengan benar:
function verifyToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      // Token expired — minta user refresh token atau login ulang
      throw new Error('SESSION_EXPIRED');
    }
    if (err.name === 'JsonWebTokenError') {
      // Token tidak valid — kemungkinan dimanipulasi
      throw new Error('INVALID_TOKEN');
    }
    throw err;
  }
}

// Di middleware Express:
export function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'Token tidak ditemukan' });
  }

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        error: 'Sesi login telah berakhir',
        code: 'TOKEN_EXPIRED'  // Frontend bisa cek code ini untuk auto-refresh
      });
    }
    return res.status(401).json({ error: 'Token tidak valid' });
  }
}

JsonWebTokenError: jwt malformed

// Token tidak dalam format JWT yang valid (header.payload.signature)
// Penyebab umum:
// 1. Frontend mengirim token tanpa prefix "Bearer "
// 2. Token terpotong saat disimpan (localStorage limit, cookie size)
// 3. Token mengandung karakter yang tidak valid

// Cek format token:
const authHeader = req.headers.authorization;
console.log('Auth header:', authHeader);
// Harus: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

// Fix di frontend — pastikan format pengiriman benar:
fetch('/api/users', {
  headers: {
    'Authorization': `Bearer ${token}`,  // Perhatikan spasi setelah "Bearer"
  }
});

// Cek apakah token tersimpan dengan benar:
const stored = localStorage.getItem('token');
console.log('Token starts with eyJ:', stored?.startsWith('eyJ'));
// JWT selalu dimulai dengan "eyJ" (base64 dari {"alg":...)

NotBeforeError: jwt not active

// Token punya claim "nbf" (not before) yang belum tercapai
// Jarang terjadi tapi bisa muncul jika ada clock skew antar server

// Fix: tambahkan toleransi waktu saat verify
jwt.verify(token, process.env.JWT_SECRET, {
  clockTolerance: 30  // Toleransi 30 detik untuk clock skew
});

2. Implementasi Refresh Token yang Benar

// Sistem dua token: access token (short-lived) + refresh token (long-lived)

// Generate token pair
function generateTokens(userId) {
  const accessToken = jwt.sign(
    { userId, type: 'access' },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }  // Short-lived
  );

  const refreshToken = jwt.sign(
    { userId, type: 'refresh' },
    process.env.JWT_REFRESH_SECRET,  // Secret berbeda untuk refresh token!
    { expiresIn: '7d' }  // Long-lived
  );

  return { accessToken, refreshToken };
}

// Endpoint refresh token
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;

  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token tidak ditemukan' });
  }

  try {
    const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    
    // Validasi: pastikan ini memang refresh token, bukan access token
    if (payload.type !== 'refresh') {
      return res.status(401).json({ error: 'Token tidak valid' });
    }

    // Validasi: cek apakah refresh token masih ada di database
    // (untuk bisa invalidate saat logout)
    const stored = await RefreshToken.findOne({ 
      token: refreshToken, 
      userId: payload.userId 
    });
    
    if (!stored) {
      return res.status(401).json({ error: 'Refresh token sudah tidak valid' });
    }

    // Generate access token baru
    const newAccessToken = jwt.sign(
      { userId: payload.userId, type: 'access' },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        error: 'Sesi login telah berakhir, silakan login ulang',
        code: 'REFRESH_TOKEN_EXPIRED'
      });
    }
    res.status(401).json({ error: 'Token tidak valid' });
  }
});

3. Auto-Refresh di Frontend

// Interceptor Axios untuk auto-refresh token
import axios from 'axios';

const api = axios.create({ baseURL: '/api' });

// Request interceptor: tambahkan access token ke setiap request
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('accessToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor: handle 401 dengan auto-refresh
let isRefreshing = false;
let failedQueue = [];

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && 
        error.response?.data?.code === 'TOKEN_EXPIRED' &&
        !originalRequest._retry) {
      
      if (isRefreshing) {
        // Tunggu refresh yang sedang berjalan
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then(token => {
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return api(originalRequest);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const { data } = await axios.post('/api/auth/refresh', { refreshToken });
        
        localStorage.setItem('accessToken', data.accessToken);
        
        // Retry semua request yang gagal
        failedQueue.forEach(({ resolve }) => resolve(data.accessToken));
        failedQueue = [];
        
        originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
        return api(originalRequest);
      } catch (refreshError) {
        // Refresh gagal → logout user
        failedQueue.forEach(({ reject }) => reject(refreshError));
        failedQueue = [];
        localStorage.clear();
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

export default api;

4. Checklist Keamanan JWT

Item Status
JWT_SECRET minimal 32 karakter random
Access token expiry pendek (15 menit - 1 jam)
Refresh token disimpan di database (bisa di-invalidate)
Secret berbeda untuk access dan refresh token
Token type dicek saat verify (access vs refresh)
Error handling membedakan expired vs invalid
Logout menghapus refresh token dari database
Access token disimpan di memory, bukan localStorage (untuk keamanan XSS)

Kesimpulan

JWT error hampir selalu bisa didiagnosis dengan melihat nama error-nya: TokenExpiredError berarti token sudah kadaluarsa (normal), JsonWebTokenError: invalid signature berarti secret tidak cocok, dan jwt malformed berarti format token salah. Implementasikan sistem refresh token dengan benar dan tambahkan auto-refresh di frontend untuk pengalaman pengguna yang mulus tanpa harus login ulang setiap 15 menit.