Kodeloka
  1. Kodeloka
  2. Kelas Keamanan Aplikasi
  3. Materi

3 · Identitas · Modul 4 dari 8

Autentikasi, Sandi & Sesi

Menyimpan sandi dengan benar, mengelola sesi, dan menutup celah pada alur lupa sandi.

Menyimpan Sandi

// SALAH — semuanya, tanpa kecuali
sandiHash = md5(sandi);
sandiHash = sha256(sandi);
sandiHash = sha256(sandi + garam);

// BENAR — fungsi yang memang dirancang untuk sandi
import argon2 from "argon2";

const hash = await argon2.hash(sandi, {
    type: argon2.argon2id,
    memoryCost: 19456,      // 19 MB
    timeCost: 2,
    parallelism: 1
});

const cocok = await argon2.verify(hash, sandiDimasukkan);
🚫 SHA-256 salah untuk sandi justru karena ia cepat

Fungsi hash umum dirancang secepat mungkin — itu keunggulannya untuk memeriksa integritas berkas. Untuk sandi, kecepatan itu bekerja untuk penyerang: satu GPU modern menghitung miliaran SHA-256 per detik, sehingga daftar sandi umum habis diuji dalam hitungan menit. Argon2 dan bcrypt sengaja dibuat lambat dan, pada Argon2, boros memori sehingga sulit dipercepat dengan GPU.

Perbandingan Waktu Tetap

// Rentan terhadap analisis waktu
if (tokenDiberikan === tokenAsli) { ... }

// Aman
import { timingSafeEqual } from "crypto";

const a = Buffer.from(tokenDiberikan);
const b = Buffer.from(tokenAsli);
if (a.length === b.length && timingSafeEqual(a, b)) { ... }

Perbandingan string biasa berhenti pada byte pertama yang berbeda. Selisih waktunya sangat kecil, tetapi dengan ribuan percobaan terukur, penyerang dapat menebak token karakter demi karakter. Pustaka verifikasi sandi seperti Argon2 sudah menangani ini secara internal.

Sesi: Cookie atau Token

Cookie sesi di server
  + dapat dicabut seketika
  + token tidak pernah terlihat JavaScript bila HttpOnly
  − perlu penyimpanan sesi bersama antar instans

JWT tanpa keadaan
  + tidak perlu penyimpanan bersama
  − TIDAK dapat dicabut sebelum kedaluwarsa
  − payload terbaca siapa pun, hanya tanda tangannya yang terlindungi
⚠️ JWT yang tidak dapat dicabut adalah masalah nyata

Ketika pengguna keluar, mengganti sandi, atau akunnya dinonaktifkan, JWT yang sudah diterbitkan tetap berlaku sampai kedaluwarsa. Bila masa berlakunya 24 jam, akun yang dibobol tetap dapat diakses selama itu meski sandi sudah diganti. Pola yang lazim: token akses berumur sangat pendek (5–15 menit) ditambah token penyegar yang disimpan di server dan dapat dicabut.

🚫 Serangan alg=none dan kebingungan algoritma

Sebagian pustaka JWT lama menerima header alg: "none" dan melewati verifikasi tanda tangan sepenuhnya — penyerang cukup menyusun payload sendiri. Varian lain: token yang seharusnya RS256 dikirim sebagai HS256, lalu pustaka memakai kunci publik sebagai kunci rahasia HMAC. Selalu tetapkan algoritma yang diterima secara eksplisit saat verifikasi.

jwt.verify(token, kunci, { algorithms: ["RS256"] });

Pesan Galat yang Tidak Membocorkan

// Membocorkan surel mana yang terdaftar
if (!pengguna)      return "Surel tidak ditemukan";
if (!sandiCocok)    return "Sandi salah";

// Aman
return "Surel atau sandi salah";
⚠️ Waktu respons juga membocorkan hal yang sama

Bila surel tidak ditemukan, kode sering langsung kembali tanpa menghitung hash — jauh lebih cepat daripada jalur dengan verifikasi Argon2. Selisih ratusan milidetik itu cukup untuk memetakan surel mana yang terdaftar. Perbaikannya: tetap lakukan verifikasi terhadap hash tiruan meski pengguna tidak ditemukan.

Alur Setel Ulang Sandi

1. Terima permintaan → SELALU balas "bila surel terdaftar, tautan dikirim"
2. Buat token acak kriptografis (32 byte), simpan HASH-nya saja
3. Masa berlaku pendek — 15 sampai 60 menit
4. Sekali pakai; hapus segera setelah dipakai
5. Setelah sandi diganti → cabut seluruh sesi yang ada
6. Kirim pemberitahuan ke surel bahwa sandi berubah
💡 Kenapa token setel ulang disimpan sebagai hash

Bila basis data bocor dan token tersimpan apa adanya, penyerang dapat langsung memakai setiap token yang masih berlaku untuk mengambil alih akun. Menyimpan hash-nya membuat isi basis data tidak cukup untuk menyelesaikan alur — persis alasan sandi tidak disimpan apa adanya.

Membatasi Percobaan

Per akun     5 gagal → tunda bertambah, lalu kunci sementara
Per IP       20 gagal per menit → tolak
Global       lonjakan tidak wajar → tinjau, mungkin credential stuffing

Pembatasan hanya per IP mudah dilewati dengan botnet, sementara pembatasan hanya per akun membuka pintu penguncian akun orang lain dengan sengaja. Keduanya diperlukan bersamaan.