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 OA

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

Auth di SPA (React, Vue, Angular):

Masalah utama: di mana menyimpan token?

Lokasi XSS CSRF Rekomendasi
localStorage Rentan Aman JANGAN untuk token
sessionStorage Rentan Aman Hindari
Memory (JS variable) Aman Aman Terbaik untuk access token
HttpOnly Cookie Aman Perlu mitigasi Terbaik untuk refresh token

Pola yang direkomendasikan untuk SPA:

// auth.js — token disimpan di memory (hilang saat refresh halaman = by design)
let accessToken = null;

export function setAccessToken(token) {
  accessToken = token;
}

export function getAccessToken() {
  return accessToken;
}

// Saat app load: gunakan refresh token (dari HttpOnly cookie) untuk dapat access token baru
export async function initAuth() {
  try {
    const res = await fetch("/api/auth/refresh", {
      method: "POST",
      credentials: "include" // kirim cookie HttpOnly
    });
    const { accessToken: token } = await res.json();
    setAccessToken(token);
  } catch {
    // User belum login atau refresh token expired
    setAccessToken(null);
  }
}

// Axios interceptor untuk auto-refresh
axios.interceptors.response.use(null, async (error) => {
  if (error.response?.status === 401 && !error.config._retry) {
    error.config._retry = true;
    await initAuth();
    error.config.headers.Authorization = `Bearer ${getAccessToken()}`;
    return axios(error.config);
  }
  return Promise.reject(error);
});

Auth di Mobile (OAuth 2.0 + PKCE):

Mobile apps tidak bisa menyimpan client_secret secara aman. Gunakan PKCE (Proof Key for Code Exchange):

// 1. Generate code verifier & challenge
function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString("base64url");
  const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
  return { verifier, challenge };
}

// 2. Buka browser untuk login OAuth
const { verifier, challenge } = generatePKCE();
const authUrl = `https://auth.example.com/oauth/authorize?`
  + `client_id=myapp`
  + `&response_type=code`
  + `&redirect_uri=myapp://callback`
  + `&code_challenge=${challenge}`
  + `&code_challenge_method=S256`
  + `&scope=openid profile`;

// Buka authUrl di system browser (lebih aman dari WebView!)

// 3. Handle callback di deep link
app.on("deep_link", async (url) => {
  const code = new URL(url).searchParams.get("code");

  // 4. Tukar code dengan token — sertakan verifier
  const tokens = await fetch("https://auth.example.com/oauth/token", {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: "myapp://callback",
      client_id: "myapp",
      code_verifier: verifier // server verifikasi ini cocok dengan challenge
    })
  }).then((r) => r.json());

  // Simpan di Secure Enclave / Keystore (bukan localStorage/AsyncStorage!)
  await SecureStore.setItemAsync("refresh_token", tokens.refresh_token);
});

Penyimpanan token di mobile:

Silent refresh untuk mobile:

// Setiap kali app kembali ke foreground
AppState.addEventListener("change", async (state) => {
  if (state === "active") {
    const refreshToken = await SecureStore.getItemAsync("refresh_token");
    if (refreshToken) await refreshTokens(refreshToken);
  }
});