Pengantar dan Tujuan

Autentikasi berbasis JWT (JSON Web Token) menawarkan cara stateless untuk mengelola sesi pengguna. Dalam aplikasi Nuxt 3 modern, tantangan utama adalah menjaga keseimbangan antara pengalaman pengguna (sesuai dengan SPA) dan keamanan terhadap token yang sudah kadaluarsa atau rentan dicuri. Artikel ini membahas cara membangun alur autentikasi yang lengkap: login, penyimpanan token, proteksi route, penanganan refresh otomatis, hingga logout dan mitigasi risiko XSS/CSRF.

Fokus kita adalah integrasi dengan backend API yang menyediakan endpoint login, refresh token, dan data user. Nuxt 3 menyediakan fondasi yang fleksibel untuk middleware, composables, dan fitur server seperti useFetch atau $fetch dengan interceptor.

1. Struktur Login dan Penerimaan Token

Alur login harus berawal dari endpoint backend yang mengeluarkan access token (berumur pendek) dan refresh token (berumur panjang). Access token digunakan untuk otentikasi request API, sedangkan refresh token digunakan untuk meminta access token baru.

Contoh payload respons backend:

{
  "access_token": "eyJhbGciOi...",
  "refresh_token": "d8e2bc...",
  "expires_in": 300
}

Pasangan token ini bisa disimpan secara berbeda: gunakan httpOnly cookie untuk refresh token agar tidak dapat diakses dari JavaScript, dan memory atau store untuk access token. Nuxt 3 memungkinkan pengiriman cookie dari server (misalnya endpoint /api/auth/login) dengan konfigurasi set-cookie.

Contoh komponen login:

<script setup>
const email = ref('')
const password = ref('')
const router = useRouter()

const handleLogin = async () => {
  const response = await $fetch('/api/auth/login', {
    method: 'POST',
    body: { email: email.value, password: password.value }
  })
  // Access token disimpan di Pinia atau composable
  useAuthStore().setAccessToken(response.access_token)
  await router.push('/dashboard')
}
</script>

Pastikan backend mengatur cookie refresh token dengan atribut httpOnly, Secure, dan SameSite=Strict untuk mengurangi risiko pencurian.

2. Menyimpan Access Token dan Refresh Token

Access token hanya boleh disimpan di memory atau state management (Pinia/composable) agar mudah dihapus saat logout. Contoh store sederhana:

export const useAuthStore = defineStore('auth', {
  state: () => ({
    accessToken: '',
    user: null
  }),
  actions: {
    setAccessToken(token) {
      this.accessToken = token
    },
    clear() {
      this.accessToken = ''
      this.user = null
    }
  }
})

Untuk refresh token, karena disimpan di httpOnly cookie, frontend hanya memanggil endpoint refresh tanpa membaca langsung token. Pastikan setiap request ke API membawa access token di Authorization header.

3. Proteksi Route dengan Middleware

Nuxt 3 mendukung middleware global atau per-route. Kita buat middleware untuk mengecek apakah access token tersedia sebelum mengakses halaman tertentu. Middleware bisa menggunakan store untuk membaca token.

export default defineNuxtRouteMiddleware((to) => {
  const auth = useAuthStore()
  if (!auth.accessToken) {
    return navigateTo('/login')
  }
})

Pasangkan middleware pada halaman yang hanya bisa diakses pengguna terautentikasi dengan properti definePageMeta({ middleware: 'auth' }). Karena middleware dijalankan di server dan client, pastikan token diisi sebelum rendering (misalnya melalui plugin nuxtServerInit atau fetch data awal).

4. Interceptor useFetch/$fetch dan Penyisipan Header

Nuxt 3 menyediakan useFetch dan $fetch yang bisa dipasangi interceptor global untuk menyisipkan access token. Gunakan defineNuxtPlugin untuk mengatur struktur interceptors.

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.$fetch = async (input, init) => {
    const auth = useAuthStore()
    const headers = new Headers(init?.headers)
    if (auth.accessToken) {
      headers.set('Authorization', `Bearer ${auth.accessToken}`)
    }
    const response = await $fetch.raw(input, { ...init, headers })
    return response.json()
  }
})

Alternatifnya, gunakan useFetch dengan opsi headers setiap kali memanggil API jika tidak ingin override global.

5. Strategi Refresh Token Otomatis

Refresh token digunakan ketika mendapatkan respons 401/403 akibat kadaluarsa. Strategi praktis:

  1. Gunakan interceptor untuk mendeteksi status 401.
  2. Panggil endpoint refresh token (mengandalkan cookie httpOnly).
  3. Update access token di store dan ulangi request.
  4. Jika refresh gagal (misalnya refresh token kadaluarsa), arahkan pengguna ke login.

Contoh fungsi refresh:

const refreshAccessToken = async () => {
  try {
    const response = await $fetch('/api/auth/refresh', { method: 'POST' })
    useAuthStore().setAccessToken(response.access_token)
    return true
  } catch (error) {
    useAuthStore().clear()
    return false
  }
}

Integrasikan dengan interceptor server-side atau client-side menggunakan fetch wrapper. Pastikan refresh token endpoint tidak memerlukan Authorization header karena hanya mengandalkan cookie.

6. Logout dan Penanganan Error 401

Logout melibatkan pembersihan access token dan permintaan ke backend untuk menghapus refresh token (misalnya menghapus cookie). Setelah logout, arahkan ke halaman login.

const handleLogout = async () => {
  await $fetch('/api/auth/logout', { method: 'POST' })
  const auth = useAuthStore()
  auth.clear()
  await navigateTo('/login')
}

Untuk error 401, interceptor harus menangani dua kasus:

  • Token kadaluarsa: otomatis refresh dan ulangi request.
  • User tidak terautentikasi: langsung redirect ke login jika refresh gagal.

Gunakan logika retry yang terbatas untuk menghindari loop tak terbatas. Simpan flag ketika sedang refresh agar request paralel tidak memicu refresh ganda.

7. Catatan Keamanan

httpOnly Cookie

Refresh token harus disimpan di cookie dengan atribut httpOnly, Secure, SameSite=Strict. Ini mencegah JavaScript mencuri token dan mengurangi risiko CSRF jika backend menuntut header custom.

Mitigasi CSRF

Karena refresh token berada di cookie, synchronizer token pattern (CSRF token) atau double submit cookie harus diterapkan di endpoint sensitif. Alternatif: periksa header Origin/Referer dan gunakan samesite=strict.

Mitigasi XSS

Jangan menyimpan access token di localStorage/sessionStorage. Tampilkan data user secara aman (hindari innerHTML). Pastikan dependency pihak ketiga tidak memperkenalkan XSS.

Trade-off

Strategi refresh token otomatis menambah kompleksitas: perlu manajemen flag, pemrosesan error, dan pengaturan middleware. Namun, ini meningkatkan UX karena pengguna tidak harus login ulang sering. Pastikan logging sisi backend memantau refresh token yang gagal.

Penutup

Membangun autentikasi JWT di Nuxt 3 membutuhkan pendekatan holistik: login terstruktur, penyimpanan token terpisah, middleware proteksi route, interceptors untuk menyisipkan token, refresh otomatis saat token kadaluarsa, serta logout/error handling. Tidak kalah pentingnya adalah menjaga keamanan (httpOnly cookie, CSRF, XSS). Dengan mengikuti praktik ini, aplikasi web modern dapat menjaga keseimbangan antara pengalaman pengguna dan keamanan.