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, SECRET_KEY)
# Enkripsi bisa di-decrypt. Jika SECRET_KEY bocor, semua password bocor.

# ANTI-PATTERN 3: hash tanpa salt — rentan rainbow table attack
import hashlib
user.password = hashlib.sha256(password.encode()).hexdigest()
# Dua user dengan password sama menghasilkan hash sama
# → attacker bisa precompute rainbow table dan langsung match

# ANTI-PATTERN 4: hash cepat — rentan brute force
user.password = hashlib.sha256(password.encode()).hexdigest()
# 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
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

# Konfigurasi Argon2id (rekomendasi OWASP 2023)
ph = PasswordHasher(
    time_cost=3,       # jumlah iterasi — lebih tinggi = lebih lambat = lebih aman
    memory_cost=65536, # 64MB RAM — mencegah GPU/ASIC attack
    parallelism=2,     # jumlah thread
    hash_len=32,       # panjang hash output
    salt_len=16        # panjang salt (otomatis di-generate)
)

def hash_password(password: str) -> str:
    """Hash password untuk disimpan di database."""
    return ph.hash(password)
    # Output: $argon2id$v=19$m=65536,t=3,p=2$<salt>$<hash>
    # Salt dan parameter disimpan bersama hash — tidak perlu kolom terpisah

def verify_password(stored_hash: str, provided_password: str) -> bool:
    """Verifikasi password saat login."""
    try:
        ph.verify(stored_hash, provided_password)
        return True
    except VerifyMismatchError:
        return False

def needs_rehash(stored_hash: str) -> bool:
    """Cek apakah hash perlu di-upgrade (parameter berubah)."""
    return ph.check_needs_rehash(stored_hash)
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)

MINIMUM_LENGTH = 12     # panjang minimum, bukan maksimum
MAXIMUM_LENGTH = 128    # ada maksimum untuk mencegah DoS via hash computation
PWNED_API_URL = "https://api.pwnedpasswords.com/range/{}"

