Authentication #

Authentication adalah proses memverifikasi identitas — memastikan bahwa entitas yang mengklaim sebagai “Ali” memang benar-benar Ali, bukan orang lain yang berpura-pura. Ini adalah gerbang pertama dari semua sistem keamanan: jika authentication lemah, semua lapisan perlindungan lain bisa dibypass.

Yang membuat authentication menarik sekaligus berbahaya: ia bersinggungan langsung dengan pengalaman pengguna. Sistem yang terlalu ketat membuat user frustrasi — terlalu banyak langkah, terlalu sering diminta login ulang, password requirement yang tidak masuk akal. Sistem yang terlalu longgar membuka celah yang dieksploitasi attacker. Menemukan keseimbangan yang tepat antara keamanan dan usability adalah salah satu tantangan terbesar dalam desain sistem.

Artikel ini membahas authentication dari sudut pandang implementasi: bagaimana membangunnya dengan benar dari awal, jebakan-jebakan yang sering menyebabkan insiden, dan praktik terbaik yang sudah teruji.

Authentication vs Authorization: Perbedaan yang Sering Dibalik #

Sebelum membahas implementasi, penting untuk memahami perbedaan dua konsep yang sering digunakan secara bergantian padahal berbeda secara fundamental.

Authentication (AuthN) — "Siapa kamu?"
  Proses memverifikasi identitas pengguna.
  Input: credential (password, token, biometrik)
  Output: identitas yang terverifikasi (atau gagal)
  Contoh: login dengan username dan password

Authorization (AuthZ) — "Apa yang boleh kamu lakukan?"
  Proses menentukan hak akses berdasarkan identitas.
  Input: identitas yang sudah terverifikasi
  Output: daftar operasi yang diizinkan
  Contoh: user biasa tidak bisa akses halaman admin

Urutan yang benar:
  Request masuk → Authentication → Authorization → Process request

  Jika authentication gagal → tolak, tidak perlu cek authorization
  Jika authorization gagal → tolak dengan 403 Forbidden
  Jika keduanya lulus → proses request

Kesalahan yang sering terjadi: melakukan authorization check tanpa authentication check yang benar, atau mengasumsikan bahwa sudah login berarti sudah punya semua akses.


Password: Fondasi yang Sering Salah Diimplementasikan #

Password masih menjadi mekanisme autentikasi yang paling umum — dan masih sering diimplementasikan dengan cara yang salah.

Hashing Password yang Benar #

// ANTI-PATTERN 1: menyimpan password plaintext
user.Password = "userpassword123" // JANGAN PERNAH

// ANTI-PATTERN 2: enkripsi (bukan hashing)
user.Password = encrypt(password, secretKey)
// Enkripsi bisa di-decrypt. Jika secretKey bocor, semua password bocor.

// ANTI-PATTERN 3: hash tanpa salt — rentan rainbow table attack
user.Password = sha256.Sum256([]byte(password))
// Dua user dengan password sama menghasilkan hash sama
// → attacker bisa precompute rainbow table dan langsung match

// ANTI-PATTERN 4: hash cepat — rentan brute force
// SHA256 bisa menghitung miliaran hash per detik di GPU modern
// Database 1 juta password bisa di-crack dalam jam jika hash bocor

// BENAR: gunakan algoritma yang dirancang untuk password hashing.
// golang.org/x/crypto/argon2 — Argon2id (rekomendasi OWASP 2023).
func hashPassword(password string, salt []byte) string {
    // time = 3 iterasi — lebih tinggi = lebih lambat = lebih aman
    // memory = 65536 KiB (64MB) — mencegah GPU/ASIC attack
    // threads = 2, keyLen = 32 byte output; salt random per password
    hash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 2, 32)
    return fmt.Sprintf("$argon2id$v=19$m=65536,t=3,p=2$%s$%s",
        base64.RawStdEncoding.EncodeToString(salt),
        base64.RawStdEncoding.EncodeToString(hash))
}

func verifyPassword(stored, provided string, salt []byte) bool {
    // Hitung ulang hash dan bandingkan dalam waktu konstan
    expected := argon2.IDKey([]byte(provided), salt, 3, 64*1024, 2, 32)
    return subtle.ConstantTimeCompare([]byte(stored), expected) == 1
}

func needsRehash(stored string) bool {
    // Cek apakah stored hash masih memakai parameter saat ini
    return !strings.HasPrefix(stored, "$argon2id$v=19$m=65536,t=3,p=2$")
}
Mengapa Argon2id:

  Memory-hard: membutuhkan RAM dalam jumlah besar
  → GPU modern punya ribuan core tapi RAM yang terbatas per core
  → Memory requirement membuat GPU attack jauh lebih mahal

  Time-cost: setiap hash butuh ~100-300ms (dikonfigurasi)
  → Server masih bisa handle ratusan login per detik
  → Attacker yang mencoba brute force: hanya bisa mencoba
    ratusan/ribuan kombinasi per detik (vs miliaran dengan MD5)

  Perbandingan kecepatan hashing (GPU RTX 4090):
  MD5:    100+ miliar hash/detik → password 8 char habis dalam detik
  SHA256: 10+ miliar hash/detik
  bcrypt: ~10 juta hash/detik
  Argon2: ~100 ribu hash/detik → miliaran kali lebih lambat dari MD5

Password Policy yang Masuk Akal #

Password policy yang terlalu ketat tidak membuat sistem lebih aman — user akan menulis password di sticky note atau menggunakan pola yang mudah ditebak.

// Password policy berdasarkan NIST SP 800-63B (lebih baru dan lebih baik)
const (
    minLength    = 12  // panjang minimum, bukan maksimum
    maxLength    = 128 // ada maksimum untuk mencegah DoS via hash computation
    pwnedAPIURL  = "https://api.pwnedpasswords.com/range/"
)

func validatePassword(password string) (bool, []string) {
    var errors []string

    // 1. Panjang minimum
    if len(password) < minLength {
        errors = append(errors, fmt.Sprintf("Password minimal %d karakter", minLength))
    }

    // 2. Panjang maksimum
    if len(password) > maxLength {
        errors = append(errors, fmt.Sprintf("Password maksimal %d karakter", maxLength))
    }

    // 3. Cek terhadap database password yang diketahui bocor (HaveIBeenPwned API)
    if isPasswordPwned(password) {
        errors = append(errors,
            "Password ini ada dalam daftar password yang diketahui bocor. "+
                "Silakan pilih password yang berbeda.")
    }

    // 4. Cek konteks (jangan gunakan nama aplikasi, username, dll)
    // Ini lebih berguna dari requirement "harus ada angka dan huruf besar"

    return len(errors) == 0, errors
}

func isPasswordPwned(password string) bool {
    // Cek HaveIBeenPwned menggunakan k-Anonymity — password tidak pernah dikirim
    sum := sha1.Sum([]byte(password))
    hexStr := strings.ToUpper(hex.EncodeToString(sum[:]))
    prefix, suffix := hexStr[:5], hexStr[5:]

    resp, err := http.Get(pwnedAPIURL + prefix)
    if err != nil {
        return false // Jika API gagal, jangan blokir user
    }
    defer resp.Body.Close()
    if resp.StatusCode != 200 {
        return false
    }

    body, _ := io.ReadAll(resp.Body)
    for _, line := range strings.Split(string(body), "\n") {
        parts := strings.Split(line, ":")
        if len(parts) == 2 && parts[0] == suffix {
            count, _ := strconv.Atoi(parts[1])
            return count > 0
        }
    }
    return false
}
Apa yang TIDAK perlu (berdasarkan NIST):

  ✗ Requirement "harus ada huruf besar, angka, dan simbol"
    → Membuat user menggunakan pola yang mudah ditebak: Password1!

  ✗ Rotasi password wajib secara periodik (misal setiap 90 hari)
    → User menggunakan password lemah yang mudah diingat jika sering diubah
    → Ganti password HANYA jika ada indikasi kompromi

  ✗ Hints dan security questions
    → Security questions sering bisa ditebak dari info publik

  Yang perlu:
  ✓ Panjang yang cukup (12+ karakter)
  ✓ Cek terhadap database password yang bocor
  ✓ Tidak boleh sama dengan username atau email
  ✓ Tampilkan strength indicator untuk membantu user memilih password kuat

Multi-Factor Authentication (MFA) #

MFA menambahkan lapisan verifikasi kedua setelah password. Bahkan jika password dikompromikan, attacker masih perlu faktor kedua yang tidak mereka miliki.

flowchart TD
    A["User masukkan\nusername + password"] --> B{Password benar?}
    B --> |Tidak| C["Tolak + log\nfailed attempt"]
    B --> |Ya| D{MFA aktif?}
    D --> |Tidak| E["Login berhasil\nTapi kurang aman"]
    D --> |Ya| F{Jenis MFA?}
    F --> G["TOTP\nGoogle Auth/Authy"]
    F --> H[SMS OTP]
    F --> I["Push notification\nke app"]
    F --> J["Hardware key\nYubiKey/FIDO2"]
    G --> K{Kode valid?}
    H --> K
    I --> K
    J --> K
    K --> |Tidak| L[Tolak + log]
    K --> |Ya| M[Login berhasil ✓]

