Pwned passwords
Check whether a password has appeared in a breach without ever sending it. The client hashes locally and only transmits a five-character prefix using k-anonymity. No API key required.
IPwnedPasswordsClientSHA-1 and NTLM range queries that keep the plaintext password on your machine.
Inject IPwnedPasswordsClient. Password checks need no API key, so this surface works with an empty HibpOptions as long as a User-Agent is set.
using HaveIBeenPwned.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddPwnedServices(options =>
{
options.UserAgent = "MyApp/1.0";
});
var app = builder.Build();GetPwnedPasswordAsyncNone
Hashes the password with SHA-1, sends the first five hash characters, and evaluates the returned range locally. IsPwned is false when no suffix matches.
GETapi.pwnedpasswords.com/range/{hashPrefix}Request detailsHide details
GET /range/{hashPrefix} HTTP/1.1Host: api.pwnedpasswords.comAccept: text/plainThis public endpoint is called without an API key or authorization header.
The SDK parses the suffix/count rows in memory and returns only the local match.
Task<PwnedPassword> GetPwnedPasswordAsync(
string plainTextPassword,
bool addPadding = false,
CancellationToken ct = default)| Name | Type | Description |
|---|---|---|
plainTextPasswordrequired | string | The candidate password. Hashed locally; never transmitted. |
addPadding | bool | Pad the response to 800-1000 records for extra privacy on the wire. Defaults to false. |
ct | CancellationToken | Signals cancellation. |
Task<PwnedPassword> | Always a value. IsPwned and PwnedCount describe the result; PwnedCount is 0 on no match. |
public sealed class SignupValidator(IPwnedPasswordsClient passwords)
{
public async Task<bool> IsAcceptableAsync(string candidate)
{
PwnedPassword result =
await passwords.GetPwnedPasswordAsync(candidate, addPadding: true);
return result.IsPwned is not true;
}
}Calling the snippet above yields the following payload:
{
"PlainTextPassword": "P@ssw0rd!",
"IsPwned": true,
"PwnedCount": 73586,
"HashedPassword": "21BD1...A0F9E"
}| Type | Thrown when |
|---|---|
ArgumentNullException | plainTextPassword is null or empty. |
PwnedApiException | Any non-success status while fetching the hash range. |
| Code | Meaning |
|---|---|
| 200 | Range returned. A non-matching suffix simply yields IsPwned = false. |
| 429 | Rate limit exceeded. The Retry-After header is surfaced on PwnedApiException.RetryAfter. |
| 503 | Service temporarily unavailable. Retried by the resilience pipeline when configured. |
Never log or persist the plaintext password or the full hash.
Enable addPadding to obscure the size of the returned range from any on-path observer.
GetPwnedPasswordWithNtlmAsyncNone
Identical flow to GetPwnedPasswordAsync but hashes with NTLM. Use it when validating against Active Directory style hashes.
GETapi.pwnedpasswords.com/range/{hashPrefix}?mode=ntlmRequest detailsHide details
GET /range/{hashPrefix}?mode=ntlm HTTP/1.1Host: api.pwnedpasswords.comAccept: text/plainThis public endpoint is called without an API key or authorization header.
The SDK parses the suffix/count rows in memory and returns only the local match.
Task<PwnedPassword> GetPwnedPasswordWithNtlmAsync(
string plainTextPassword,
bool addPadding = false,
CancellationToken ct = default)| Name | Type | Description |
|---|---|---|
plainTextPasswordrequired | string | The candidate password. Hashed with NTLM locally. |
addPadding | bool | Pad the response for extra privacy. Defaults to false. |
ct | CancellationToken | Signals cancellation. |
Task<PwnedPassword> | The evaluation result using NTLM hashing. |
PwnedPassword result =
await passwords.GetPwnedPasswordWithNtlmAsync("Winter2024");
Console.WriteLine(result.IsPwned is true
? $"Seen {result.PwnedCount:N0} times."
: "Not found.");Running the snippet above prints:
Seen 4,112 times.| Type | Thrown when |
|---|---|
ArgumentNullException | plainTextPassword is null or empty. |
PwnedApiException | Any non-success status while fetching the hash range. |
| Code | Meaning |
|---|---|
| 200 | Range returned and evaluated locally. |
| 429 | Rate limit exceeded. The Retry-After header is surfaced on PwnedApiException.RetryAfter. |
| 503 | Service temporarily unavailable. Retried by the resilience pipeline when configured. |
IsPasswordPwnedAsync (extension)None
A convenience extension on IPwnedPasswordsClient that returns a simple tuple when you only care about the yes/no answer and the count.
GETapi.pwnedpasswords.com/range/{hashPrefix}Request detailsHide details
GET /range/{hashPrefix} HTTP/1.1Host: api.pwnedpasswords.comAccept: text/plainThis public endpoint is called without an API key or authorization header.
The SDK parses the suffix/count rows in memory and returns only the local match.
Task<(bool? IsPwned, long? Count)> IsPasswordPwnedAsync(
this IPwnedPasswordsClient client,
string password,
CancellationToken ct = default)| Name | Type | Description |
|---|---|---|
passwordrequired | string | The candidate password. |
ct | CancellationToken | Signals cancellation. |
Task<(bool? IsPwned, long? Count)> | A tuple of the pwned flag and the occurrence count. |
var (isPwned, count) = await passwords.IsPasswordPwnedAsync("hunter2");
Console.WriteLine($"Pwned: {isPwned}, count: {count}.");Running the snippet above prints:
Pwned: True, count: 25891.| Type | Thrown when |
|---|---|
ArgumentNullException | password is null or empty. |
PwnedApiException | Any non-success status while fetching the hash range. |
| Code | Meaning |
|---|---|
| 200 | Range returned and evaluated locally. |
| 429 | Rate limit exceeded. The Retry-After header is surfaced on PwnedApiException.RetryAfter. |
| 503 | Service temporarily unavailable. Retried by the resilience pipeline when configured. |
Never log or export plaintext passwords. K-anonymity range results that do not match the requested value must not be retained.