Token Refresh Flow — Auth

Masalah: JWT access token harus punya expiry pendek (15-30 menit) untuk keamanan. Tapi kita tidak mau user harus login ulang setiap 15 menit. Solusi: Refresh To

Masalah: JWT access token harus punya expiry pendek (15-30 menit) untuk keamanan. Tapi kita tidak mau user harus login ulang setiap 15 menit. Solusi: Refresh Token.

Dua jenis token:

Token Lifetime Penyimpanan Fungsi
Access Token 15-30 menit Memory/cookie Akses API
Refresh Token 7-30 hari HttpOnly cookie Mendapatkan access token baru

Alur Refresh Token:

1. Login berhasil → server kirim:
   - Access token (pendek, 15m)
   - Refresh token (panjang, 7d, di HttpOnly cookie)

2. User mengakses API dengan access token
   → Server verifikasi → OK

3. Access token kadaluarsa (setelah 15 menit)
   → Server return 401

4. Client kirim refresh token ke /api/refresh
   → Server verifikasi refresh token
   → Server buat access token BARU + refresh token BARU
   → Client simpan token baru dan retry request awal

5. Refresh token kadaluarsa (setelah 7 hari)
   → User harus login ulang

Implementasi server:

// Login — kirim kedua token
app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  if (!user) return res.status(401).json({ error: "Login gagal" });

  const accessToken = jwt.sign({ userId: user.id }, ACCESS_SECRET, { expiresIn: "15m" });
  const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET, { expiresIn: "7d" });

  // Simpan refresh token di database (untuk bisa revoke)
  await db.saveRefreshToken(user.id, refreshToken);

  // Refresh token di HttpOnly cookie (aman dari XSS)
  res.cookie("refreshToken", refreshToken, {
    httpOnly: true, secure: true, sameSite: "strict",
    maxAge: 7 * 24 * 60 * 60 * 1000
  });

  res.json({ accessToken });
});

// Refresh — tukar refresh token lama dengan token baru
app.post("/refresh", async (req, res) => {
  const oldRefreshToken = req.cookies.refreshToken;
  if (!oldRefreshToken) return res.status(401).json({ error: "No refresh token" });

  try {
    const decoded = jwt.verify(oldRefreshToken, REFRESH_SECRET);

    // Cek apakah token masih valid di database
    const stored = await db.findRefreshToken(oldRefreshToken);
    if (!stored) return res.status(401).json({ error: "Token revoked" });

    // Rotate: hapus token lama, buat baru
    await db.deleteRefreshToken(oldRefreshToken);

    const newAccessToken = jwt.sign({ userId: decoded.userId }, ACCESS_SECRET, { expiresIn: "15m" });
    const newRefreshToken = jwt.sign({ userId: decoded.userId }, REFRESH_SECRET, { expiresIn: "7d" });

    await db.saveRefreshToken(decoded.userId, newRefreshToken);

    res.cookie("refreshToken", newRefreshToken, {
      httpOnly: true, secure: true, sameSite: "strict",
      maxAge: 7 * 24 * 60 * 60 * 1000
    });

    res.json({ accessToken: newAccessToken });
  } catch {
    return res.status(401).json({ error: "Invalid refresh token" });
  }
});

Token Rotation: Setiap kali refresh token dipakai, buat refresh token BARU dan hapus yang lama. Jika token lama dipakai lagi (oleh attacker), server tahu bahwa token sudah dicuri dan bisa revoke semua token user.

Client-side auto-refresh:

async function fetchWithAuth(url, options = {}) {
  let res = await fetch(url, {
    ...options,
    headers: { ...options.headers, Authorization: "Bearer " + accessToken }
  });

  if (res.status === 401) {
    // Coba refresh
    const refreshRes = await fetch("/api/refresh", { method: "POST", credentials: "include" });
    if (refreshRes.ok) {
      const data = await refreshRes.json();
      accessToken = data.accessToken; // update token
      // Retry request awal
      res = await fetch(url, {
        ...options,
        headers: { ...options.headers, Authorization: "Bearer " + accessToken }
      });
    } else {
      window.location.href = "/login"; // refresh gagal → login ulang
    }
  }

  return res;
}

Yang akan kamu pelajari