def validate_password(password: str) -> tuple[bool, list[str]]:
    errors = []

    # 1. Panjang minimum
    if len(password) < MINIMUM_LENGTH:
        errors.append(f"Password minimal {MINIMUM_LENGTH} karakter")

    # 2. Panjang maksimum
    if len(password) > MAXIMUM_LENGTH:
        errors.append(f"Password maksimal {MAXIMUM_LENGTH} karakter")

    # 3. Cek di database password yang diketahui bocor (HaveIBeenPwned API)
    if is_password_pwned(password):
        errors.append(
            "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

def is_password_pwned(password: str) -> bool:
    """Cek HaveIBeenPwned menggunakan k-Anonymity — password tidak dikirim."""
    import hashlib
    import requests

    sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
    prefix = sha1[:5]
    suffix = sha1[5:]

    response = requests.get(PWNED_API_URL.format(prefix), timeout=5)
    if response.status_code != 200:
        return False  # Jika API gagal, jangan blokir user

    for line in response.text.splitlines():
        hash_suffix, count = line.split(':')
        if hash_suffix == suffix:
            return int(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 pyotp
import pyotp
import qrcode
import secrets

def setup_totp_for_user(user_id: int, username: str) -> dict:
    """Generate TOTP secret dan QR code untuk setup MFA."""

    # Generate secret key yang aman
    secret = pyotp.random_base32()

    # Simpan secret ke database (terenkripsi!)
    store_mfa_secret(user_id, encrypt_secret(secret))

    # Generate provisioning URI untuk QR code
    totp = pyotp.TOTP(secret)
    provisioning_uri = totp.provisioning_uri(
        name=username,
        issuer_name="MyApp"
    )

    # Generate QR code
    qr = qrcode.make(provisioning_uri)
    # Tampilkan QR code ke user untuk di-scan dengan authenticator app

    # Juga tampilkan secret key sebagai teks (untuk manual entry)
    return {
        'secret': secret,  # tampilkan SEKALI, jangan simpan di plain
        'provisioning_uri': provisioning_uri,
        # backup codes untuk recovery
        'backup_codes': generate_backup_codes(user_id)
    }

def verify_totp(user_id: int, provided_code: str) -> bool:
    """Verifikasi TOTP code dari user."""
    encrypted_secret = get_mfa_secret(user_id)
    secret = decrypt_secret(encrypted_secret)

    totp = pyotp.TOTP(secret)

    # valid_window=1 mengizinkan 1 interval maju/mundur (30 detik)
    # untuk mengakomodasi sedikit perbedaan waktu antara server dan device
    return totp.verify(provided_code, valid_window=1)

def generate_backup_codes(user_id: int) -> list[str]:
    """Generate backup codes untuk recovery jika device hilang."""
    codes = [secrets.token_hex(4).upper() for _ in range(10)]
    # Simpan hash dari backup codes (bukan plaintext)
    # User hanya bisa lihat sekali saat setup
    store_backup_codes_hashed(user_id, 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):

  Server              Database/Redis          Client
  ┌──────┐           ┌──────────────┐        ┌──────────┐
  │Login │──store──→ │session_id:   │        │Cookie:   │
  │      │           │  user_id: 1  │ ←read─ │session=X │
  │      │ ←set─────│  expires: ..  │        │          │
  └──────┘           └──────────────┘        └──────────┘

  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:

  Server              Client
  ┌──────┐           ┌──────────────────────────────────┐
  │Login │──sign───→ │Header: eyJhbGciOiJSUzI1NiJ9.    │
  │      │           │Payload: eyJ1c2VyX2lkIjoxfQ.      │
  │      │ ←send────│Signature: ...                      │
  └──────┘           └──────────────────────────────────┘

  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 #

import jwt
import secrets
from datetime import datetime, timedelta

# ANTI-PATTERN 1: algoritma "none" — mematikan signature verification
token = jwt.encode({'user_id': 1}, None, algorithm='none')
decoded = jwt.decode(token, options={"verify_signature": False})
# Siapapun bisa membuat token tanpa signature

# ANTI-PATTERN 2: kunci HS256 yang lemah
token = jwt.encode({'user_id': 1}, "secret", algorithm='HS256')
# "secret" bisa di-crack dengan wordlist attack dalam detik

# ANTI-PATTERN 3: expiry yang terlalu panjang
payload = {
    'user_id': 1,
    'exp': datetime.utcnow() + timedelta(days=365)  # 1 tahun!
}
# Token yang dikompromikan valid selama 1 tahun tanpa cara revoke

# BENAR: konfigurasi JWT yang aman
import os

# HS256: secret key minimal 256-bit (32 byte)
JWT_SECRET = os.environ['JWT_SECRET']  # 32+ byte random key
assert len(JWT_SECRET) >= 32, "JWT secret too short"

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

def create_access_token(user_id: int, additional_claims: dict = None) -> str:
    now = datetime.utcnow()
    payload = {
        'sub': str(user_id),           # subject
        'iat': now,                     # issued at
        'exp': now + timedelta(minutes=15),  # expiry pendek!
        'jti': secrets.token_urlsafe(16),    # unique token ID
        'type': 'access',              # bedakan access vs refresh token
        **(additional_claims or {})
    }
    return jwt.encode(payload, JWT_SECRET, algorithm='HS256')

def verify_access_token(token: str) -> dict:
    try:
        payload = jwt.decode(
            token,
            JWT_SECRET,
            algorithms=['HS256'],   # eksplisit — jangan ['HS256', 'none']
            options={
                'require': ['exp', 'iat', 'sub', 'jti', 'type']
            }
        )
        if payload.get('type') != 'access':
            raise ValueError("Wrong token type")

        # Opsional: cek token blacklist jika butuh revocation
        if is_token_revoked(payload['jti']):
            raise ValueError("Token has been revoked")

        return payload
    except jwt.ExpiredSignatureError:
        raise AuthError("Token expired, please login again")
    except jwt.InvalidTokenError as e:
        raise AuthError(f"Invalid token: {e}")

Brute Force Protection #

import time
from collections import defaultdict

class RateLimiter:
    def __init__(self, max_attempts: int, window_seconds: int,
                 lockout_seconds: int):
        self.max_attempts = max_attempts
        self.window_seconds = window_seconds
        self.lockout_seconds = lockout_seconds

    def check_rate_limit(self, identifier: str) -> tuple[bool, int]:
        """
        Returns: (is_allowed, retry_after_seconds)
        """
        key = f"login_attempts:{identifier}"
        lockout_key = f"login_lockout:{identifier}"

        # Cek apakah sedang dalam lockout
        lockout_remaining = redis_client.ttl(lockout_key)
        if lockout_remaining > 0:
            return False, lockout_remaining

        # Cek jumlah percobaan dalam window
        attempts = redis_client.get(key)
        if attempts and int(attempts) >= self.max_attempts:
            # Set lockout
            redis_client.setex(lockout_key, self.lockout_seconds, 1)
            redis_client.delete(key)
            return False, self.lockout_seconds

        return True, 0

    def record_attempt(self, identifier: str):
        """Record satu attempt."""
        key = f"login_attempts:{identifier}"
        pipe = redis_client.pipeline()
        pipe.incr(key)
        pipe.expire(key, self.window_seconds)
        pipe.execute()

    def reset_attempts(self, identifier: str):
        """Reset setelah login berhasil."""
        redis_client.delete(f"login_attempts:{identifier}")
        redis_client.delete(f"login_lockout:{identifier}")

# Limiter per IP: 10 percobaan per 5 menit
ip_limiter = RateLimiter(max_attempts=10, window_seconds=300, lockout_seconds=300)

# Limiter per akun: 5 percobaan per 15 menit (lebih ketat)
account_limiter = RateLimiter(max_attempts=5, window_seconds=900, lockout_seconds=900)

@app.route('/login', methods=['POST'])
def login():
    email = request.json.get('email', '')
    password = request.json.get('password', '')
    ip = request.remote_addr

    # Cek rate limit per IP
    allowed, retry_after = ip_limiter.check_rate_limit(ip)
    if not allowed:
        return jsonify({
            'error': 'Terlalu banyak percobaan. Coba lagi dalam beberapa menit.',
        }), 429, {'Retry-After': str(retry_after)}

    # Cek rate limit per akun
    allowed, retry_after = account_limiter.check_rate_limit(email)
    if not allowed:
        return jsonify({
            'error': 'Akun sementara terkunci karena terlalu banyak percobaan.',
        }), 429, {'Retry-After': str(retry_after)}

    # Record attempt SEBELUM verifikasi (untuk mencegah timing attack)
    ip_limiter.record_attempt(ip)
    account_limiter.record_attempt(email)

    user = User.query.filter_by(email=email).first()

    # PENTING: selalu verifikasi password (timing-safe)
    # Jika user tidak ada, tetap jalankan verify untuk waktu yang konsisten
    if user is None:
        ph.verify("dummy_hash", password)  # timing dummy
        return jsonify({'error': 'Email atau password tidak valid'}), 401

    if not verify_password(user.password_hash, password):
        return jsonify({'error': 'Email atau password tidak valid'}), 401

    # Login berhasil — reset rate limit
    ip_limiter.reset_attempts(ip)
    account_limiter.reset_attempts(email)

    # Cek apakah hash perlu di-upgrade
    if needs_rehash(user.password_hash):
        user.password_hash = hash_password(password)
        db.session.commit()

    session_token = create_session(user.id, request)
    response = make_response(jsonify({'status': 'ok'}))
    set_auth_cookie(response, session_token)
    return response

Secure Password Reset #

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

import secrets
import hashlib
from datetime import datetime, timedelta

RESET_TOKEN_EXPIRY = timedelta(minutes=15)

def request_password_reset(email: str):
    """Kirim email reset password ke user."""
    user = User.query.filter_by(email=email).first()

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

    if user:
        # Generate token yang kuat
        raw_token = secrets.token_urlsafe(32)

        # Simpan HASH dari token (bukan token itu sendiri)
        # Jika database bocor, token tidak langsung bisa dipakai
        token_hash = hashlib.sha256(raw_token.encode()).hexdigest()

        # Invalidasi semua token reset sebelumnya
        PasswordResetToken.query.filter_by(user_id=user.id).delete()

        PasswordResetToken.create(
            user_id=user.id,
            token_hash=token_hash,
            expires_at=datetime.utcnow() + RESET_TOKEN_EXPIRY,
            used=False
        )

        send_reset_email(
            to=user.email,
            reset_url=f"https://app.com/reset-password?token={raw_token}"
            # raw_token dikirim ke email, bukan token_hash
        )

    # Response selalu sama — jangan bedakan "email ada" vs "email tidak ada"
    return jsonify({
        'message': 'Jika email terdaftar, instruksi reset akan dikirimkan.'
    })

def reset_password(raw_token: str, new_password: str):
    """Proses reset password dengan token dari email."""
    token_hash = hashlib.sha256(raw_token.encode()).hexdigest()

    record = PasswordResetToken.query.filter_by(
        token_hash=token_hash,
        used=False
    ).first()

    if not record:
        raise InvalidTokenError("Token tidak valid atau sudah digunakan")

    if record.expires_at < datetime.utcnow():
        raise InvalidTokenError("Token sudah kadaluarsa")

    # Validasi password baru
    is_valid, errors = validate_password(new_password)
    if not is_valid:
        raise ValidationError(errors)

    # Update password
    user = User.query.get(record.user_id)
    user.password_hash = hash_password(new_password)

    # Tandai token sebagai sudah digunakan (single-use)
    record.used = True
    record.used_at = datetime.utcnow()

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

    db.session.commit()

    # Kirim notifikasi email bahwa password berhasil diubah
    send_password_changed_notification(user.email)

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