Build CoreOrdered learning track

Password Storage & Verification

Learn Java Authentication Pattern - Part 010

Password Storage & Verification untuk Java production-grade: Argon2id, BCrypt, PBKDF2, salt, pepper, parameter tuning, DelegatingPasswordEncoder, rehash strategy, legacy migration, verification pipeline, dan operational failure mode.

12 min read2349 words
PrevNext
Lesson 1040 lesson track09–22 Build Core
#java#authentication#password-storage#password-hashing+6 more

Part 010 — Password Storage & Verification

Target part ini: memahami password storage sebagai desain cryptographic-operational system. Kita akan membahas kenapa SHA-256 salah, kapan Argon2id/BCrypt/PBKDF2 dipakai, bagaimana menyimpan algorithm metadata, bagaimana migration tanpa memaksa semua user reset, bagaimana pepper dikelola, dan bagaimana verification pipeline di Java dibuat aman serta operasional.

Password storage sering disalahpahami sebagai:

hash(password) lalu simpan

Itu terlalu dangkal. Yang kita butuhkan:

password -> normalization policy -> pre-validation -> adaptive one-way function -> encoded credential -> metadata -> verification -> upgrade decision -> audit

Password storage yang baik harus tahan terhadap dua kondisi:

  1. online attack, saat attacker menebak lewat login endpoint;
  2. offline attack, saat attacker mencuri database hash dan menebak tanpa menyentuh sistem kita.

Rate limiting membantu online attack. Password hashing yang benar membantu offline attack.


1. Threat Model Password Storage

Misalkan attacker berhasil mendapatkan tabel password credential:

select account_id, password_hash from auth_password_credential;

Jika hash dibuat dengan SHA-256:

sha256(password)

attacker bisa mencoba tebakan sangat cepat menggunakan GPU/ASIC. Masalahnya bukan SHA-256 “rusak”; masalahnya SHA-256 memang dirancang cepat. Password storage membutuhkan fungsi yang sengaja mahal.

Proper password hashing function harus:

  • one-way;
  • salted;
  • adaptive/tunable;
  • lambat untuk attacker;
  • masih cukup cepat untuk user legitimate;
  • menyimpan metadata parameter;
  • mendukung upgrade parameter.

Untuk memory-hard algorithms seperti Argon2id/scrypt, kita juga ingin membuat parallel cracking mahal dari sisi memory, bukan hanya CPU.


2. Jangan Gunakan Ini untuk Password Storage

Jangan pakaiKenapa
Plaintextcredential leak langsung menjadi account takeover
Encryption reversiblejika key bocor, semua password bocor; password tidak perlu didekripsi
MD5/SHA-1cepat dan obsolete untuk password storage
SHA-256/SHA-512 langsungterlalu cepat untuk offline guessing
hash(password + staticSalt)static salt tidak melindungi antar-user
custom cryptohampir selalu salah
global pepper sebagai pengganti hash kuatpepper bukan pengganti Argon2/bcrypt/PBKDF2

Rule:

Untuk password, kita tidak butuh fungsi hash cepat.
Kita butuh password hashing/KDF yang sengaja mahal dan bisa di-tune.

3. Salt, Pepper, Work Factor

3.1 Salt

Salt adalah nilai unik per password hash.

Tujuannya:

password sama pada dua account menghasilkan hash berbeda.

Salt bukan secret. Ia boleh disimpan bersama hash. Password hashing library modern biasanya menyertakan salt di encoded hash string.

Bad:

hash = sha256(password)

Better:

hash = argon2id(password, randomSalt, memory, iterations, parallelism)
encoded = $argon2id$v=19$m=...,t=...,p=...$salt$hash

3.2 Pepper

Pepper adalah secret tambahan yang tidak disimpan di database yang sama dengan hash.

Tujuannya:

Jika database credential bocor tetapi pepper tidak bocor, offline cracking menjadi lebih sulit atau tidak bisa langsung dilakukan.

Pepper bukan replacement untuk salt. Pepper juga bukan alasan memakai SHA-256.

Pepper sebaiknya disimpan di:

  • KMS;
  • HSM;
  • secrets manager;
  • environment secret dengan rotation policy yang jelas.

3.3 Work Factor

Work factor adalah biaya verifikasi.

Untuk BCrypt: cost/log rounds.

Untuk Argon2id:

  • memory cost;
  • iteration/time cost;
  • parallelism.

Untuk PBKDF2:

  • iteration count;
  • PRF;
  • derived key length.

Work factor harus ditentukan dari benchmark sistem sendiri, bukan copy-paste.


4. Algorithm Decision Matrix

AlgorithmKelebihanKekuranganCocok untuk
Argon2idmemory-hard, modern, direkomendasikan luasbutuh library/parameter tuning; memory pressuredefault modern non-FIPS
BCryptmatang, luas, Spring support stabillimit/truncation historis; CPU-hard bukan memory-hardsistem umum, compatibility tinggi
PBKDF2FIPS-friendly, JDK/Spring support baiktidak memory-hard; perlu iteration tinggienvironment yang butuh FIPS/compliance
scryptmemory-hardlebih jarang dipakai dibanding Argon2id/BCrypt di Java enterprisecompatibility tertentu

Practical recommendation:

Default modern: Argon2id.
Compatibility enterprise: BCrypt.
FIPS-driven environment: PBKDF2.

Namun keputusan final harus mempertimbangkan:

  • compliance;
  • library availability;
  • CPU/memory budget;
  • login QPS;
  • latency SLO;
  • container memory limit;
  • disaster recovery;
  • migration path;
  • security team standard.

5. Encoded Password Format

Jangan hanya simpan hash mentah.

Bad:

$2a$10$....

Lebih baik simpan format yang memberi tahu verifier:

{bcrypt}$2a$12$...
{argon2id}$argon2id$v=19$m=65536,t=3,p=2$...
{pbkdf2@SpringSecurity_v5_8}...

Spring Security DelegatingPasswordEncoder memakai prefix {id} untuk memilih encoder.

Mental model:

Encoded password adalah self-describing credential artifact.

Ia harus menjawab:

algorithm apa?
parameter apa?
salt mana?
hash mana?
versi policy mana?
apakah perlu upgrade?

6. Credential Entity Design

public final class PasswordCredential {
    private final CredentialId id;
    private final AccountId accountId;
    private final long credentialVersion;
    private final String encodedHash;
    private final PasswordAlgorithm algorithm;
    private final String parameterProfile;
    private final Instant createdAt;
    private final Instant revokedAt;

    public boolean active() {
        return revokedAt == null;
    }
}

Database:

create table auth_password_credential (
    id uuid primary key,
    account_id uuid not null,
    credential_version bigint not null,
    encoded_hash text not null,
    algorithm varchar(80) not null,
    parameter_profile varchar(80) not null,
    created_at timestamptz not null,
    revoked_at timestamptz,
    revoke_reason varchar(80),
    unique(account_id, credential_version)
);

Kenapa algorithm tetap disimpan jika encoded hash sudah punya prefix?

Karena:

  • memudahkan query migration progress;
  • memudahkan audit;
  • memudahkan policy report;
  • memudahkan incident response saat algorithm tertentu dianggap lemah.

Tetapi sumber kebenaran verifikasi tetap encoded hash, bukan kolom algorithm yang bisa drift.


7. Verification Pipeline

Implementation principle:

Verify first. Upgrade after successful verification.

Jangan rehash password yang tidak valid. Jangan mengubah credential ketika verification gagal.


8. Spring Security PasswordEncoder Pattern

Spring Security memberi abstraction PasswordEncoder.

Minimal:

PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

String encoded = encoder.encode(rawPassword);
boolean valid = encoder.matches(rawPassword, encoded);

Production-grade config sebaiknya eksplisit.

@Configuration
public class PasswordHashingConfig {

    @Bean
    PasswordEncoder passwordEncoder() {
        String currentId = "argon2id-v1";

        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("argon2id-v1", new Argon2PasswordEncoder(
                16,      // salt length
                32,      // hash length
                2,       // parallelism
                65536,   // memory KiB
                3        // iterations
        ));
        encoders.put("bcrypt-v1", new BCryptPasswordEncoder(12));
        encoders.put("pbkdf2-v1", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());

        return new DelegatingPasswordEncoder(currentId, encoders);
    }
}

Catatan:

  • parameter di atas adalah contoh profile, bukan angka universal;
  • benchmark di environment sendiri;
  • Spring version dapat mengubah defaults antargenerasi;
  • simpan profile id agar migration jelas.

