Skip to main content

Modern password policy 2026: stop Password@1

·8 min read
securityauthenticationpasswordstypescript

If you remember one thing: Password security is mostly won by (1) length + uniqueness, and (2) server-side defenses. Complexity rules and forced rotation mostly make people choose more predictable passwords.

This post is for SWE building or fixing a login system who already knows basic auth flows and wants a copyable policy + the reasoning behind each rule.

When it applies: any consumer or SaaS app that still supports passwords (even if you also offer passkeys/MFA). When it doesn't: environments where you can go fully passwordless, or where policy is dictated by a fixed compliance baseline with no flexibility.


TL;DR (the policy you can ship)

  • Min length: 15 when password is the only factor; 8+ if the password is only used alongside MFA.
  • Max length: allow ≥64 chars.
  • No composition rules: don't force "upper+lower+number+symbol".
  • No periodic rotation: only reset on compromise evidence.
  • Blocklist: reject common/expected/compromised passwords (breach corpuses, dictionary words, app name, username variants).
  • UX that enables strength: allow password managers/autofill, permit paste, offer "show password".
  • No password hints / no KBA ("first pet" questions).
  • Rate-limit/throttle failed logins.

Industry anchor for these specifics: NIST SP 800-63B.


The problem: legacy password rules feel "secure" but fail in practice

A very common inherited policy looks like this:

  • minimum 8 characters
  • must include symbol + uppercase
  • expires every 90 days
  • security questions for recovery

Symptoms you'll see:

  • huge password-reset volume
  • users reuse a "base password" everywhere and mutate it (Password@1Password@2)
  • credential-stuffing incidents still happen
  • support and security teams fight UX, but nobody wins

The root issue: those rules optimize for a neat checkbox, not for attacker economics + human behavior.


Constraints (what actually matters when you set a password policy)

A policy is only "good" if it fits these constraints:

  1. Online attacks are the default (credential stuffing + guessing).
  2. Offline attacks happen after breaches (attacker steals hashes, cracks at scale).
  3. Humans are predictable under friction (complexity + rotation produces patterns).
  4. Your system needs stable UX (password managers, paste, fewer typos).
  5. You need defenses that scale (rate limiting, hashing cost, monitoring).

The modern guidelines are basically: reduce predictability, increase search space, and slow attackers down.


A single running example: fixing the signup + login policy for a SaaS

Let's say you're shipping "v1 password auth" for a small SaaS:

  • You support passwords today.
  • You'll add passkeys and stronger MFA later.
  • You want the simplest policy that's defensible and doesn't annoy users into unsafe behavior.

We'll apply the rules below using a consistent template:

Decision → Because → Instead of → Watch out for


1) Minimum length: 15 (password-only) / 8 (with MFA)

Decision:

  • Require ≥15 chars if a password can unlock the account by itself.
  • Allow ≥8 chars only when the password is used as part of MFA.

Because: Length is the strongest "user-chosen" lever you control. Longer passwords (especially passphrases) explode the guess space, and it's much harder for attackers to crack them after a breach.

Instead of: "8 characters + symbol" (users satisfy it with predictable patterns).

Watch out for: If you also offer MFA, don't let "MFA optional" quietly become "8-char passwords everywhere." Treat password-only logins as higher risk.


2) Max length: at least 64 characters

Decision: Accept passwords up to ≥64 chars (more is fine, but set a sane upper bound).

Because:

  • Password managers generate long random strings.
  • Passphrases are long by nature.
  • Rejecting long passwords pushes users toward shorter, reused passwords.

Instead of: Arbitrary caps like 16/20/32 that silently punish good behavior.

Watch out for: Very large max lengths can be used for DoS via expensive hashing. Pick an upper bound you can afford (e.g., 128 or 256) and enforce it consistently.


3) No composition rules

Decision: Don't require mixed character classes.

Because: When you force complexity, users converge on patterns attackers already guess early (capital first letter, number at end, ! at end). You get more predictability, not less.

Instead of: Require length + blocklist + password manager support.

Watch out for: Some security teams equate "no complexity" with "weak." Bring data: the standard explicitly says don't impose composition rules.


4) No periodic forced rotation

Decision: Do not expire passwords on a timer. Force changes only with evidence of compromise.

Because: Expiry creates "incrementing passwords" (…1…2) and more reuse/writing-down. It also trains users to treat passwords as disposable, which reduces care.

Instead of:

  • detect compromise signals (breach match, suspicious login, leaked credential report)
  • reset then, immediately

Watch out for: You still need a way to revoke sessions and handle account takeover fast. Rotation is not a substitute for incident response.


5) Blocklist checks (common/expected/compromised)

Decision: Reject passwords that are likely to be guessed early: breached lists, dictionary words, and context terms like your app name or username derivatives.

Because: Most real-world password compromise is credential stuffing + known-common guesses. Blocklists eliminate the "lowest hanging fruit" at signup and password change time.

Instead of: Relying only on complexity rules.

Watch out for:

  • Don't leak too much detail ("Your password is in breach X"). A generic rejection reason is fine.
  • Don't make the blocklist absurdly huge: NIST notes oversized blocklists add little because online attempts are rate-limited anyway.

6) UX that enables strength: password managers, paste, show password

Decision:

  • Allow password managers + autofill.
  • Allow paste.
  • Offer "show password" during entry.

Because: Password managers are one of the few interventions that reliably increase password strength. Blocking paste/autofill actively prevents the best user behavior.

Instead of: Treating "paste disabled" as a security feature (it's mostly friction).

Watch out for:

  • Set proper HTML attributes (autocomplete="current-password" / new-password") so managers work.
  • "Show password" should be optional and mindful of shoulder-surfing.

7) No password hints / no knowledge-based questions

Decision: No hints. No "first pet" questions.

Because: Hints leak information. KBA answers are often guessable, researchable, or already leaked.

Instead of: Use stronger recovery:

  • email magic link + device/session confirmation
  • recovery codes
  • authenticator-based recovery

Watch out for: Recovery is usually your weakest link. Removing KBA means you must invest in a real recovery flow.


8) Rate limit + throttle failed attempts

Decision: Implement effective rate limiting per account (and usually per IP / device risk bucket).

Because: Even a perfect password policy fails if attackers can try unlimited guesses. Rate limiting changes attacker economics and buys time for detection.

Instead of: Hard lockouts that create easy denial-of-service.

Watch out for:

  • Too aggressive limits = attackers can lock out real users. Prefer progressive delays and risk-based throttling.

Minimal implementation sketch (copyable)

Server-side validation (policy only)

export function validateNewPassword(
  pw: string,
  ctx: { username?: string; serviceName: string; mfaRequired: boolean }
) {
  const min = ctx.mfaRequired ? 8 : 15;
  if (pw.length < min) return { ok: false, reason: `too_short_min_${min}` };

  if (pw.length > 128) return { ok: false, reason: "too_long_max_128" }; // choose a sane cap

  // No composition rules.

  // Blocklist check (pseudo):
  // - breached/common list
  // - dictionary words
  // - service name, username, derivatives
  if (isBlocklisted(pw, ctx)) return { ok: false, reason: "too_common_or_compromised" };

  return { ok: true as const };
}

Storage (the part people forget)

Use a slow, memory-hard hash (e.g., Argon2id). OWASP recommends Argon2id with a baseline config and explains the offline-cracking threat model. (cheatsheetseries.owasp.org)

(Exact code depends on your language/library; the important part is salt + strong parameters + ability to increase cost over time.)

Login throttling (progressive delay)

// pseudo
const attempts = await failedAttempts.get(accountId, (window = 15 * 60));
if (attempts > 5) await sleep(backoffMs(attempts)); // progressive delay
if (attempts > 30) requireCaptchaOrStepUp();

Tradeoffs (what you give up / what you must add)

  • Longer minimums can frustrate some users → mitigate with passphrase examples and password manager support.
  • Blocklists require maintenance and careful privacy handling (don't log raw passwords; hash-check locally if you can).
  • No forced rotation means you must take compromise signals seriously (monitoring + incident flow).
  • Rate limiting can be weaponized for lockout DoS → prefer delays/risk scoring over hard locks.
  • Unicode acceptance improves UX but adds normalization concerns (standards explicitly call this out).

Further reading (the "why" sources)