Menyimpan data otentikasi atau claims pengguna pada cookie tanpa proteksi kriptografis membuka celah manipulasi state di sisi klien. Penggunaan composable useCookie di layer frontend Nuxt 3 secara default rentan terhadap manipulasi nilai jika tidak ditandatangani (signed) atau dienkripsi dari server. Untuk kebutuhan autentikasi stateful yang aman, engine Nitro pada Nuxt 3 menyediakan utilitas bawaan H3 yaitu useSession yang menerapkan enkripsi simetris (AES-256-GCM) langsung di server-side.
Risiko useCookie Sisi Klien vs Server-Side useSession
Composable useCookie di frontend Nuxt umumnya hanya melakukan serialisasi JSON dan enkoding Base64. Karakteristik ini menimbulkan risiko keamanan signifikan:
- Client Tampering: Pengguna dapat mengubah payload cookie (seperti
role: "admin"atauuserId) melalui DevTools jika integritas data tidak divalidasi dengan signature kriptografis. - XSS Exploitation: Cookie yang dapat diakses oleh JavaScript rentan dicuri saat terjadi serangan Cross-Site Scripting jika flag
HttpOnlytidak diaktifkan. - Data Exposure: Data sensitif yang disimpan dalam plain text atau Base64 dapat dibaca oleh perantara atau pihak ketiga tanpa memerlukan kunci dekripsi.
Sebaliknya, useSession dari H3 mengenkripsi seluruh payload session menggunakan password berbasis server sebelum mengirimkannya ke header Set-Cookie. Klien hanya menerima string terenkripsi yang tidak dapat dibaca maupun dimodifikasi tanpa merusak integritas ciphertext.
1. Konfigurasi Secret Key di Runtime Config
Enkripsi H3 memerlukan secret key minimal 32 karakter. Definisikan key ini di environment variable dan daftarkan ke dalam nuxt.config.ts pada konfigurasi server privat.
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
sessionPassword: process.env.NUXT_SESSION_PASSWORD || ''
}
})Tambahkan nilai aman pada file .env lokal. Pastikan string memiliki entropi tinggi:
# .env
NUXT_SESSION_PASSWORD=k9#mP2$vL9z@qW8*xR5!tY2^bN7(uJ4-eA1&zC8%2. Abstraksi Session Helper
Untuk memastikan konsistensi atribut cookie di seluruh endpoint Nitro, buat helper terpusat di folder server/utils/session.ts.
// server/utils/session.ts
import type { H3Event } from 'h3'
export interface UserSessionData {
userId: string
role: string
email: string
createdAt: number
}
export function useAppSession(event: H3Event) {
const config = useRuntimeConfig(event)
if (!config.sessionPassword || config.sessionPassword.length < 32) {
throw new Error('NUXT_SESSION_PASSWORD must be at least 32 characters long.')
}
return useSession<UserSessionData>(event, {
password: config.sessionPassword,
name: '__Host-session',
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 8, // 8 jam
path: '/'
}
})
}Catatan Penamaan: Prefix
__Host-adalah spesifikasi standar keamanan browser. Cookie dengan prefix ini wajib memiliki atributSecure, berada pada root path (path=/), dan tidak boleh dikirim dari subdomain yang berbeda.
3. Endpoint Login & Pembuatan Session
Pada server route /api/auth/login.post.ts, verifikasi kredensial pengguna lalu inisialisasi session data menggunakan method update().
// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validasi payload input sederhana
if (!body?.email || !body?.password) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid credentials payload'
})
}
// Dummy verifikasi: ganti dengan database query dan password hashing (Argon2/Bcrypt)
if (body.email !== '[email protected]' || body.password !== 'SuperSecret123!') {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized: Invalid email or password'
})
}
const session = await useAppSession(event)
// Payload yang dienkripsi ke dalam cookie
await session.update({
userId: 'usr_99824',
role: 'administrator',
email: body.email,
createdAt: Date.now()
})
return { success: true, message: 'Authentication successful' }
})4. Server Middleware: Validasi, Refresh, dan Tamper Handling
Middleware server Nitro akan mengeksekusi pemeriksaan pada rute privat (misal rute /api/protected/**). Jika ciphertext cookie dimanipulasi oleh klien, H3 secara internal gagal mendekripsi data dan mengosongkan session data.
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
const path = getRequestPath(event)
// Batasi pengecekan hanya pada rute proteksi
if (!path.startsWith('/api/protected')) {
return
}
try {
const session = await useAppSession(event)
// Cek keberadaan claims esensial
if (!session.data.userId) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized: Session missing or invalid'
})
}
// Implementasi Rolling Expiration: perbarui timestamp untuk memperpanjang sesi aktif
await session.update({
...session.data,
createdAt: Date.now()
})
// Injeksi konteks auth ke Nitro event context
event.context.user = {
id: session.data.userId,
role: session.data.role
}
} catch (error: any) {
// Tangani kemungkinan dekripsi gagal akibat secret key berubah atau cookie tampered
throw createError({
statusCode: 401,
statusMessage: 'Session tampered or expired'
})
}
})5. Invalidation Session via Endpoint Logout
Untuk mengakhiri sesi, panggil method clear() pada instance session. H3 akan menghapus data pada store internal dan mengirim header penghapusan cookie ke browser.
// server/api/auth/logout.post.ts
export default defineEventHandler(async (event) => {
const session = await useAppSession(event)
await session.clear()
return { success: true, message: 'Logged out successfully' }
})Verifikasi Header via cURL
Lakukan request langsung untuk memastikan bahwa flag keamanan cookie aktif dan nilai session tidak berbentuk plain text.
# Eksekusi Login
curl -i -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"SuperSecret123!"}'Output HTTP response header harus mengandung deklarasi berikut:
HTTP/1.1 200 OK
Set-Cookie: __Host-session=fe26.2**...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=28800
Content-Type: application/json; charset=utf-8Dua hal penting pada output di atas:
- Format Sealed Value: Nilai cookie diawali signature terenkripsi (misalnya format iron-webcrypto), membuktikan data tidak tersimpan dalam bentuk raw JSON.
- Keberadaan Flag Proteksi:
HttpOnlymencegah pembacaan viadocument.cookie,Securememastikan transfer hanya via HTTPS, danSameSite=Laxmemitigasi serangan CSRF standar.
Trade-offs dan Limitasi
Pendekatan encrypted stateless-cookie ini mengeliminasi kebutuhan datastore session terpusat seperti Redis pada tahap awal. Namun terdapat batasan teknis:
- Kapasitas Cookie: Standar HTTP membatasi total ukuran cookie maksimum 4096 byte. Hindari menyimpan objek besar dalam session. Simpan hanya pengenal identitas minimal (
userId,role). - Immediate Revocation: Karena session bersifat self-contained di dalam cookie, Anda tidak dapat membatalkan satu sesi tertentu dari backend sebelum masa berlakunya habis tanpa menambahkan mekanisme blacklist token di database/Redis.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!