TOTP (Time-based One-Time Password) #

// Implementasi TOTP dengan github.com/pquerna/otp
func setupTOTPForUser(userID int, username string) map[string]any {
    // Generate secret key yang aman
    key, err := totp.Generate(totp.GenerateOpts{
        Issuer:      "MyApp",
        AccountName: username,
    })
    if err != nil {
        return nil
    }

    // Simpan secret ke database (terenkripsi!)
    storeMFASecret(userID, encryptSecret(key.Secret()))

    // Generate provisioning URI untuk QR code
    provisioningURI := key.URL()
    // Generate QR code (github.com/skip2/go-qrcode) dari URI dan
    // tampilkan ke user untuk di-scan dengan authenticator app

    // Juga tampilkan secret key sebagai teks (untuk manual entry)
    return map[string]any{
        "secret":           key.Secret(), // tampilkan SEKALI, jangan simpan di plain
        "provisioning_uri": provisioningURI,
        // backup codes untuk recovery
        "backup_codes": generateBackupCodes(userID),
    }
}

func verifyTOTP(userID int, providedCode string) bool {
    secret := decryptSecret(getMFASecret(userID))

    // validWindow=1 mengizinkan 1 interval maju/mundur (30 detik)
    // untuk mengakomodasi sedikit perbedaan waktu antara server dan device
    return totp.Validate(providedCode, secret)
}

func generateBackupCodes(userID int) []string {
    // Generate 10 backup codes; simpan hanya hash-nya
    codes := make([]string, 0, 10)
    for i := 0; i < 10; i++ {
        codes = append(codes, randomHex(4))
    }
    // Simpan hash dari backup codes (bukan plaintext)
    // User hanya bisa melihatnya sekali saat setup
    storeBackupCodesHashed(userID, codes)
    return codes // tampilkan ke user sekali saja
}
Pilihan MFA dari yang paling ke kurang aman:

  1. FIDO2/WebAuthn (hardware key atau passkey)
     → Tidak bisa di-phish karena terikat ke domain
     → Paling aman, tapi butuh hardware atau device yang support

  2. TOTP (Google Authenticator, Authy)
     → Time-based, kode berbeda setiap 30 detik
     → Rentan phishing jika user ditipu memasukkan kode ke situs palsu
     → Tapi masih jauh lebih baik dari SMS

  3. Push notification (Duo, Okta Verify)
     → User approve di app → lebih user-friendly dari TOTP
     → Rentan MFA fatigue attack (spam notifikasi sampai user approve)

  4. SMS OTP
     → Rentan SIM swapping attack
     → Lebih baik dari tidak ada MFA sama sekali
     → Hindari untuk aplikasi dengan risiko tinggi (banking, crypto)

Session vs Token-Based Authentication #

Ada dua pendekatan utama untuk mempertahankan state autentikasi setelah login berhasil.

Session-based (stateful):

flowchart LR
    Server["Server (Login)"] -->|"store"| DB["Database/Redis<br>session_id<br>user_id: 1<br>expires: .."]
    Client["Client<br>Cookie: session=X"] -->|"read/verify"| DB
    Server -->|"set-cookie"| Client

Keuntungan: → Session bisa di-revoke kapan saja (hapus dari database) → Server selalu punya info terkini tentang session → Ideal untuk web app tradisional

Kekurangan: → Butuh shared storage jika ada multiple server instance → Tambahan latency untuk lookup ke database/Redis

Token-based (stateless) — JWT:

flowchart LR
    Server["Server (Login)"] -->|"sign & send"| Client["Client<br>Header.Payload.Signature"]
    Client -->|"send token"| Server

Keuntungan: → Stateless — tidak perlu database lookup untuk verifikasi → Horizontal scaling mudah (tidak perlu shared session store) → Bisa diverifikasi oleh multiple services tanpa komunikasi ke auth server

Kekurangan: → Tidak bisa di-revoke sebelum expire (tanpa additional mechanism) → Payload terbaca oleh siapapun (hanya signature yang tidak bisa dipalsukan)

JWT — Pitfall yang Sering Diabaikan #