9. Password Hash Policy

Buat object policy eksplisit.

public record PasswordHashPolicy(
        String currentEncoderId,
        Set<String> acceptedEncoderIds,
        Set<String> deprecatedEncoderIds,
        Duration targetVerificationLatency,
        int maxPasswordCodePoints
) {
    public boolean upgradeRequired(String encodedHash) {
        String id = extractId(encodedHash);
        return deprecatedEncoderIds.contains(id) || !id.equals(currentEncoderId);
    }
}

Policy harus menjawab:

PertanyaanContoh jawaban
encoder baru untuk password baru?argon2id-v1
hash lama masih diterima?bcrypt-v1, pbkdf2-v1
kapan rehash?setelah login sukses
kapan force reset?hash legacy terlalu lemah atau format tidak dapat diverifikasi aman
berapa maksimum password length?1024 code points atau sesuai risk/capacity

10. Rehash-on-Login Strategy

Migration paling halus:

Saat user login sukses dengan hash lama, sistem membuat hash baru dengan policy saat ini.
public final class PasswordVerifier {
    private final PasswordEncoder passwordEncoder;
    private final PasswordHashPolicy policy;
    private final PasswordCredentialRepository repository;

    public VerificationResult verify(AccountId accountId, char[] rawPassword, PasswordCredential credential) {
        String raw = CharBuffer.wrap(rawPassword).toString();
        boolean matches = passwordEncoder.matches(raw, credential.encodedHash());

        if (!matches) {
            return VerificationResult.invalid();
        }

        boolean upgraded = false;
        if (policy.upgradeRequired(credential.encodedHash())) {
            String newEncoded = passwordEncoder.encode(raw);
            repository.replaceCredential(accountId, credential.id(), newEncoded, policy.currentEncoderId());
            upgraded = true;
        }

        return VerificationResult.valid(upgraded);
    }
}

Caveat:

String raw = ... membuat copy password di heap.

Spring PasswordEncoder umumnya memakai CharSequence, sehingga copy kadang sulit dihindari. Tetap clear buffer yang kita kontrol, hindari logging, dan minimalkan lifetime object.

10.1 Race Condition saat Rehash

Dua login paralel bisa sama-sama mencoba rehash.

Mitigation:

update auth_password_credential
set encoded_hash = ?, algorithm = ?, parameter_profile = ?, updated_at = now()
where id = ?
  and encoded_hash = ?
  and revoked_at is null;

Jika row count 0, ada concurrent update. Tidak masalah selama verification sudah sukses dan credential masih valid.


11. Legacy Hash Migration

Legacy case umum:

md5(password)
sha1(password)
sha256(password)
bcrypt cost terlalu rendah
PBKDF2 iteration terlalu rendah
custom salted hash

Migration options:

StrategyKelebihanKekurangan
rehash-on-loginUX smoothuser inactive tetap legacy
forced resetcepat menghapus legacyfriction tinggi
wrapped legacy hashtidak butuh plaintextbisa membawa kelemahan lama
staged migrationkontrol risikobutuh operasi matang

11.1 Wrapped Legacy Hash

Kadang sistem lama menyimpan:

sha1(password + salt)

Saat login sukses, langsung rehash password mentah ke Argon2id/BCrypt. Jangan membuat desain jangka panjang seperti:

argon2id(sha1(password + salt))

kecuali sebagai bridge sementara dengan dokumen risiko. Wrapping bisa berguna untuk migration tanpa plaintext, tetapi tidak selalu meningkatkan security seperti yang diharapkan karena input entropy tetap password user dan format lama mungkin punya kelemahan.

11.2 Migration State

public enum CredentialMigrationStatus {
    CURRENT,
    DEPRECATED_ACCEPTED,
    LEGACY_REHASH_ON_LOGIN,
    FORCE_RESET_REQUIRED,
    REVOKED
}

Policy:

Legacy hash yang terlalu lemah tidak harus selalu diterima selamanya.
Untuk sistem high-risk, pakai force reset setelah cut-off date.

12. Pepper Design

Ada dua pola umum.

12.1 Pre-hash Pepper

hash = Argon2id(HMAC(pepper, password), salt, params)

Kelebihan:

  • database leak saja tidak cukup untuk cracking langsung;
  • pepper dipakai sebelum password hashing.

