Skip to content

Feat/password policy - #9

Merged
raymondproguy merged 10 commits into
mainfrom
feat/password-policy
Sep 3, 2026
Merged

Feat/password policy#9
raymondproguy merged 10 commits into
mainfrom
feat/password-policy

Conversation

@raymondproguy

Copy link
Copy Markdown
Contributor

No description provided.

Plain configuration data with a validation method, not a pluggable
interface like the rest of this package — there's nothing to swap out
here, just numbers and booleans.

Validate reports every violated rule at once (as stable, machine-
readable string codes — the engine doesn't own UI copy/localization
anywhere else, so it doesn't start here), not just the first one hit.

DefaultPasswordPolicy (8 min, 72 max, no character-class requirements)
follows NIST 800-63B guidance that length matters far more than
forced complexity rules. MaxLength defaults to 72 specifically because
that's bcrypt's own real limit — without this check, a longer password
hits a raw bcrypt library error at hash time instead of a clean
policy violation.
Covers: multiple violations reported together, a good password
passing cleanly, MaxLength 0 meaning no upper bound, symbol detection,
and the default policy's actual bounds (8 min, 72 max).
Both take a policy security.PasswordPolicy param now, checked BEFORE
the breach check (cheapest/local check first — no reason to make an
external call for a password that's already rejected). A violation
returns *ErrPasswordPolicyViolation{Violations []string}, every
broken rule at once.

New Config.PasswordPolicy field. Unlike every other optional feature
in this engine, this has NO 'unconfigured means off' state — leaving
it as the zero value applies security.DefaultPasswordPolicy instead
(detected via MaxLength == 0, which no real policy would intentionally
set). Public facade signatures for SignUp/ChangePassword are
unchanged.

Updates existing signup_test.go/account_test.go call sites for the
new param.
Covers: a policy violation rejecting SignUp/ChangePassword with every
broken rule reported together, a satisfying password succeeding, the
breach checker never being called when policy already rejected the
password, and current-password verification running before the new
password's policy check on ChangePassword.
That case needed security.PasswordPolicy, which didn't exist yet on
the branch this smoke test was first written on — deferred there,
added back now that password policy exists.
Runnable end-to-end check with no database dependency: go run
./cmd/smoketest/password-policy. Walks the default policy applying
automatically when Config.PasswordPolicy is left unset, a short
password rejected, a 73-byte password rejected (bcrypt's real limit
is 72), a custom stricter policy reporting multiple violations
together, and a password satisfying that custom policy succeeding.
applyDefaults detected "unset" via PasswordPolicy.MaxLength == 0
alone. A policy that set MinLength/character requirements but left
MaxLength untouched — exactly what a caller would naturally write —
looked unset and got silently overwritten by DefaultPasswordPolicy,
which has none of those requirements. Comparing the whole struct
against its zero value instead means only a truly untouched
Config.PasswordPolicy gets defaulted; setting even one field counts
as a real custom policy and is used as-is.
@raymondproguy
raymondproguy merged commit 1f19988 into main Sep 3, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant