Anatomi Masalah: Sesi Habis dan Error HTTP 419
Aplikasi monolithic modern berbasis Inertia.js bergantung pada cookie session HTTP backend (seperti Laravel) dan token CSRF untuk validasi mutasi data. Saat sesi kedaluwarsa di latar belakang, request POST, PUT, atau DELETE berikutnya akan memicu respons HTTP 419 Unknown Status (CSRF token mismatch).
Secara default, Inertia menganggap respons non-Inertia atau error status tertentu sebagai modal konflik sistem atau melakukan full page reload via window.location.reload(). Tindakan reload ini menghancurkan seluruh state JavaScript di memory, termasuk data formulir multi-step, draft panjang, atau file upload yang belum sempat tersimpan di database.
Arsitektur Solusi: In-Place Re-Authentication
Untuk mencegah data loss, alur interupsi harus diterapkan pada sisi client dan server:
- Intercept: Cegah reload bawaan Inertia saat status 419 terdeteksi via event hook global.
- State Holding: Tahan parameter request yang gagal (URL, method, payload data).
- In-Place Auth: Munculkan modal re-autentikasi tanpa membongkar DOM aktif.
- Session Regeneration: Autentikasi ulang backend, regenerasi ID sesi dan token CSRF baru.
- Replay: Eksekusi ulang request yang tertunda menggunakan token baru, lalu lanjutkan navigasi normal.
Frontend: Interceptor dengan router.on('invalid')
Inertia menyediakan event lifecycle invalid yang terpicu saat menerima respons HTTP non-Inertia atau status error yang tidak tertangani. Gunakan hook ini untuk membatalkan perilaku default dan memicu modal re-autentikasi.
import { router } from '@inertiajs/vue3'; // atau @inertiajs/react
import { reactive } from 'vue';
export const authModalState = reactive({
isOpen: false,
previousUserId: null,
pendingRequest: null,
});
export function setupSessionInterceptor(currentUserId) {
authModalState.previousUserId = currentUserId;
router.on('invalid', (event) => {
const response = event.detail.response;
if (response.status === 419) {
// 1. Batalkan reload modal default Inertia
event.preventDefault();
// 2. Tangkap opsi visit yang gagal
const failedVisit = event.detail.interaction;
authModalState.pendingRequest = {
url: failedVisit.url.href,
method: failedVisit.method,
data: failedVisit.data,
preserveState: true,
preserveScroll: true,
};
// 3. Tampilkan modal login ulang
authModalState.isOpen = true;
}
});
}Backend: Endpoint Re-Autentikasi & Anti-Fixation
Endpoint backend wajib memverifikasi kredensial, memastikan integritas identitas pengguna, dan mencegah kerentanan session fixation. Endpoint tidak boleh menerima sembarang user: identitas user yang re-login harus identik dengan sesi yang kedaluwarsa.
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class ReAuthenticationController extends Controller
{
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
'previous_user_id' => ['required', 'integer'],
]);
// Verifikasi kredensial manual tanpa langsung mengubah sesi
if (! Auth::validate(['email' => $validated['email'], 'password' => $validated['password']])) {
throw ValidationException::withMessages([
'password' => __('Kredensial yang diberikan tidak cocok.'),
]);
}
$user = Auth::getProvider()->retrieveByCredentials(['email' => $validated['email']]);
// Cegah pergantian identitas: pengguna baru harus sama dengan pemilik sesi sebelumnya
if ((int) $user->getAuthIdentifier() !== (int) $validated['previous_user_id']) {
return response()->json([
'message' => 'Identitas akun tidak sesuai dengan pemilik form.',
], 403);
}
// Login resmi, cegah session fixation & terbitkan token baru
Auth::login($user);
$request->session()->regenerate();
$request->session()->regenerateToken();
return response()->json([
'csrf_token' => csrf_token(),
]);
}
}Replay Request dan Sanitasi Kredensial
Setelah backend merespons sukses, perbarui token CSRF pada client, eksekusi ulang request yang tersimpan, lalu bersihkan password dari memori JavaScript secara instan.
import axios from 'axios';
import { router } from '@inertiajs/vue3';
import { authModalState } from './sessionInterceptor';
async function handleReAuthSubmit(email, password) {
try {
const response = await axios.post('/re-authenticate', {
email,
password,
previous_user_id: authModalState.previousUserId,
});
// Perbarui meta tag CSRF dan header Axios default
const newToken = response.data.csrf_token;
document.querySelector('meta[name="csrf-token"]')?.setAttribute('content', newToken);
axios.defaults.headers.common['X-CSRF-TOKEN'] = newToken;
// Bersihkan data kredensial dari scope form
password = null;
// Tutup modal
authModalState.isOpen = false;
// Replay request yang tertunda
if (authModalState.pendingRequest) {
const { url, method, data, ...options } = authModalState.pendingRequest;
authModalState.pendingRequest = null;
router.visit(url, {
method,
data,
...options,
});
}
} catch (error) {
// Tangani validasi gagal di sini
console.error('Re-autentikasi gagal:', error);
}
}Security Checklist
- Session Fixation: Pemanggilan
$request->session()->regenerate()wajib dijalankan sesaat setelah otentikasi tervalidasi guna membatalkan ID sesi lama yang berpotensi terkontaminasi. - User Hijacking: Pengecekan
previous_user_idmutlak diperlukan agar pihak lain tidak memanfaatkan modal re-auth terbuka di komputer korban untuk login menggunakan akun berbeda dan menerima payload sensitif yang ada di memori form. - Memory Leak Credentials: Hindari menyimpan kata sandi re-autentikasi pada global state atau reactive storage yang persisten. Gunakan variabel lokal sesingkat mungkin dan set ke
nullsetelah request selesai. - Multipart Form Data: Jika request yang gagal memuat file (upload),
failedVisit.databerupa instanceFormDatatetap utuh di memory dan dapat langsung diteruskan kembali viarouter.visit()tanpa meminta input file ulang dari pengguna.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!