// ANTI-PATTERN 1: algoritma "none" — mematikan signature verification
token := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{"user_id": 1})
// Siapapun bisa membuat token tanpa signature

// ANTI-PATTERN 2: kunci HS256 yang lemah
token, _ = jwt.NewWithClaims(jwt.SigningMethodHS256,
    jwt.MapClaims{"user_id": 1}).SignedString([]byte("secret"))
// "secret" bisa di-crack dengan wordlist attack dalam detik

// ANTI-PATTERN 3: expiry yang terlalu panjang
claims := jwt.MapClaims{
    "user_id": 1,
    "exp":     time.Now().Add(365 * 24 * time.Hour).Unix(), // 1 tahun!
}
// Token yang dikompromikan valid selama 1 tahun tanpa cara revoke

// BENAR: konfigurasi JWT yang aman (github.com/golang-jwt/jwt/v5)
// HS256: secret key minimal 256-bit (32 byte)
jwtSecret := os.Getenv("JWT_SECRET") // kunci random 32+ byte
if len(jwtSecret) < 32 {
    panic("JWT secret terlalu pendek")
}

// Atau RS256 untuk arsitektur multi-service
// RS256 memungkinkan service lain verifikasi token tanpa tahu private key

func createAccessToken(userID int, additionalClaims map[string]any) (string, error) {
    now := time.Now()
    claims := jwt.MapClaims{
        "sub":  strconv.Itoa(userID),             // subjek
        "iat":  now.Unix(),                       // diterbitkan pada
        "exp":  now.Add(15 * time.Minute).Unix(), // expiry pendek!
        "jti":  randomToken(16),                  // ID token unik
        "type": "access",                         // bedakan access vs refresh token
    }
    for k, v := range additionalClaims {
        claims[k] = v
    }
    return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(jwtSecret))
}

func verifyAccessToken(tokenStr string) (jwt.MapClaims, error) {
    token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
        // eksplisit — jangan terima algoritma "none" atau campuran
        if t.Method != jwt.SigningMethodHS256 {
            return nil, errors.New("metode signing tidak terduga")
        }
        return []byte(jwtSecret), nil
    })
    if err != nil {
        return nil, err
    }
    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok || !token.Valid {
        return nil, errors.New("token tidak valid")
    }
    // wajib ada exp, iat, sub, jti, type
    if claims["type"] != "access" {
        return nil, errors.New("tipe token salah")
    }

    // Opsional: cek token blacklist jika butuh revocation
    if isTokenRevoked(claims["jti"].(string)) {
        return nil, errors.New("token sudah di-revoke")
    }
    return claims, nil
}

Brute Force Protection #

// Rate limiter berbasis Redis (github.com/redis/go-redis/v9)
type RateLimiter struct {
    client         *redis.Client
    maxAttempts    int
    windowSeconds  time.Duration
    lockoutSeconds time.Duration
}

func (r *RateLimiter) checkRateLimit(ctx context.Context, identifier string) (bool, int) {
    key := "login_attempts:" + identifier
    lockoutKey := "login_lockout:" + identifier

    // Cek apakah sedang dalam lockout
    lockoutRemaining, _ := r.client.TTL(ctx, lockoutKey).Result()
    if lockoutRemaining > 0 {
        return false, int(lockoutRemaining.Seconds())
    }

    // Cek jumlah percobaan dalam window
    attempts, _ := r.client.Get(ctx, key).Int()
    if attempts >= r.maxAttempts {
        // Set lockout
        r.client.Set(ctx, lockoutKey, 1, r.lockoutSeconds)
        r.client.Del(ctx, key)
        return false, int(r.lockoutSeconds.Seconds())
    }
    return true, 0
}

func (r *RateLimiter) recordAttempt(ctx context.Context, identifier string) {
    // Record satu attempt
    key := "login_attempts:" + identifier
    pipe := r.client.Pipeline()
    pipe.Incr(ctx, key)
    pipe.Expire(ctx, key, r.windowSeconds)
    pipe.Exec(ctx)
}

func (r *RateLimiter) resetAttempts(ctx context.Context, identifier string) {
    // Reset setelah login berhasil
    r.client.Del(ctx, "login_attempts:"+identifier)
    r.client.Del(ctx, "login_lockout:"+identifier)
}

// Limiter per IP: 10 percobaan per 5 menit
ipLimiter := &RateLimiter{client: rdb, maxAttempts: 10,
    windowSeconds: 5 * time.Minute, lockoutSeconds: 5 * time.Minute}

