feat(haveibeenpwned): add enable option (#8728)

Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
armful
2026-03-22 23:30:56 +01:00
committed by GitHub
parent b647ef3488
commit df9abae0bc
4 changed files with 70 additions and 6 deletions

View File

@@ -32,3 +32,4 @@ Timestampz
Vercel
rgba
idtoken
HIBP

View File

@@ -26,19 +26,46 @@ When a user attempts to create an account or update their password with a compro
```json
{
"code": "PASSWORD_COMPROMISED",
"message": "Password is compromised"
"message": "The password you entered has been compromised. Please choose a different password."
}
```
## Config
## Options
You can customize the error message:
### `enabled`
```ts
haveIBeenPwned({
customPasswordCompromisedMessage: "Please choose a more secure password."
Enable or disable password checks against the HIBP database. Useful for skipping checks in development or testing without removing the plugin. Defaults to `true`.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { haveIBeenPwned } from "better-auth/plugins"
const auth = betterAuth({
plugins: [
haveIBeenPwned({
enabled: process.env.NODE_ENV === 'production' // [!code highlight]
})
]
})
```
### `customPasswordCompromisedMessage`
Customize the error message shown when a compromised password is detected.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { haveIBeenPwned } from "better-auth/plugins"
const auth = betterAuth({
plugins: [
haveIBeenPwned({
customPasswordCompromisedMessage: "Please choose a more secure password." // [!code highlight]
})
]
})
```
## Security Notes
- Only the first 5 characters of the password hash are sent to the API

View File

@@ -12,6 +12,7 @@ describe("have-i-been-pwned", async () => {
},
);
const ctx = await auth.$context;
it("should prevent account creation with compromised password", async () => {
const uniqueEmail = `test-${Date.now()}@example.com`;
const compromisedPassword = "123456789";
@@ -63,4 +64,28 @@ describe("have-i-been-pwned", async () => {
expect(result.error).toBeDefined();
expect(result.error?.status).toBe(400);
});
describe("when enabled is false", async () => {
const { client: disabledClient } = await getTestInstance(
{
plugins: [haveIBeenPwned({ enabled: false })],
},
{
disableTestUser: true,
},
);
it("should allow account creation with compromised password", async () => {
const uniqueEmail = `test-${Date.now()}@example.com`;
const result = await disabledClient.signUp.email({
email: uniqueEmail,
password: "123456789",
name: "Test User",
});
expect(result.error).toBeNull();
expect(result.data?.user).toBeDefined();
});
});
});

View File

@@ -66,6 +66,9 @@ async function checkPasswordCompromise(
}
export interface HaveIBeenPwnedOptions {
/**
* Custom error message shown when a compromised password is detected.
*/
customPasswordCompromisedMessage?: string | undefined;
/**
* Paths to check for password
@@ -73,6 +76,12 @@ export interface HaveIBeenPwnedOptions {
* @default ["/sign-up/email", "/change-password", "/reset-password"]
*/
paths?: string[];
/**
* Enable or disable password checks against the HIBP database.
*
* @default true
*/
enabled?: boolean | undefined;
}
export const haveIBeenPwned = (options?: HaveIBeenPwnedOptions | undefined) => {
@@ -91,6 +100,8 @@ export const haveIBeenPwned = (options?: HaveIBeenPwnedOptions | undefined) => {
password: {
...ctx.password,
async hash(password) {
if (options?.enabled === false) return originalHash(password);
const c = await getCurrentAuthContext();
if (!c.path || !paths.includes(c.path)) {
return originalHash(password);