Files
HiveGrid-Wireframes/hivegrid-cloud/docs/auth/password-policy.md
T

5.0 KiB
Raw Blame History

Password Policy

Server-side password validation that every password-write endpoint goes through. Configurable via env without a code deploy. Implementation lives in apps/web/app/services/auth/password-policy.server.ts.

Defaults (OWASP 2024 aligned)

Env var Default Effect
AUTH_PASSWORD_MIN_LENGTH 12 Minimum character count.
AUTH_PASSWORD_REQUIRE_UPPER true Requires at least one A–Z character.
AUTH_PASSWORD_REQUIRE_LOWER true Requires at least one a–z character.
AUTH_PASSWORD_REQUIRE_DIGIT true Requires at least one 0–9 character.
AUTH_PASSWORD_REQUIRE_SPECIAL false Requires at least one non-alphanumeric character. Off by default — adds friction without much entropy beyond the length floor.
AUTH_PASSWORD_HIBP_CHECK true Consults api.pwnedpasswords.com via SHA-1 k-anonymity. The plaintext password never leaves the server; only the first 5 characters of its SHA-1 are sent.
AUTH_PASSWORD_HIBP_MAX_OCCURRENCES 0 Accept passwords seen at most this many times in HIBP. 0 = reject any leak.

Also enforced regardless of env:

  • An absolute max length of 1024 characters, so the validate-then-hash chain can't be amplified into a DoS via 100 MB password strings.
  • If the caller supplies ctx.email, the password must not contain the email's local-part (case-insensitive), provided the local-part is ≥3 characters. Short local-parts (x@example.com) are skipped to avoid false positives.

Enforcement points

Every endpoint that writes a password runs the policy:

Route Caller
apps/web/app/utils/services/auth.service.server.ts (AuthService.validateRegistration) validatePassword before users.db.server.ts hashes/stores the password
apps/web/app/routes/reset.tsx UserValidationUtils.validatePasswords
apps/web/app/routes/invitation.$id.tsx UserValidationUtils.validatePasswords
apps/web/app/routes/admin/settings/profile.tsx (change password) validatePassword directly
apps/web/app/routes/app.$tenant/settings/profile.tsx (change password) validatePassword directly
apps/web/app/routes/admin/accounts/users.tsx (admin reset) validatePassword directly
apps/web/app/routes/app.$tenant/__portals/portals.$portal/users.tsx (portal user creation / reset) validatePassword directly

Login (apps/web/app/modules/users/services/AuthService.ts) deliberately does NOT run the strength policy. Enforcing 12-char + class requirements there would lock out every user with a pre-policy password instead of prompting them to rotate. The plain "is the field a non-empty 4+ char string" presence-check in UserValidationUtils.validatePassword is what guards the login form. The dashboard-banner soft-check from the detail-MD is a follow-up.

HIBP fail-open

If the HIBP API times out (>3 s), returns a 5xx, or refuses the connection, the validation result carries hibpBypassed: true and the password is accepted (the other policy rules still apply). Caller code can log the bypass so operators have a signal when api.pwnedpasswords.com is having a bad day. We chose fail-open over fail-closed because the alternative ("can't register new users while HIBP is down") is worse for availability than the alternative ("a newly-leaked password might slip through during an HIBP outage").

To force fail-closed during a known-bad incident: set AUTH_PASSWORD_HIBP_CHECK=false to skip HIBP entirely until the API recovers. (Yes, that's the same as the bypass — but the env switch makes it explicit and avoids the per-request 3 s timeout cost.)

Error model

Each failure produces a bounded i18n key plus optional non-secret args:

{
  valid: false,
  errors: [
    { code: "auth.password.errors.tooShort", args: { minLength: 12 } },
    { code: "auth.password.errors.missingDigit" }
  ]
}

Codes:

  • auth.password.errors.empty — input was empty / wrong type
  • auth.password.errors.tooShort — below AUTH_PASSWORD_MIN_LENGTH
  • auth.password.errors.tooLong — above the absolute max (1024)
  • auth.password.errors.missingUpper
  • auth.password.errors.missingLower
  • auth.password.errors.missingDigit
  • auth.password.errors.missingSpecial
  • auth.password.errors.containsEmail
  • auth.password.errors.pwned

Routes are expected to surface the first error to the user; the i18n catalog should map every code above.

Operator playbook

  • Relax the policy (e.g. temporary lower bound during a migration): set AUTH_PASSWORD_MIN_LENGTH=10 + restart. The change takes effect on next request; no code deploy.
  • Tighten the policy: set AUTH_PASSWORD_REQUIRE_SPECIAL=true and bump AUTH_PASSWORD_MIN_LENGTH=16. Existing users keep their old passwords until they next rotate; the new policy applies only to writes.
  • HIBP-Outage handling: monitor hibpBypassed in the audit log; if the rate sustains, set AUTH_PASSWORD_HIBP_CHECK=false to drop the 3 s per-write timeout. Re-enable when api.pwnedpasswords.com recovers.