Kekurangan:

  • rotate pepper sulit tanpa password mentah;
  • jika pepper hilang, semua password tidak dapat diverifikasi;
  • perlu secret availability tinggi.

12.2 Post-hash Pepper

stored = HMAC(pepper, passwordHash)

Atau simpan hash dan MAC terpisah.

Kelebihan:

  • bisa memvalidasi integrity hash;
  • rotation model bisa lebih fleksibel tergantung desain.

Kekurangan:

  • design lebih kompleks;
  • salah implementasi bisa mengurangi benefit;
  • tetap butuh Argon2/bcrypt/PBKDF2.

12.3 Pepper Versioning

public record PepperProfile(
        String version,
        SecretKey key,
        boolean activeForNewHash,
        boolean acceptedForVerification
) {}

Credential menyimpan pepper version, bukan pepper value.

alter table auth_password_credential
add column pepper_version varchar(40);

12.4 Pepper Rotation Reality

Pepper rotation tidak seperti API key rotation.

Jika pepper dipakai untuk password hashing, kita tidak bisa rehash semua password tanpa password mentah. Pilihan:

  • rotate on next successful login;
  • force reset untuk user tertentu;
  • support multiple pepper versions selama transition;
  • incident response jika pepper diduga bocor.

Rule:

Jangan menambahkan pepper jika tim belum siap mengoperasikan lifecycle-nya.

13. Parameter Tuning

Tidak ada angka universal. Parameter harus memenuhi:

verification cukup lambat untuk attacker,
tetapi cukup cepat untuk legitimate login SLO dan kapasitas peak.

Benchmark minimal:

public final class PasswordHashBenchmark {
    public static void main(String[] args) {
        PasswordEncoder encoder = new Argon2PasswordEncoder(16, 32, 2, 65536, 3);
        String password = "correct horse battery staple";

        int warmup = 10;
        int runs = 100;

        for (int i = 0; i < warmup; i++) encoder.encode(password);

        List<Long> millis = new ArrayList<>();
        for (int i = 0; i < runs; i++) {
            long start = System.nanoTime();
            String encoded = encoder.encode(password);
            encoder.matches(password, encoded);
            long end = System.nanoTime();
            millis.add(TimeUnit.NANOSECONDS.toMillis(end - start));
        }

        millis.sort(Long::compareTo);
        System.out.println("p50=" + percentile(millis, 50));
        System.out.println("p95=" + percentile(millis, 95));
        System.out.println("p99=" + percentile(millis, 99));
    }
}

Benchmark harus dilakukan pada:

  • container limit sebenarnya;
  • instance type production;
  • JVM flags production;
  • concurrency login realistis;
  • cold start scenario;
  • peak attack scenario.

13.1 Capacity Formula Sederhana

required_auth_workers ≈ peak_login_qps * p95_verification_seconds / target_utilization

Contoh:

peak_login_qps = 100
p95_verify = 250ms = 0.25s
target_utilization = 0.70
workers ≈ 100 * 0.25 / 0.70 = 35.7

Ini kasar, tetapi cukup untuk memaksa diskusi kapasitas.

Untuk Argon2id, memory juga penting:

max_concurrent_hashes <= available_memory_for_hashing / memory_per_hash

Jika memory per hash 64 MiB dan budget memory 2 GiB:

max_concurrent_hashes ≈ 2048 / 64 = 32

Jadi auth service harus punya concurrency guard.


14. Hashing Worker Isolation

Jangan biarkan expensive password hash menghabiskan request thread pool utama.

Pattern:

Java sketch:

public final class BoundedPasswordHashExecutor {
    private final ExecutorService executor;
    private final Semaphore permits;

    public BoundedPasswordHashExecutor(int concurrency) {
        this.executor = Executors.newFixedThreadPool(concurrency);
        this.permits = new Semaphore(concurrency);
    }

    public <T> T run(Callable<T> task) {
        boolean acquired = permits.tryAcquire();
        if (!acquired) {
            throw new AuthenticationOverloadedException();
        }
        try {
            Future<T> future = executor.submit(task);
            return future.get(2, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            throw new AuthenticationOverloadedException(e);
        } catch (ExecutionException | InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new AuthenticationFailureException(e);
        } finally {
            permits.release();
        }
    }
}

Caveat:

