Password policy
Every password on create / change must satisfy:
- Length - at least
Z4J_PASSWORD_MIN_LENGTHcharacters (default 8). Max 256. - Character classes - at least 3 of 4 of: lowercase, uppercase, digit, symbol.
- Not in denylist - a ~1,500-entry list of common passwords and breached patterns.
Denylist composition
Section titled “Denylist composition”- Top 1,000 common passwords (rockyou, SecLists breach compilations).
- Seasonal patterns (
Summer2024,Winter24, etc.) with year variants for the past 10 years. - Product-name variants for common SaaS (
<product>123,<product>!1).
Generated via _generate_patterns() in z4j_brain.auth.common_passwords. Static; baked at build time.
argon2id, OWASP 2024 defaults:
time_cost=3memory_cost=64 MiBparallelism=4- 32-byte hash, 16-byte salt
Tuneable via Z4J_ARGON2_* env vars. Target: 50–80 ms per verify on a 2024 server CPU.
Rehash on login
Section titled “Rehash on login”If stored-hash parameters are weaker than current config, the brain rehashes on successful verify. Silent upgrade for users who were around before a parameter bump.
Error codes
Section titled “Error codes”password_too_short(< min length)password_too_long(> 256)password_too_simple(< 3 of 4 classes)password_in_breach_list(matches denylist entry)
All are 422-returned from the password-setting endpoints.
Timing
Section titled “Timing”The wrong-username branch of login verifies against a dummy hash generated at brain boot. Takes the same wall time as a real verify, so timing doesn’t disclose account existence.
Why 3-of-4 (not a fixed regex)
Section titled “Why 3-of-4 (not a fixed regex)”An audit flagged the old “letter + digit” rule: Summer24 and qwertyui1 both passed, both are in the top 1,000. 3-of-4 classes + denylist knocks out the long tail of dictionary-plus-one-digit passwords.
Password strength meters
Section titled “Password strength meters”The dashboard’s password input shows a strength indicator (zxcvbn-like) as guidance - but the server is authoritative. The meter can show “strong” for a password the server rejects because of denylist match, and that’s fine.