Audit Logs & Auth Trail — Auth

Audit log adalah catatan permanen dari semua aktivitas autentikasi dan otorisasi. Ini kritis untuk keamanan, debugging, dan kepatuhan regulasi (GDPR, SOC2…

Audit log autentikasi adalah catatan permanen setiap kejadian penting di sistem login: siapa, melakukan apa, kapan, dari mana, dan berhasil atau gagal. Catatan ini cuma boleh ditambah, gak pernah diubah atau dihapus. Bayangin mutasi rekening bank. Tiap transaksi jadi satu baris baru. Kalau ada transfer yang salah, bank gak menghapus barisnya, tapi menambah baris koreksi. Justru karena gak ada yang bisa dihapus, mutasi itu bisa dipercaya sebagai bukti.

🧪 Tebakan cepat: log pagi ini di Portal Sekolah punya 10 baris, 3 di antaranya gagal. Penyaringnya ditulis if (filters.success && log.success !== filters.success) return false, lalu dipanggil dengan { success: false }. Berapa baris yang tampil? Tebak dulu, jawabannya ada di bagian "Query: semua syarat harus cocok".

Event apa yang dicatat

Audit log auth mencatat setiap kejadian yang mengubah siapa bisa masuk, atau membuktikan siapa yang masuk:

action Kapan ditulis Contoh di Portal Sekolah
login setiap percobaan login, berhasil maupun gagal Siti salah password, lalu berhasil
logout user keluar Pak Andi keluar dari laptop sekolah
mfa kode 2FA dicek, berhasil maupun gagal kode TOTP Pak Andi salah sekali
password_change password diganti atau di-reset Siti mengganti password
role_change role atau permission seseorang berubah Bu Dewi mengubah role Rizky jadi alumni
token_revoke sesi atau token dicabut dua sesi lain Siti dicabut

Page view biasa bukan event auth; tempatnya di access log web server. Mengawasi log untuk alert, misalnya banyak login gagal dari satu IP, dibahas di lesson Security Logging & Monitoring di track Security. Lesson ini membangun catatannya.

Lima pertanyaan di tiap baris

Setiap baris menjawab lima pertanyaan yang sama:

function createAuditLog(userId, action, resource, success, metadata) {
  return {
    userId: userId,
    action: action,
    resource: resource,
    success: success,
    metadata: metadata,
    timestamp: new Date().toISOString(),
  };
}

const entry = createAuditLog("user1", "login", "/auth/login", false, {
  ip: "36.68.10.21",
  userAgent: "Chrome 141 di Windows 11",
  reason: "wrong_password",
});
console.log(entry.userId, entry.success, typeof entry.timestamp); // user1 false string

Yang gak boleh masuk log

Log dibaca banyak orang dan banyak alat. Apa pun yang tertulis di sana ikut terbuka ke mereka semua, dan di log append-only gak bisa dihapus lagi. Jadi jangan pernah mencatat:

Kalau perlu menunjuk sesi yang dicabut, catat id barisnya ("sess_1_1"), bukan isi token-nya. Pilih field satu per satu; jangan pernah menyimpan seluruh isi request, karena body login membawa password.

Append-only: cuma boleh ditambah

Audit log berguna sebagai bukti hanya kalau gak bisa diedit. Kalau admin, atau orang yang berhasil memakai akun admin, bisa menghapus baris, jejak kejadiannya bisa ikut hilang. Buat itu mustahil di level database, bukan cuma di kode aplikasi. Di PostgreSQL, user database milik aplikasi cuma diberi izin menambah dan membaca:

GRANT SELECT, INSERT ON audit_logs TO portal_app;

Kalau kode aplikasi mencoba mengubah baris, database menolak:

portal=> UPDATE audit_logs SET success = true WHERE id = 1;
ERROR:  permission denied for table audit_logs

Di Laravel, tambahkan pagar di model juga:

class AuditLog extends Model
{
    public const UPDATED_AT = null; // baris gak pernah di-update

    protected static function booted(): void
    {
        static::updating(fn () => false); // batalkan setiap update
        static::deleting(fn () => false); // batalkan setiap delete
    }
}

Lalu tulis barisnya dari event auth bawaan Laravel, misalnya Illuminate\Auth\Events\Failed untuk login gagal dan Login untuk login berhasil. Untuk lapis tambahan, kirim salinan log ke layanan terpisah yang server aplikasimu gak bisa hapus.

Query: semua syarat harus cocok

Mencari jejak berarti menyaring log dengan beberapa syarat sekaligus. Syarat yang gak dikirim gak boleh mempersempit hasil:

function queryAuditLogs(logs, filters) {
  return logs.filter((log) => {
    if (filters.userId !== undefined && log.userId !== filters.userId) return false;
    if (filters.action !== undefined && log.action !== filters.action) return false;
    if (filters.success !== undefined && log.success !== filters.success) return false;
    return true; // lolos semua syarat yang dikirim
  });
}

Dengan 10 baris log Portal Sekolah pagi ini, persis yang ada di Lab:

Query Hasil
{ success: false } 3: login Siti 07.00.05, kode 2FA Pak Andi 07.05.15, login Siti 07.31.00
{ userId: "user1" } 6: semua kejadian Siti
{ userId: "user1", success: false } 2: dua login Siti yang gagal

Penyaring di tebakan tadi menulis filters.success && .... Saat filters.success bernilai false, ekspresi itu langsung false, syaratnya dilewati, dan semua 10 baris tampil. Itu jawaban tebakannya. Bandingkan dengan !== undefined: nilai false tetap dianggap syarat. Di latihan, dengan empat log contoh, tiga query yang sama menghasilkan 2, 3 dan 1.

⚠️ Jebakan yang sering ketemu

TL;DR: Audit log auth mencatat login (berhasil dan gagal), logout, MFA, ganti password, perubahan role, dan pencabutan token. Tiap baris menjawab siapa (userId), apa (action, resource), kapan (timestamp ISO 8601 UTC dari toISOString()), dari mana (IP, user agent), dan hasilnya (success boolean, plus alasannya). Jangan pernah mencatat password, token, session ID atau kode OTP. Simpan append-only: database cuma mengizinkan INSERT dan SELECT, supaya log bisa dipercaya sebagai bukti. Query menyaring dengan semua syarat yang dikirim sekaligus, dan syarat yang gak dikirim (undefined) gak mempersempit hasil.

Yang akan kamu pelajari