How to Enforce a Password Policy
Enforce a password policy with character classes, length, and a breach check, so weak passwords are rejected before hashing with actionable feedback.
On this page
Introduction
The hardest part of reject weak passwords is not writing the code — it is deciding what the code is allowed to reject. A password policy setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in password policy, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.
A password policy guard for reject weak passwords should be proportional to the risk. A ten-field form does not need a hundred-line schema; it needs the five checks that catch the five mistakes the users actually make. Ship the five, measure the rejections, and add the sixth only when the traffic proves it exists. Speculative generality is the bug you write on purpose, and password policy makes it easy to write, which is why discipline matters.
The sections below walk through the concrete decisions: where to put the password policy check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.
Define the policy rules
Define the policy rules is the first decision because it determines what the rest of the password policy pipeline can assume. A wrong choice here turns into reject weak passwords under load, and the invariant you thought password policy guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Define the policy rules is the one most teams skip: drive password policy to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
const Policy = z.string().min(12).refine(s => /[a-z]/.test(s), 'lowercase')
.refine(s => /[A-Z]/.test(s), 'uppercase')
.refine(s => /\d/.test(s), 'digit')
.refine(s => !COMMON.has(s), 'too common');
The typescript above is the shape to ship for reject weak passwords. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.
Check each class
The mechanism behind Check each class is straightforward in password policy, but the consequences ripple. A wrong choice here turns into reject weak passwords under load, and the invariant you thought password policy guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Check each class is the one most teams skip: drive password policy to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Reject common passwords
Reject common passwords is where most password policy implementations leak performance. A wrong choice here turns into reject weak passwords under load, and the invariant you thought password policy guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Reject common passwords is the one most teams skip: drive password policy to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
test('rejects common password', () => {
expect(Policy.safeParse('password12345').success).toBe(false);
});
Pin the behavior with this test. A password policy setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.
Score and give feedback
The hard part of Score and give feedback is not the happy path — it is what happens when password policy fails halfway. A wrong choice here turns into reject weak passwords under load, and the invariant you thought password policy guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Score and give feedback is the one most teams skip: drive password policy to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Hash after pass
Hash after pass should be observable without a debugger. A wrong choice here turns into reject weak passwords under load, and the invariant you thought password policy guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Hash after pass is the one most teams skip: drive password policy to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Test policy edge cases
Test policy edge cases is a trade, not a winner. A wrong choice here turns into reject weak passwords under load, and the invariant you thought password policy guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Test policy edge cases is the one most teams skip: drive password policy to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Common pitfalls
The common pitfalls in reject weak passwords all reduce to one mistake: treating password policy as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.
- Skipping the password policy boundary so reject weak passwords enters unvalidated.
- Assuming password policy is fast without measuring the tail latency under load.
- Letting reject weak passwords fail silently instead of failing loudly with a metric.
- Testing only the happy path so the password policy failure surfaces in production.
- Sharing mutable state across requests so one bad input poisons the next.
Conclusion
Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: password policy fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.
Self-check: write a test that drives password policy to the failing condition and assert the system degrades gracefully. Then reject weak passwords is a property you maintain, not a feature you hope for.
Sources: NIST 800-63B, Have I Been Pwned
FAQ
- Should I check breach databases?
- Yes, via k-anonymity API. Rejecting known breached passwords is cheap and high-value.
- Length or complexity?
- Length. NIST 800-63B prefers length over arbitrary complexity rules.
Report a problem with this articleOpens a pre-filled issue on GitHub.