// Limiter per akun: 5 percobaan per 15 menit (lebih ketat)
accountLimiter := &RateLimiter{client: rdb, maxAttempts: 5,
    windowSeconds: 15 * time.Minute, lockoutSeconds: 15 * time.Minute}

// Handler login (net/http)
func loginHandler(w http.ResponseWriter, r *http.Request) {
    email := r.FormValue("email")
    password := r.FormValue("password")
    ip := remoteIP(r)

    // Cek rate limit per IP
    allowed, retryAfter := ipLimiter.checkRateLimit(r.Context(), ip)
    if !allowed {
        w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
        http.Error(w, `{"error": "Terlalu banyak percobaan. Coba lagi dalam beberapa menit."}`, http.StatusTooManyRequests)
        return
    }

    // Cek rate limit per akun
    allowed, retryAfter = accountLimiter.checkRateLimit(r.Context(), email)
    if !allowed {
        w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
        http.Error(w, `{"error": "Akun sementara terkunci karena terlalu banyak percobaan."}`, http.StatusTooManyRequests)
        return
    }

    // Record attempt SEBELUM verifikasi (mencegah timing attack)
    ipLimiter.recordAttempt(r.Context(), ip)
    accountLimiter.recordAttempt(r.Context(), email)

    user := findUserByEmail(email)

    // PENTING: selalu verifikasi password (timing-safe)
    // Jika user tidak ada, tetap jalankan verify untuk waktu yang konsisten
    if user == nil {
        verifyPassword(dummyHash, password) // timing dummy
        http.Error(w, `{"error": "Email atau password tidak valid"}`, http.StatusUnauthorized)
        return
    }

    if !verifyPassword(user.PasswordHash, password) {
        http.Error(w, `{"error": "Email atau password tidak valid"}`, http.StatusUnauthorized)
        return
    }

    // Login berhasil — reset rate limit
    ipLimiter.resetAttempts(r.Context(), ip)
    accountLimiter.resetAttempts(r.Context(), email)

    // Cek apakah hash perlu di-upgrade
    if needsRehash(user.PasswordHash) {
        user.PasswordHash = hashPassword(password, newSalt())
    }

    sessionToken := createSession(user.ID, r)
    setAuthCookie(w, sessionToken)
    w.WriteHeader(http.StatusOK)
}

Secure Password Reset #

Flow reset password adalah salah satu yang paling sering diimplementasikan dengan lemah — tapi berpotensi menjadi celah account takeover yang serius.

// Flow reset password
var resetTokenExpiry = 15 * time.Minute

func requestPasswordReset(email string) {
    user := findUserByEmail(email)

    // PENTING: response SELALU sama terlepas apakah email ada atau tidak
    // Mencegah user enumeration (mengetahui apakah email terdaftar)

    if user != nil {
        // Generate token yang kuat
        rawToken := randomToken(32)

        // Simpan HASH dari token (bukan token itu sendiri)
        // Jika database bocor, token tidak langsung bisa dipakai
        tokenHash := sha256Hex(rawToken)

        // Invalidasi semua token reset sebelumnya
        deleteResetTokensForUser(user.ID)

        createResetToken(ResetTokenRecord{
            UserID:    user.ID,
            TokenHash: tokenHash,
            ExpiresAt: time.Now().Add(resetTokenExpiry),
            Used:      false,
        })

        // rawToken dikirim ke email, bukan tokenHash
        sendResetEmail(user.Email,
            "https://app.com/reset-password?token="+rawToken)
    }

    // Response selalu sama — jangan bedakan "email ada" vs "tidak ada"
    _ = jsonResponse(map[string]string{
        "message": "Jika email terdaftar, instruksi reset akan dikirimkan.",
    })
}

func resetPassword(rawToken, newPassword string) error {
    tokenHash := sha256Hex(rawToken)

    record := findResetToken(tokenHash, false)
    if record == nil {
        return errors.New("token tidak valid atau sudah digunakan")
    }
    if record.ExpiresAt.Before(time.Now()) {
        return errors.New("token sudah kedaluwarsa")
    }

    // Validasi password baru
    isValid, errorsList := validatePassword(newPassword)
    if !isValid {
        return errors.New(strings.Join(errorsList, "; "))
    }

    // Update password
    user := findUserByID(record.UserID)
    user.PasswordHash = hashPassword(newPassword)

    // Tandai token sebagai sudah digunakan (single-use)
    record.Used = true
    record.UsedAt = time.Now()

    // PENTING: invalidasi semua session aktif setelah reset
    // Jika account dikompromikan dan attacker yang set password,
    // session attacker juga harus diinvalidasi
    invalidateAllSessions(user.ID)

    saveResetToken(record)

    // Kirim notifikasi email bahwa password berhasil diubah
    sendPasswordChangedNotification(user.Email)
    return nil
}

