Auth di SPA & Mobile — Auth

Autentikasi di SPA (Single Page Application) dan aplikasi mobile memiliki tantangan berbeda dari aplikasi web tradisional. Pilihan penyimpanan token dan alur…

SPA (Single Page Application, misalnya React atau Vue) dan aplikasi mobile sama-sama harus menyimpan tanda login di sisi client. Pertanyaannya: di mana, supaya aman tapi gak bikin user login ulang terus? Bayangin rumah kos. localStorage itu kunci yang digantung di dinding ruang tamu: siapa pun yang masuk rumah bisa mengambilnya, termasuk tamu yang gak kamu kenal. Memori JavaScript itu kunci di saku: aman, tapi ikut hilang begitu kamu ganti baju. Cookie HttpOnly itu kunci yang dipegang penjaga kos: dia membukakan pintu untukmu setiap kali, tapi kuncinya gak pernah diserahkan ke tanganmu. Di HP, Keychain (iOS) dan Keystore (Android) itu brankas pribadi milik satu aplikasi.

🧪 Tebakan cepat: SPA Portal Sekolah menyimpan access token Siti di variabel JavaScript, bukan di localStorage. Siti menekan reload. Dia harus login ulang, atau tetap masuk? Tebak dulu, jawabannya ada di bagian "Reload: memori kosong, cookie masih ada".

Tiga tempat menyimpan token di browser

Tempat Bisa dibaca script di halaman? Bertahan saat reload? Dikirim otomatis?
localStorage ya, script mana pun ya, sampai dihapus gak
Memori (variabel) cuma kode yang memegang variabelnya gak gak
Cookie HttpOnly gak sama sekali ya, sampai kedaluwarsa ya, ke domainnya

"Script di halaman" termasuk script pihak ketiga dan script yang masuk lewat celah XSS. Token di localStorage bisa dibaca dan dibawa pergi kapan saja, dan berlaku sampai kedaluwarsa. Token di memori gak tersimpan di mana pun, jadi gak ada yang bisa diambil setelah tab ditutup. Cookie HttpOnly gak terlihat oleh document.cookie, tapi karena dikirim otomatis, endpoint yang memakainya butuh perlindungan CSRF (lesson CSRF Protection). Perbandingan lengkapnya ada di lesson Secure Storage di Browser di track Security.

Pola Portal Sekolah: memori + cookie HttpOnly

Persis seperti di lesson Token Refresh Flow: access token 15 menit di memori, refresh token 30 hari di cookie HttpOnly. Tempat simpan access token-nya sebuah closure:

function createTokenStore() {
  let accessToken = null; // di closure: gak di localStorage, hilang saat reload

  return {
    setAccessToken(token) { accessToken = token; }, // { value, expiresAt } dalam milidetik
    getAccessToken() { return accessToken; },
    clearTokens() { accessToken = null; },
    isAuthenticated() { return accessToken !== null && accessToken.expiresAt > Date.now(); },
    needsRefresh(token) { return token.expiresAt - Date.now() < 60000; }, // sisa kurang dari 60 detik
  };
}

Siti login pukul 07.00.00, dan token A1 habis 07.15.00 (expiresAt 1791504900000):

Jam isAuthenticated() needsRefresh(token) Yang dilakukan app
sebelum login false tampilkan form login
07.00.00 true false, sisa 900.000 ms panggil API seperti biasa
07.14.30 true true, sisa 30.000 ms refresh dulu, baru panggil API
07.15.00 false true token habis: refresh

Reload: memori kosong, cookie masih ada

Reload mengosongkan semua variabel, jadi A1 hilang. Tapi cookie refresh_token masih ada di browser. Begitu aplikasi dimuat, ia memanggil /auth/refresh sekali:

const store = createTokenStore();

async function initAuth() {
  const res = await fetch("/auth/refresh", { method: "POST", credentials: "include" }); // cookie ikut otomatis
  if (!res.ok) return false; // belum login, atau refresh token habis
  const { access_token, expires_in } = await res.json();
  store.setAccessToken({ value: access_token, expiresAt: Date.now() + expires_in * 1000 });
  return true;
}

Server menjawab dengan access token baru A2, dan Siti tetap masuk tanpa melihat form login. Itu jawaban tebakannya.

Laravel Sanctum: cookie sesi atau API token

Laravel Sanctum punya dua mode, dan memilihnya gampang:

Mode SPA (cookie sesi), untuk SPA milikmu sendiri di domain yang sama, misalnya portal.sekolah.test dan API di api.sekolah.test. SPA gak memegang token sama sekali. Sanctum memakai session Laravel biasa:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->statefulApi(); // request dari domain stateful memakai cookie sesi
})

// .env
// SANCTUM_STATEFUL_DOMAINS=portal.sekolah.test
// SESSION_DOMAIN=.sekolah.test
axios.defaults.withCredentials = true; // kirim cookie ke api.sekolah.test
axios.defaults.withXSRFToken = true; // salin cookie XSRF-TOKEN ke header X-XSRF-TOKEN

await axios.get("https://api.sekolah.test/sanctum/csrf-cookie"); // 204, set cookie XSRF-TOKEN
await axios.post("https://api.sekolah.test/login", { email: "[email protected]", password: "rahasia123" });
const { data } = await axios.get("https://api.sekolah.test/api/user"); // { id: 1, name: "Siti", ... }

Mode API token, untuk aplikasi mobile dan client pihak ketiga. $user->createToken('HP Siti')->plainTextToken menghasilkan string seperti 1|k3J9.... Database cuma menyimpan hash SHA-256-nya, client mengirimnya di Authorization: Bearer, dan $token->delete() mencabutnya.

BFF: token tetap di server

Kalau SPA harus login lewat OAuth (misalnya akun Google sekolah), polanya BFF (Backend for Frontend): server kecil khusus untuk satu frontend. BFF yang menjalankan alur OAuth dan menyimpan access token serta refresh token di session server. SPA cuma memegang cookie sesi HttpOnly ke BFF, dan BFF yang menempelkan token ke request API. Token gak pernah sampai ke JavaScript. Mode SPA Sanctum sebenarnya bentuk sederhana dari ide ini: API-mu sendiri sekaligus jadi BFF-nya.

Aplikasi mobile: tanpa client_secret, simpan di Keychain atau Keystore

Aplikasi mobile adalah public client: file aplikasinya (APK atau IPA) ada di HP semua orang dan bisa dibongkar, jadi client_secret apa pun yang ditanam di dalamnya bisa diambil. Karena itu login OAuth dari aplikasi mobile memakai PKCE tanpa client_secret: aplikasi membuat code_verifier acak untuk setiap login dan mengirim code_challenge-nya (caranya di lesson OAuth 2.0 Flow). Tiga aturan lain:

⚠️ Jebakan yang sering ketemu

TL;DR: Di SPA, simpan access token di memori (closure seperti createTokenStore) dan refresh token di cookie HttpOnly; jangan di localStorage. isAuthenticated() = accessToken !== null && accessToken.expiresAt > Date.now(), needsRefresh(token) = sisa umur di bawah 60000 ms. Saat reload, memori kosong tapi cookie masih ada, jadi app memanggil /auth/refresh dan user tetap masuk. Laravel Sanctum: mode SPA memakai cookie sesi (/sanctum/csrf-cookie, statefulApi(), XSRF-TOKEN) untuk SPA di domain yang sama, mode API token (createToken, Bearer) untuk mobile. BFF menyimpan token OAuth di server dan memberi SPA cookie sesi saja. Aplikasi mobile adalah public client: tanpa client_secret, pakai PKCE, login di browser sistem, simpan token di Keychain atau Keystore.

Yang akan kamu pelajari