From df9abae0bcb3cbb0fca17ddb43562a158ffb228a Mon Sep 17 00:00:00 2001 From: armful Date: Sun, 22 Mar 2026 23:30:56 +0100 Subject: [PATCH] feat(haveibeenpwned): add enable option (#8728) Co-authored-by: Taesu --- .cspell/auth-terms.txt | 1 + .../docs/plugins/have-i-been-pwned.mdx | 39 ++++++++++++++++--- .../haveibeenpwned/haveibeenpwned.test.ts | 25 ++++++++++++ .../src/plugins/haveibeenpwned/index.ts | 11 ++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/.cspell/auth-terms.txt b/.cspell/auth-terms.txt index b090e3954f..7ff341d88b 100644 --- a/.cspell/auth-terms.txt +++ b/.cspell/auth-terms.txt @@ -32,3 +32,4 @@ Timestampz Vercel rgba idtoken +HIBP diff --git a/docs/content/docs/plugins/have-i-been-pwned.mdx b/docs/content/docs/plugins/have-i-been-pwned.mdx index 706d6c246a..0abaff5848 100644 --- a/docs/content/docs/plugins/have-i-been-pwned.mdx +++ b/docs/content/docs/plugins/have-i-been-pwned.mdx @@ -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 diff --git a/packages/better-auth/src/plugins/haveibeenpwned/haveibeenpwned.test.ts b/packages/better-auth/src/plugins/haveibeenpwned/haveibeenpwned.test.ts index d32bd48f6e..f16209c6f1 100644 --- a/packages/better-auth/src/plugins/haveibeenpwned/haveibeenpwned.test.ts +++ b/packages/better-auth/src/plugins/haveibeenpwned/haveibeenpwned.test.ts @@ -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(); + }); + }); }); diff --git a/packages/better-auth/src/plugins/haveibeenpwned/index.ts b/packages/better-auth/src/plugins/haveibeenpwned/index.ts index 5fefd984bd..2de07c0215 100644 --- a/packages/better-auth/src/plugins/haveibeenpwned/index.ts +++ b/packages/better-auth/src/plugins/haveibeenpwned/index.ts @@ -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);