  • jangan asal interrupt hashing library jika tidak mendukung cancellation;
  • timeout harus dipilih hati-hati;
  • overload response jangan bocorkan account existence;
  • metric wajib ada.

15. Constant-Time Comparison

Password hashing library biasanya mengurus compare hash secara aman. Tetapi untuk token hash, reset token, atau API key hash, gunakan constant-time compare.

public final class ConstantTime {
    public static boolean equals(byte[] a, byte[] b) {
        return MessageDigest.isEqual(a, b);
    }
}

Jangan:

Arrays.equals(a, b)

untuk secret comparison yang exposed terhadap timing. Untuk password, jangan implement compare sendiri jika library sudah benar.


16. Unicode dan Password Input

Unicode password adalah area rawan.

Pilihan policy:

  1. simpan/verifikasi byte persis dari input setelah transport decoding;
  2. lakukan normalization tertentu sebelum hashing;
  3. batasi karakter tertentu.

Trade-off:

  • normalization bisa membantu user yang mengetik karakter ekivalen;
  • normalization bisa membuat dua string berbeda menjadi sama;
  • tanpa normalization, user bisa terkunci karena keyboard/input method berbeda;
  • logging/debugging Unicode bisa bocor.

Practical approach:

Tetapkan policy eksplisit.
Jangan diam-diam mengubah policy setelah credential sudah dibuat.
Simpan password_policy_version pada credential jika normalization policy berubah.

Example:

public interface PasswordInputCanonicalizer {
    char[] canonicalize(char[] rawPassword);
    String policyVersion();
}

Untuk banyak sistem, pendekatan yang aman adalah menerima passphrase panjang, mendukung space, dan tidak menerapkan composition transformation aneh. Jika melakukan Unicode normalization, dokumentasikan dan versioning policy.


17. Maximum Password Length dan DoS

“Allow long passwords” bukan berarti tanpa batas.

Hashing password 10 MB bisa membuat DoS.

Set batas:

public final class PasswordPreValidator {
    private static final int MAX_CODE_POINTS = 1024;

    public void validateBeforeHash(char[] password) {
        if (password == null) throw new InvalidPasswordException();
        int cp = new String(password).codePointCount(0, password.length);
        if (cp > MAX_CODE_POINTS) throw new InvalidPasswordException();
    }
}

Juga set:

  • HTTP request body limit;
  • JSON parser limit;
  • reverse proxy body limit;
  • login endpoint rate limit.

18. Breached Password Checking

Password hashing melindungi setelah database bocor. Ia tidak mencegah user memilih password yang sudah ada di breach corpus.

Pattern:

registration/change password -> check known compromised password -> reject

Jangan kirim password mentah ke layanan eksternal.

Options:

  • local denylist untuk common passwords;
  • k-anonymity API seperti prefix hash lookup;
  • offline breach corpus internal;
  • commercial password intelligence.

Java port:

public interface BreachedPasswordChecker {
    boolean isKnownCompromised(char[] password);
}

Implementation harus:

  • tidak log password;
  • tidak cache password mentah;
  • time out dengan policy jelas;
  • memiliki fail-open/fail-closed per risk tier.

19. Password Verification Result

Return result yang kaya, bukan boolean.

public record PasswordVerificationResult(
        boolean valid,
        boolean hashUpgraded,
        String encoderId,
        String parameterProfile,
        AuthenticationAssuranceLevel assuranceLevel
) {
    public static PasswordVerificationResult invalid() {
        return new PasswordVerificationResult(false, false, null, null, null);
    }
}

Kenapa perlu encoderId?

  • audit migration;
  • metrics algorithm distribution;
  • incident response;
  • detecting weak hash still used.

Jangan kirim field ini ke client.


20. Observability

Metrics penting:

auth_password_verify_duration_seconds{encoder_id, outcome}
auth_password_hash_upgrade_total{from,to}
auth_password_verify_failure_total{reason}
auth_password_hash_overload_total
auth_password_credential_algorithm_count{algorithm}
auth_password_breached_rejected_total
auth_password_legacy_force_reset_total

Log event harus punya:

  • correlation id;
  • account id jika ada;
  • identifier hash jika account tidak ada;
  • algorithm/profile id;
  • outcome;
  • failure reason internal;
  • no raw password;
  • no encoded hash kecuali redacted.

Bad log:

Login failed for alice@example.com with password hunter2

Bad log:

Password hash for account 123 = {bcrypt}$2a$...

Better:

{
  "event": "auth.password.verify.failed",
  "accountId": "acc_123",
  "identifierHash": "sha256:...",
  "reason": "PASSWORD_INVALID",
  "correlationId": "req_abc",
  "encoderId": "bcrypt-v1"
}

21. Testing Strategy

21.1 Unit Tests

  • valid password matches;
  • invalid password fails;
  • deprecated hash upgrades after success;
  • deprecated hash does not upgrade after failure;
  • max length rejected before hash;
  • unknown account uses synthetic verifier;
  • revoked credential cannot authenticate;
  • race on rehash is safe.

21.2 Property Tests

Useful invariants:

encode(password) != encode(password) for same password due to salt.
matches(password, encode(password)) always true.
matches(other, encode(password)) false with high probability.

21.3 Performance Tests

  • p50/p95/p99 encode/matches latency;
  • concurrent login under attack;
  • memory pressure for Argon2id;
  • Redis/rate limiter unavailable scenario;
  • GC behavior under failed login flood.

21.4 Migration Tests

  • legacy MD5 accepted before cut-off;
  • legacy MD5 rehashed on success;
  • legacy MD5 rejected after cut-off;
  • bcrypt low cost rehashed;
  • pepper version old accepted;
  • pepper version retired rejected/force reset.

22. Failure Modes

22.1 Hash Parameter Too Weak

Symptom:

bcrypt cost 4, PBKDF2 iteration rendah, Argon2 memory terlalu kecil.

Impact:

Database leak lebih mudah di-crack.

Mitigation:

  • credential inventory;
  • rehash-on-login;
  • forced reset for dormant accounts;
  • parameter benchmark schedule.

22.2 Hash Parameter Too Strong

Symptom:

Login latency tinggi, CPU/memory habis saat traffic spike.

Impact:

Authentication outage.

Mitigation:

  • benchmark;
  • bounded executor;
  • rate limit before hash;
  • autoscaling;
  • degrade safely.

22.3 Pepper Lost

Symptom:

Semua password verification gagal.

Impact:

Mass lockout.

Mitigation:

  • backup/escrow policy;
  • KMS/HSM HA;
  • pepper versioning;
  • disaster runbook;
  • emergency recovery flow.

22.4 Pepper Leaked

Symptom:

Pepper secret exposed in logs/repo/secret dump.

Impact:

Jika DB hash juga bocor, offline attack lebih mudah.

Mitigation:

  • rotate pepper with staged login rehash;
  • force reset high-risk accounts;
  • investigate DB exposure;
  • revoke sessions if credential compromise suspected;
  • incident communication.

22.5 Logging Raw Password

Symptom:

Request logging middleware menyimpan body /login.

Impact:

Credential leak dari log system.

Mitigation:

  • redact sensitive fields at ingress;
  • disable body logging for auth endpoints;
  • secret scanning logs;
  • incident response.

23. Production Runbook: Password Hash Upgrade

Step 1 — Inventory

Query:

select algorithm, parameter_profile, count(*)
from auth_password_credential
where revoked_at is null
group by algorithm, parameter_profile
order by count(*) desc;

Step 2 — Define Target

currentEncoderId = argon2id-v2
accepted = argon2id-v1, bcrypt-v1, pbkdf2-v1
forceReset = md5-v1, sha1-v1

Step 3 — Deploy Accept-All Verify Policy

Pastikan semua format yang masih valid bisa diverifikasi.

Step 4 — Rehash on Successful Login

Aktifkan upgrade setelah verification sukses.

Step 5 — Notify Dormant Accounts

Untuk user yang tidak login setelah periode tertentu, minta reset.

Step 6 — Cut Off Legacy

Setelah date tertentu, legacy terlalu lemah masuk FORCE_RESET_REQUIRED.

Step 7 — Monitor

Monitor:

  • upgrade rate;
  • login failure spike;
  • support tickets;
  • auth latency;
  • algorithm distribution.

24. End-to-End Java Sketch

public final class ProductionPasswordCredentialService {
    private final PasswordEncoder passwordEncoder;
    private final PasswordHashPolicy hashPolicy;
    private final PasswordPreValidator preValidator;
    private final BreachedPasswordChecker breachedPasswordChecker;
    private final PasswordCredentialRepository repository;

