Strength
NIST-style password policy with @virastack/password/strength: length, common passwords, repeats and personal info.
definePolicy()
Define the policy once in a shared file without "use client". The form and the server import the same object.
// lib/password-policy.ts
// Shared by the form and the server, so no "use client".
import { definePolicy } from "@virastack/password/strength";
export const policy = definePolicy({ preset: "nist", blocklist: ["virastack"] });Presets
| Preset | Length | Composition |
|---|---|---|
nist (default) | 15–64 | None. Per NIST SP 800-63B-4: length and a blocklist, no composition rules |
legacy | 8–64 | Lowercase, uppercase, digit and symbol |
Every preset value can be changed:
definePolicy({ preset: "nist", minLength: 8 }) // with a second factor (MFA)
definePolicy({ preset: "legacy", require: { symbol: false } })
definePolicy({ blocklist: ["acme", "acme pay"] }) // your product, contained anywhere
definePolicy({ minTokenLength: 4 }) // fewer false positives for short names
definePolicy({ blockCommon: false, blockRepetitive: false })Conflicting rules (for example minLength above maxLength) throw when the policy is defined.
Result
import { evaluate } from "@virastack/password/strength"
import { policy } from "@/lib/password-policy"
evaluate("correct horse battery staple", policy)
// {
// score: 0, // 0–1 fail the policy, 2–4 pass
// level: "weak", // "weak" | "fair" | "good" | "strong"
// length: 28, // code points after NFKC
// valid: false,
// checks: { minLength: true, maxLength: true, notCommon: false, ... },
// issues: ["password.common"],
// }| Key | When |
|---|---|
password.tooShort / tooLong | Outside minLength–maxLength |
password.noLowercase / noUppercase / noDigit / noSymbol | A required class is missing (legacy) |
password.common | A common password, also with digits, symbols or leetspeak around it |
password.blocked | Contains a word from the policy blocklist |
password.repetitive | Repeats or sequences: aaaa…, abcabc…, 12345…, qwerty… |
password.containsPersonalInfo | Contains a forbidden value |
The score is a heuristic from length and character variety, not a crack-time estimate. For that, see the comparison.
Why is a complex 14-character password weak? The score follows the policy, not a universal rule. The default nist preset asks for 15 characters, so anything shorter fails it and scores 0–1, however mixed it is. From 15 characters the score rises with length and variety. Need shorter passwords? Use minLength: 8 (NIST allows it with a second factor) or the legacy preset.
In the hook
Pass policy and read strength, issues and strengthProps. The hook doesn't import the engine; it comes with the policy, so a toggle-only page doesn't download it.
15 characters or more
forbidden
The package doesn't know your user; pass the values. Matching ignores case, accents and simple leetspeak.
evaluate(password, policy, { forbidden: [email, name, username] })
// "hello@virastack.com" → "hello" (domain ignored)
// "Vira Stack" → "vira stack", "vira", "stack"Parts shorter than minTokenLength (default 3) are ignored. Short parts cause false positives: with dev@virastack.com, the part dev rejects devoted-garden-path. Raise it to 4 if that matters to you.
Try vira-garden-path
Unicode
- The password is normalized with NFKC, so the same text typed on different keyboards gives the same result.
- Length counts code points: an emoji or a Turkish letter counts once.
- Zero-width characters don't count towards the length.
- Nothing is cut at
maxLength; a longer password getspassword.tooLong.
Paste
Don't block pasting into password fields. Password managers depend on it, and NIST recommends allowing it. The hook never blocks paste.
