Enforce a strong password policy
One lookahead per rule: a lowercase, an uppercase, a digit and a symbol, minimum 8 characters — and why lookaheads are the right tool for the job.
Last updated
Matches
- Tr0ub4dor&3
- correct-Horse-7!
Doesn’t match
- alllowercase1!
- NOLOWER1!
- Short1!
Why it’s written this way
Each (?=…) is a lookahead: it scans ahead for one required character class without consuming anything, then hands control back to the start. Four independent rules become four independent lookaheads — add or drop a rule without rewriting the rest.
Only the final .{8,} actually consumes characters, which is where the length rule lives. When validating a whole input field, anchor it as ^…$ so a strong substring inside a weak password cannot pass.
Edge cases to know
- →The symbol rule [^\w\s] means "not alphanumeric, not whitespace" — spaces inside a passphrase pass the length rule but do not count as the symbol.
- →Composition rules are a floor, not a strength meter: Password1! passes. Pair it with a length slider or a breach check for real strength.
- →NIST guidance now favours length over forced composition — this pattern is for when policy demands composition anyway.
Part by part
- (?=.*[a-z])
- A lookahead: from the start, somewhere ahead there is a lowercase letter. It checks without consuming, which is what lets four separate requirements apply to the same string.
- (?=.*[A-Z])
- The same test for an uppercase letter. Order between the lookaheads is irrelevant — each scans the whole string independently.
- (?=.*\d)
- Requires at least one digit anywhere in the password.
- (?=.*[^\w\s])
- Requires a symbol, defined as anything that is neither a word character nor whitespace. Written as an exclusion so it covers punctuation the author never thought to list.
- .{8,}
- The only part that consumes anything: eight or more characters. Every lookahead above has already passed by the time this runs.
Common mistakes
[a-z][A-Z]\d[^\w\s].{4,}Listing the classes in sequence demands them in that exact order, so it accepts aA1!xxxx and rejects almost every password a person would actually choose. Lookaheads exist precisely to avoid ordering the requirements.
(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}Dropping the symbol lookahead while keeping the messaging that promises a symbol requirement is worse than either choice alone: the form tells users a rule that it does not enforce.
(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,20}An upper length bound breaks password managers and passphrases, and a maximum length is a strong hint that the password is being stored somewhere it should not be. Hashes are fixed-length regardless of input.
Questions
Does it allow spaces in a password?
Yes. The dot matches a space, so a passphrase counts toward the length requirement — but whitespace will not satisfy the symbol rule, because that class excludes it explicitly. A four-word passphrase still needs one punctuation mark.
Should I require character classes at all?
Current NIST guidance says no: composition rules push people toward predictable substitutions like P@ssw0rd1 while length is what actually resists cracking. If you can choose, a long minimum with a breached-password check beats this pattern outright.
Does the dot match every character?
Not newlines, unless the s flag is set. It rarely matters for a password field, but it does mean a multi-line paste will not match, which can look like an arbitrary rejection to whoever pasted it.
Is this enough on its own?
No. It cannot tell that a password appears in a breach list, matches the user's email, or is the same one they used last time. Those checks catch far more weak passwords than any composition rule does.