Checklist Authentication #

PASSWORD:
  □ Password di-hash dengan Argon2id, bcrypt, atau scrypt — bukan MD5/SHA256
  □ Salt di-generate otomatis untuk setiap password (Argon2 melakukan ini)
  □ Panjang minimum 12 karakter
  □ Cek terhadap HaveIBeenPwned API untuk password yang diketahui bocor
  □ Tidak memaksa requirement kompleksitas yang tidak perlu (simbol wajib, dll)
  □ Tidak ada rotasi password periodik paksa

MFA:
  □ MFA tersedia untuk semua user
  □ MFA wajib untuk akun admin dan akun dengan privilege tinggi
  □ TOTP atau FIDO2 lebih diprioritaskan dari SMS OTP
  □ Backup codes tersedia jika device hilang
  □ Recovery process yang aman jika user kehilangan akses ke MFA

BRUTE FORCE PROTECTION:
  □ Rate limiting per IP dan per akun
  □ Account lockout setelah N percobaan gagal
  □ Pesan error tidak membedakan "email tidak ada" vs "password salah"
  □ Constant-time comparison untuk verifikasi credential
  □ Log semua percobaan gagal dengan metadata (IP, timestamp, user agent)

SESSION & TOKEN:
  □ Session ID dibuat dengan CSPRNG (secrets.token_urlsafe)
  □ JWT menggunakan algoritma eksplisit, expiry pendek, dan jti unique
  □ Session di-invalidasi saat logout
  □ Session di-invalidasi saat password diubah
  □ Idle timeout dan absolute timeout dikonfigurasi

PASSWORD RESET:
  □ Token reset dibuat dengan CSPRNG (32+ byte)
  □ Token reset di-hash sebelum disimpan di database
  □ Token hanya berlaku 15-60 menit
  □ Token single-use — langsung invalid setelah dipakai
  □ Response reset tidak membocorkan apakah email terdaftar
  □ Semua session di-invalidasi setelah reset password
  □ Notifikasi email dikirim setelah password berhasil diubah

MONITORING:
  □ Failed login attempts di-log dan di-alert jika anomali
  □ Login dari lokasi baru/tidak biasa memicu notifikasi
  □ Concurrent login dari lokasi yang tidak mungkin dideteksi

Ringkasan #

  • Authentication dan authorization adalah dua hal yang berbeda — authn memverifikasi identitas, authz memverifikasi hak akses. Keduanya harus dilakukan secara terpisah dan berurutan.
  • Argon2id adalah standar untuk password hashing — memory-hard, time-configurable, dengan salt otomatis. MD5, SHA256, dan enkripsi bukan pilihan yang valid untuk password.
  • Policy password yang terlalu ketat justru melemahkan keamanan — fokus pada panjang minimum dan cek terhadap database password yang bocor (HaveIBeenPwned), bukan requirement karakter khusus yang kompleks.
  • MFA adalah lapisan perlindungan yang paling efektif terhadap credential compromise — bahkan jika password bocor, attacker masih perlu faktor kedua. FIDO2/WebAuthn adalah yang paling aman karena tidak bisa di-phish.
  • Session-based auth memungkinkan revocation, JWT tidak — session bisa di-invalidasi kapan saja. JWT yang dikompromikan valid sampai expire, kecuali ada mekanisme revocation tambahan.
  • JWT harus menggunakan algoritma eksplisit dan expiry pendek — jangan izinkan ’none’ algorithm, gunakan HS256/RS256, set access token expire dalam 15 menit.
  • Brute force protection harus ada di dua level — per IP dan per akun. Keduanya diperlukan karena attacker bisa datang dari banyak IP atau targetkan satu akun dari satu IP.
  • Constant-time comparison untuk verifikasi credential mencegah timing attackhmac.compare_digest atau secrets.compare_digest, bukan == operator biasa.
  • Token reset password harus di-hash sebelum disimpan — sama seperti password, simpan hash-nya bukan token aslinya. Jika database bocor, token tidak langsung bisa dipakai.
  • Reset password harus invalidasi semua session aktif — jika akun dikompromikan dan attacker yang memicu reset, session mereka juga harus ikut dihapus.

← Sebelumnya: CSRF   Berikutnya: Authorization →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact