Breached Password Detectionwith HaveIBeenPwned & k-Anonymity
Even a perfectly implemented authentication system can be undermined by one thing: users choosing passwords that have already been exposed in data breaches elsewhere. A user who sets their password to one leaked from a previous breach is vulnerable to credential stuffing attacks: automated attempts using known username/password pairs harvested from other breaches. The HaveIBeenPwned Pwned Passwords API gives you a way to catch this at registration and login, without ever sending the user's password to a third party.
What Is a Breached Password and Why Does It Matter?
A breached password is one that has appeared in a publicly known data breach: a database leak, a credential dump, or a security incident where user credentials were exposed. The HaveIBeenPwned project, maintained by Troy Hunt, aggregates these breaches and exposes the compromised passwords through an API. As of 2026, the database contains over a billion breached passwords.
The risk is specific: an attacker who obtains a breached credential list doesn't need to crack passwords: they already know them. They test those credentials against other services. If your users reuse passwords, and most do, a breach on another platform becomes an attack vector on yours. Checking for breached passwords at registration time blocks the most common credential stuffing targets before they can be used.
OWASP's Authentication Cheat Sheet explicitly recommends checking new passwords against breach databases as part of a robust credential policy: "Ensure that credentials are not found in known breached datasets."
How Does the HaveIBeenPwned API Work?
The Pwned Passwords API uses a Range search endpoint. You send the first 5 characters of the SHA-1 hash of the password. The API responds with a list of all SHA-1 hash suffixes in its database that share that prefix, along with how many times each appeared in known breaches.
Your client then reconstructs the full hash by prepending the 5-character prefix to each returned suffix and checks whether the user's full SHA-1 hash is in that list. If it is, the password has been breached. If it is not, the password is unknown to the database.
The API never receives the full hash: only the first 5 characters. A 5-character hex prefix matches thousands of different possible full hashes, so the API cannot determine which specific password you are checking. This is the k-anonymity property.
What Is k-Anonymity and Why Is It Critical?
k-anonymity is a privacy property. In this context, it means that any single API request is indistinguishable from thousands of other requests that share the same 5-character prefix. The HaveIBeenPwned server sees the prefix: but that prefix corresponds to a minimum of k different possible full hashes, so it cannot determine which specific password triggered the request.
| Approach | What Leaves Your Server | Privacy Risk |
|---|---|---|
| Send full password | The raw password | Critical: third party sees the password |
| Send full SHA-1 hash | The complete hash | High: hash can be reversed or matched |
| Send first 5 chars of SHA-1 (k-anonymity) | 5-character prefix only | Minimal: thousands of passwords share any prefix |
The k-anonymity approach means you can use the HaveIBeenPwned API in production without violating user trust or creating a privacy liability. The password never leaves your process: only an ambiguous prefix does.
Implementing the Check in Node.js
The implementation has three steps: hash the password with SHA-1, send the first 5 characters to the API, then check the response. Using Node.js built-in modules:
- Step 1: Hash:
const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase(); - Step 2: Split:
const prefix = hash.slice(0, 5); const suffix = hash.slice(5); - Step 3: Request:
GET https://api.pwnedpasswords.com/range/{prefix} - Step 4: Check: Parse the response lines (each is
SUFFIX:COUNT) and look for a line where the suffix matches yoursuffixvariable. - Step 5: Decide: If found, the password is breached. Return an error and ask the user to choose a different password.
If you use OwlAuth, this entire flow is handled automatically. The library makes the k-anonymous API call, parses the response, and returns a structured result: the raw password never touches the HaveIBeenPwned infrastructure, and the check adds under 100ms of latency in typical conditions.
Where in the Auth Flow Should You Check?
The check should happen at two points:
| Trigger Point | Action | User Experience |
|---|---|---|
| Registration: password creation | Block if breached, show friendly error | User selects a different password before account is created |
| Password change / reset | Block if breached, show friendly error | User cannot cycle to a known-bad password |
| Login (optional) | Warn and prompt password change | Existing users with breached passwords are notified over time |
Checking at login is optional and has UX trade-offs: blocking a login because the password is breached can be confusing if the user doesn't know why. A common pattern is to allow login but immediately prompt a password change with a clear explanation. The mandatory check points are registration and password reset.
What OWASP Says About Compromised Credentials
OWASP's Authentication Cheat Sheet includes compromised credential checking as a recommended control under password policy. The ASVS (Application Security Verification Standard) 5.0 includes it in Level 1 requirements: the baseline that all applications should meet:
- Verify that passwords are checked against known breached password lists
- Reject passwords that match common patterns, dictionary words, or context-specific terms (username, email, app name)
- Provide clear, non-generic error messages when a password is rejected so the user understands what to change
The breached password check is not an advanced security feature: it is a baseline requirement. The HaveIBeenPwned API makes it free and straightforward to implement. There is no good reason to skip it.
Rate Limits and Caching Considerations
- The HaveIBeenPwned Pwned Passwords API is free and has no official rate limit, but the API documentation recommends caching responses to the same prefix to reduce load.
- In practice, common password prefixes will be requested frequently. A short-lived in-memory or Redis cache keyed on the 5-character prefix improves performance without compromising privacy: you are caching the prefix response, not the user's password.
- The API also supports a
Add-Paddingheader that adds random padding to responses, further improving privacy against traffic analysis.