    public PasswordCredential create(AccountId accountId, char[] rawPassword) {
        preValidator.validateBeforeHash(rawPassword);

        if (breachedPasswordChecker.isKnownCompromised(rawPassword)) {
            throw new WeakPasswordException("Password is known to be compromised");
        }

        String encoded = passwordEncoder.encode(CharBuffer.wrap(rawPassword));
        return repository.insert(accountId, encoded, hashPolicy.currentEncoderId());
    }

    public PasswordVerificationResult verify(AccountId accountId, char[] rawPassword) {
        preValidator.validateBeforeHash(rawPassword);

        PasswordCredential credential = repository.findActiveByAccountId(accountId)
                .orElseThrow(() -> new MissingCredentialException(accountId));

        String raw = CharBuffer.wrap(rawPassword).toString();
        boolean valid = passwordEncoder.matches(raw, credential.encodedHash());
        if (!valid) {
            return PasswordVerificationResult.invalid();
        }

        boolean upgraded = false;
        if (hashPolicy.upgradeRequired(credential.encodedHash())) {
            String upgradedHash = passwordEncoder.encode(raw);
            repository.replaceCredential(accountId, credential.id(), upgradedHash, hashPolicy.currentEncoderId());
            upgraded = true;
        }

        return new PasswordVerificationResult(
                true,
                upgraded,
                extractId(credential.encodedHash()),
                credential.parameterProfile(),
                AuthenticationAssuranceLevel.AAL1
        );
    }
}

Production improvements:

  • wrap hashing in bounded executor;
  • clear char[] after use;
  • avoid converting to String if selected library supports char/byte directly;
  • add audit outside service;
  • add transaction boundary for replacement;
  • add optimistic locking.

25. Checklist

Algorithm

  • Tidak memakai plaintext/reversible encryption/MD5/SHA langsung.
  • Menggunakan Argon2id, BCrypt, PBKDF2, atau scrypt sesuai policy.
  • Parameter di-benchmark di environment production-like.
  • Encoded hash self-describing dengan algorithm id.
  • Salt unik per password.
  • Pepper lifecycle jelas jika dipakai.

Storage

  • Credential punya version.
  • Credential bisa revoked tanpa menghapus audit history.
  • Algorithm/profile bisa di-query untuk inventory.
  • Hash tidak pernah muncul di log/event/client.
  • Reset token/API key tidak memakai password hash table yang sama tanpa purpose separation.

Verification

  • Max password length dicek sebelum hashing.
  • Unknown account path memakai synthetic verification.
  • Rehash hanya setelah successful verification.
  • Deprecated hash punya migration policy.
  • Hashing concurrency dibatasi.
  • Metrics latency dan overload tersedia.

Operations

  • Ada runbook hash upgrade.
  • Ada runbook pepper lost/leaked.
  • Ada credential inventory dashboard.
  • Ada forced reset path untuk legacy high-risk.
  • Ada testing untuk migration dan race condition.

26. Ringkasan

Password storage production-grade adalah desain lifecycle:

policy -> validation -> adaptive hashing -> encoded metadata -> verification -> rehash -> migration -> incident response

Ingat prinsip utama:

Fast hash is a feature for checksums.
Fast hash is a bug for password storage.

Gunakan Argon2id/BCrypt/PBKDF2/scrypt dengan parameter yang ditentukan melalui benchmark. Simpan metadata. Dukung migration. Jangan lupakan overload, pepper lifecycle, logging, dan legacy cut-off.

Di part berikutnya kita akan membangun Login Flow as State Machine: attempt, challenge, success, failure, lock, recovery, concurrency, idempotency, dan domain event yang membuat authentication flow bisa diaudit dan diuji secara ketat.


References

  • Spring Security Reference — Password Storage and PasswordEncoder / DelegatingPasswordEncoder.
  • OWASP Password Storage Cheat Sheet — password hashing, salt, pepper, Argon2id, bcrypt, PBKDF2 guidance.
  • OWASP Authentication Cheat Sheet — authentication response, password policy, anti-enumeration, and login defense guidance.
  • NIST SP 800-63B-4 — password authenticator and verifier requirements in digital identity systems.
  • RFC 9106 — Argon2 memory-hard password hashing and key derivation function.
Lesson Recap

You just completed lesson 10 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.