Repository: daveyplate/better-auth-ui
Pull request #76: [feat] Enhance password validation
Author: login: k9982874
Created: 2025-05-07T09:50:42Z | Merged: 2025-05-16T08:48:45Z

- Allow user set minimum length and maximum lenght for the password field

- Allow user set a regexp to validate the password format

<img width="530" alt="example" src="https://github.com/user-attachments/assets/2835b925-09c0-4d07-8962-a9393546dec5" />



Diff:
diff --git a/package.json b/package.json
index 028ce5fd8..ce23241a9 100644
--- a/package.json
+++ b/package.json
@@ -63,17 +63,8 @@
             }
         }
     },
-    "files": [
-        "src",
-        "dist"
-    ],
-    "keywords": [
-        "typescript",
-        "react",
-        "better-auth",
-        "shadcn",
-        "nextjs"
-    ],
+    "files": ["src", "dist"],
+    "keywords": ["typescript", "react", "better-auth", "shadcn", "nextjs"],
     "author": "daveycodez",
     "license": "MIT",
     "devDependencies": {
diff --git a/src/components/auth/auth-card.tsx b/src/components/auth/auth-card.tsx
index 0cab88510..3e9ccb6ee 100644
--- a/src/components/auth/auth-card.tsx
+++ b/src/components/auth/auth-card.tsx
@@ -221,88 +221,94 @@ export function AuthCard({
                     </div>
                 )}
 
-                {view !== "resetPassword" && (providers?.length || otherProviders?.length || passkey) && (
-                    <>
-                        {(credentials || magicLink || emailOTP) && (
-                            <div className="flex items-center gap-2">
-                                <Separator className={cn("!w-auto grow", classNames?.separator)} />
+                {view !== "resetPassword" &&
+                    (providers?.length || otherProviders?.length || passkey) && (
+                        <>
+                            {(credentials || magicLink || emailOTP) && (
+                                <div className="flex items-center gap-2">
+                                    <Separator
+                                        className={cn("!w-auto grow", classNames?.separator)}
+                                    />
 
-                                <span className="flex-shrink-0 text-muted-foreground text-sm">
-                                    {localization.orContinueWith}
-                                </span>
+                                    <span className="flex-shrink-0 text-muted-foreground text-sm">
+                                        {localization.orContinueWith}
+                                    </span>
 
-                                <Separator className={cn("!w-auto grow", classNames?.separator)} />
-                            </div>
-                        )}
+                                    <Separator
+                                        className={cn("!w-auto grow", classNames?.separator)}
+                                    />
+                                </div>
+                            )}
 
-                        <div className="grid gap-4">
-                            {(providers?.length || otherProviders?.length) && (
-                                <div
-                                    className={cn(
-                                        "flex w-full items-center justify-between gap-4",
-                                        socialLayout === "horizontal" && "flex-wrap",
-                                        socialLayout === "vertical" && "flex-col",
-                                        socialLayout === "grid" && "grid grid-cols-2"
-                                    )}
-                                >
-                                    {providers?.map((provider) => {
-                                        const socialProvider = socialProviders.find(
-                                            (socialProvider) => socialProvider.provider === provider
-                                        )
-                                        if (!socialProvider) return null
-
-                                        return (
+                            <div className="grid gap-4">
+                                {(providers?.length || otherProviders?.length) && (
+                                    <div
+                                        className={cn(
+                                            "flex w-full items-center justify-between gap-4",
+                                            socialLayout === "horizontal" && "flex-wrap",
+                                            socialLayout === "vertical" && "flex-col",
+                                            socialLayout === "grid" && "grid grid-cols-2"
+                                        )}
+                                    >
+                                        {providers?.map((provider) => {
+                                            const socialProvider = socialProviders.find(
+                                                (socialProvider) =>
+                                                    socialProvider.provider === provider
+                                            )
+                                            if (!socialProvider) return null
+
+                                            return (
+                                                <ProviderButton
+                                                    key={provider}
+                                                    classNames={classNames}
+                                                    callbackURL={callbackURL}
+                                                    isSubmitting={isSubmitting}
+                                                    localization={localization}
+                                                    provider={socialProvider}
+                                                    redirectTo={redirectTo}
+                                                    setIsSubmitting={setIsSubmitting}
+                                                    socialLayout={socialLayout}
+                                                />
+                                            )
+                                        })}
+
+                                        {otherProviders?.map((provider) => (
                                             <ProviderButton
-                                                key={provider}
+                                                key={provider.provider}
                                                 classNames={classNames}
                                                 callbackURL={callbackURL}
                                                 isSubmitting={isSubmitting}
                                                 localization={localization}
-                                                provider={socialProvider}
+                                                provider={provider}
                                                 redirectTo={redirectTo}
                                                 setIsSubmitting={setIsSubmitting}
                                                 socialLayout={socialLayout}
+                                                other
                                             />
-                                        )
-                                    })}
+                                        ))}
+                                    </div>
+                                )}
 
-                                    {otherProviders?.map((provider) => (
-                                        <ProviderButton
-                                            key={provider.provider}
+                                {passkey &&
+                                    [
+                                        "signIn",
+                                        "magicLink",
+                                        "emailOTP",
+                                        "recoverAccount",
+                                        "twoFactor",
+                                        "forgotPassword"
+                                    ].includes(view) && (
+                                        <PasskeyButton
                                             classNames={classNames}
-                                            callbackURL={callbackURL}
                                             isSubmitting={isSubmitting}
                                             localization={localization}
-                                            provider={provider}
                                             redirectTo={redirectTo}
                                             setIsSubmitting={setIsSubmitting}
-                                            socialLayout={socialLayout}
-                                            other
                                         />
-                                    ))}
-                                </div>
-                            )}
-
-                            {passkey &&
-                                [
-                                    "signIn",
-                                    "magicLink",
-                                    "emailOTP",
-                                    "recoverAccount",
-                                    "twoFactor",
-                                    "forgotPassword"
-                                ].includes(view) && (
-                                    <PasskeyButton
-                                        classNames={classNames}
-                                        isSubmitting={isSubmitting}
-                                        localization={localization}
-                                        redirectTo={redirectTo}
-                                        setIsSubmitting={setIsSubmitting}
-                                    />
-                                )}
-                        </div>
-                    </>
-                )}
+                                    )}
+                            </div>
+                        </>
+                    )}
             </CardContent>
 
             {credentials && signUp && (
diff --git a/src/components/auth/forms/reset-password-form.tsx b/src/components/auth/forms/reset-password-form.tsx
index fc0a0924d..5eaca8a76 100644
--- a/src/components/auth/forms/reset-password-form.tsx
+++ b/src/components/auth/forms/reset-password-form.tsx
@@ -7,8 +7,8 @@ import { useForm } from "react-hook-form"
 import * as z from "zod"
 
 import type { AuthLocalization } from "../../../lib/auth-localization"
-import { AuthUIContext } from "../../../lib/auth-ui-provider"
-import { cn, getLocalizedError } from "../../../lib/utils"
+import { AuthUIContext, type PasswordValidation } from "../../../lib/auth-ui-provider"
+import { cn, getLocalizedError, getPasswordSchema } from "../../../lib/utils"
 import { PasswordInput } from "../../password-input"
 import { Button } from "../../ui/button"
 import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../../ui/form"
@@ -18,9 +18,15 @@ export interface ResetPasswordFormProps {
     className?: string
     classNames?: AuthFormClassNames
     localization: Partial<AuthLocalization>
+    passwordValidation?: PasswordValidation
 }
 
-export function ResetPasswordForm({ className, classNames, localization }: ResetPasswordFormProps) {
+export function ResetPasswordForm({
+    className,
+    classNames,
+    localization,
+    passwordValidation
+}: ResetPasswordFormProps) {
     const tokenChecked = useRef(false)
 
     const {
@@ -30,19 +36,27 @@ export function ResetPasswordForm({ className, classNames, localization }: Reset
         localization: contextLocalization,
         viewPaths,
         navigate,
-        toast
+        toast,
+        passwordValidation: contextPasswordValidation
     } = useContext(AuthUIContext)
 
     localization = { ...contextLocalization, ...localization }
+    passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
 
     const formSchema = z
         .object({
-            newPassword: z.string().min(1, {
-                message: `${localization.password} ${localization.isRequired}`
+            newPassword: getPasswordSchema(passwordValidation, {
+                passwordRequired: localization.newPasswordRequired,
+                passwordTooShort: localization.passwordTooShort,
+                passwordTooLong: localization.passwordTooLong,
+                passwordInvalid: localization.passwordInvalid
             }),
             confirmPassword: confirmPasswordEnabled
-                ? z.string().min(1, {
-                      message: `${localization.confirmPassword} ${localization.isRequired}`
+                ? getPasswordSchema(passwordValidation, {
+                      passwordRequired: localization.confirmPasswordRequired,
+                      passwordTooShort: localization.passwordTooShort,
+                      passwordTooLong: localization.passwordTooLong,
+                      passwordInvalid: localization.passwordInvalid
                   })
                 : z.string().optional()
         })
diff --git a/src/components/auth/forms/sign-in-form.tsx b/src/components/auth/forms/sign-in-form.tsx
index c32b41d0c..503217470 100644
--- a/src/components/auth/forms/sign-in-form.tsx
+++ b/src/components/auth/forms/sign-in-form.tsx
@@ -11,8 +11,8 @@ import { useCaptcha } from "../../../hooks/use-captcha"
 import { useIsHydrated } from "../../../hooks/use-hydrated"
 import { useOnSuccessTransition } from "../../../hooks/use-success-transition"
 import type { AuthLocalization } from "../../../lib/auth-localization"
-import { AuthUIContext } from "../../../lib/auth-ui-provider"
-import { cn, getLocalizedError, isValidEmail } from "../../../lib/utils"
+import { AuthUIContext, type PasswordValidation } from "../../../lib/auth-ui-provider"
+import { cn, getLocalizedError, getPasswordSchema, isValidEmail } from "../../../lib/utils"
 import type { AuthClient } from "../../../types/auth-client"
 import { Captcha } from "../../captcha/captcha"
 import { PasswordInput } from "../../password-input"
@@ -29,6 +29,7 @@ export interface SignInFormProps {
     localization: Partial<AuthLocalization>
     redirectTo?: string
     setIsSubmitting?: (isSubmitting: boolean) => void
+    passwordValidation?: PasswordValidation
 }
 
 export function SignInForm({
@@ -37,7 +38,8 @@ export function SignInForm({
     isSubmitting,
     localization,
     redirectTo,
-    setIsSubmitting
+    setIsSubmitting,
+    passwordValidation
 }: SignInFormProps) {
     const isHydrated = useIsHydrated()
     const { captchaRef, getCaptchaHeaders } = useCaptcha({ localization })
@@ -50,13 +52,14 @@ export function SignInForm({
         rememberMe: rememberMeEnabled,
         username: usernameEnabled,
         viewPaths,
-        captcha,
         navigate,
         toast,
-        Link
+        Link,
+        passwordValidation: contextPasswordValidation
     } = useContext(AuthUIContext)
 
     localization = { ...contextLocalization, ...localization }
+    passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
 
     const { onSuccess, isPending: transitionPending } = useOnSuccessTransition({ redirectTo })
 
@@ -73,9 +76,7 @@ export function SignInForm({
                   .email({
                       message: `${localization.email} ${localization.isInvalid}`
                   }),
-        password: z.string().min(1, {
-            message: `${localization.password} ${localization.isRequired}`
-        }),
+        password: getPasswordSchema(passwordValidation, localization),
         rememberMe: z.boolean().optional()
     })
 
diff --git a/src/components/auth/forms/sign-up-form.tsx b/src/components/auth/forms/sign-up-form.tsx
index 03e1b5975..7a605245d 100644
--- a/src/components/auth/forms/sign-up-form.tsx
+++ b/src/components/auth/forms/sign-up-form.tsx
@@ -1,16 +1,18 @@
 "use client"
+
 import { zodResolver } from "@hookform/resolvers/zod"
 import type { BetterFetchOption } from "better-auth/react"
 import { Loader2 } from "lucide-react"
 import { useCallback, useContext, useEffect } from "react"
 import { useForm } from "react-hook-form"
 import * as z from "zod"
+
 import { useCaptcha } from "../../../hooks/use-captcha"
 import { useIsHydrated } from "../../../hooks/use-hydrated"
 import { useOnSuccessTransition } from "../../../hooks/use-success-transition"
 import type { AuthLocalization } from "../../../lib/auth-localization"
-import { AuthUIContext } from "../../../lib/auth-ui-provider"
-import { cn, getLocalizedError, getSearchParam } from "../../../lib/utils"
+import { AuthUIContext, type PasswordValidation } from "../../../lib/auth-ui-provider"
+import { cn, getLocalizedError, getPasswordSchema, getSearchParam } from "../../../lib/utils"
 import type { AuthClient } from "../../../types/auth-client"
 import { Captcha } from "../../captcha/captcha"
 import { PasswordInput } from "../../password-input"
@@ -28,6 +30,7 @@ export interface SignUpFormProps {
     localization: Partial<AuthLocalization>
     redirectTo?: string
     setIsSubmitting?: (value: boolean) => void
+    passwordValidation?: PasswordValidation
 }
 
 export function SignUpForm({
@@ -37,7 +40,8 @@ export function SignUpForm({
     isSubmitting,
     localization,
     redirectTo,
-    setIsSubmitting
+    setIsSubmitting,
+    passwordValidation
 }: SignUpFormProps) {
     const isHydrated = useIsHydrated()
     const { captchaRef, getCaptchaHeaders } = useCaptcha({ localization })
@@ -47,7 +51,6 @@ export function SignUpForm({
         authClient,
         basePath,
         baseURL,
-        captcha,
         confirmPassword: confirmPasswordEnabled,
         emailVerification,
         localization: contextLocalization,
@@ -58,10 +61,12 @@ export function SignUpForm({
         username: usernameEnabled,
         viewPaths,
         navigate,
-        toast
+        toast,
+        passwordValidation: contextPasswordValidation
     } = useContext(AuthUIContext)
 
     localization = { ...contextLocalization, ...localization }
+    passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
 
     const getRedirectTo = useCallback(
         () => redirectTo || getSearchParam("redirectTo") || contextRedirectTo,
@@ -91,15 +96,16 @@ export function SignUpForm({
             .email({
                 message: `${localization.email} ${localization.isInvalid}`
             }),
-        password: z.string().min(1, {
-            message: `${localization.password} ${localization.isRequired}`
-        })
+        password: getPasswordSchema(passwordValidation, localization)
     }
 
     // Add confirmPassword field if enabled
     if (confirmPasswordEnabled) {
-        schemaFields.confirmPassword = z.string().min(1, {
-            message: `${localization.confirmPassword} ${localization.isRequired}`
+        schemaFields.confirmPassword = getPasswordSchema(passwordValidation, {
+            passwordRequired: localization.confirmPasswordRequired,
+            passwordTooShort: localization.passwordTooShort,
+            passwordTooLong: localization.passwordTooLong,
+            passwordInvalid: localization.passwordInvalid
         })
     }
 
diff --git a/src/components/settings/change-password-card.tsx b/src/components/settings/change-password-card.tsx
index 773cba7e5..cb3fc3a86 100644
--- a/src/components/settings/change-password-card.tsx
+++ b/src/components/settings/change-password-card.tsx
@@ -5,8 +5,8 @@ import { useContext } from "react"
 import { useForm } from "react-hook-form"
 import * as z from "zod"
 import type { AuthLocalization } from "../../lib/auth-localization"
-import { AuthUIContext } from "../../lib/auth-ui-provider"
-import { cn, getLocalizedError } from "../../lib/utils"
+import { AuthUIContext, type PasswordValidation } from "../../lib/auth-ui-provider"
+import { cn, getLocalizedError, getPasswordSchema } from "../../lib/utils"
 import { PasswordInput } from "../password-input"
 import { CardContent } from "../ui/card"
 import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"
@@ -20,6 +20,7 @@ export interface ChangePasswordCardProps {
     isPending?: boolean
     localization?: AuthLocalization
     skipHook?: boolean
+    passwordValidation?: PasswordValidation
 }
 
 export function ChangePasswordCard({
@@ -28,7 +29,8 @@ export function ChangePasswordCard({
     accounts,
     isPending,
     localization,
-    skipHook
+    skipHook,
+    passwordValidation
 }: ChangePasswordCardProps) {
     const {
         authClient,
@@ -38,10 +40,12 @@ export function ChangePasswordCard({
         hooks: { useSession, useListAccounts },
         localization: contextLocalization,
         viewPaths,
-        toast
+        toast,
+        passwordValidation: contextPasswordValidation
     } = useContext(AuthUIContext)
 
     localization = { ...contextLocalization, ...localization }
+    passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
 
     const { data: sessionData } = useSession()
 
@@ -53,10 +57,20 @@ export function ChangePasswordCard({
 
     const formSchema = z
         .object({
-            currentPassword: z.string().min(1, { message: localization.passwordRequired }),
-            newPassword: z.string().min(1, { message: localization.newPasswordRequired }),
+            currentPassword: getPasswordSchema(passwordValidation, localization),
+            newPassword: getPasswordSchema(passwordValidation, {
+                passwordRequired: localization.newPasswordRequired,
+                passwordTooShort: localization.passwordTooShort,
+                passwordTooLong: localization.passwordTooLong,
+                passwordInvalid: localization.passwordInvalid
+            }),
             confirmPassword: confirmPasswordEnabled
-                ? z.string().min(1, { message: localization.confirmPasswordRequired })
+                ? getPasswordSchema(passwordValidation, {
+                      passwordRequired: localization.confirmPasswordRequired,
+                      passwordTooShort: localization.passwordTooShort,
+                      passwordTooLong: localization.passwordTooLong,
+                      passwordInvalid: localization.passwordInvalid
+                  })
                 : z.string().optional()
         })
         .refine((data) => !confirmPasswordEnabled || data.newPassword === data.confirmPassword, {
diff --git a/src/lib/auth-localization.ts b/src/lib/auth-localization.ts
index b3e8ac045..615175d1b 100644
--- a/src/lib/auth-localization.ts
+++ b/src/lib/auth-localization.ts
@@ -439,12 +439,12 @@ export const authLocalization = {
     /** @default "Go back" */
     goBack: "Go back",
 
-    /** @default "Invalid password" */
-    invalidPassword: "Invalid password",
-
     /** @default "Invalid email or password" */
     invalidEmailOrPassword: "Invalid email or password",
 
+    /** @default "Invalid password" */
+    passwordInvalid: "Invalid password",
+
     /** @default "Your session is not fresh. Please sign in again." */
     sessionNotFresh: "Your session is not fresh. Please sign in again.",
 
@@ -454,6 +454,9 @@ export const authLocalization = {
     /** @default "Password too short" */
     passwordTooShort: "Password too short",
 
+    /** @default "Password too long" */
+    passwordTooLong: "Password too long",
+
     /** @default "Upload Avatar" */
     uploadAvatar: "Upload Avatar",
 
diff --git a/src/lib/auth-ui-provider.tsx b/src/lib/auth-ui-provider.tsx
index b68066714..9ec8bc15d 100644
--- a/src/lib/auth-ui-provider.tsx
+++ b/src/lib/auth-ui-provider.tsx
@@ -40,6 +40,23 @@ const defaultToast: RenderToast = ({ variant = "default", message }) => {
     }
 }
 
+export type PasswordValidation = {
+    /**
+     * Maximum password length
+     */
+    maxLength?: number
+
+    /**
+     * Minimum password length
+     */
+    minLength?: number
+
+    /**
+     * Password validation regex
+     */
+    regex?: RegExp
+}
+
 export type AuthUIContextType = {
     authClient: AnyAuthClient
     /**
@@ -260,6 +277,10 @@ export type AuthUIContextType = {
      * @default <a>
      */
     Link: Link
+    /**
+     * Customize the password validation
+     */
+    passwordValidation?: PasswordValidation
 }
 
 export type AuthUIProviderProps = {
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 42810bf23..b8bc34c44 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,8 @@
 import { type ClassValue, clsx } from "clsx"
 import { twMerge } from "tailwind-merge"
+import * as z from "zod"
 import type { AuthLocalization } from "./auth-localization"
+import type { PasswordValidation } from "./auth-ui-provider"
 import type { AuthView, AuthViewPaths } from "./auth-view-paths"
 
 export function cn(...inputs: ClassValue[]) {
@@ -70,3 +72,28 @@ export function getKeyByValue<T extends Record<string, unknown>>(
 ): keyof T | undefined {
     return (Object.keys(object) as Array<keyof T>).find((key) => object[key] === value)
 }
+
+export function getPasswordSchema(
+    passwordValidation?: PasswordValidation,
+    localization?: Partial<AuthLocalization>
+) {
+    let schema = z.string().min(1, {
+        message: localization?.passwordRequired
+    })
+    if (passwordValidation?.minLength) {
+        schema = schema.min(passwordValidation.minLength, {
+            message: localization?.passwordTooShort
+        })
+    }
+    if (passwordValidation?.maxLength) {
+        schema = schema.max(passwordValidation.maxLength, {
+            message: localization?.passwordTooLong
+        })
+    }
+    if (passwordValidation?.regex) {
+        schema = schema.regex(passwordValidation.regex, {
+            message: localization?.passwordInvalid
+        })
+    }
+    return schema
+}


Reviews:
user: login: daveycodez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T09:57:44Z
user: login: daveycodez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:02:46Z
user: login: daveycodez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:03:09Z
user: login: daveycodez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:04:39Z
user: login: daveycodez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:05:32Z
user: login: k9982874
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T06:12:03Z
user: login: daveycodez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T06:16:33Z
user: login: daveycodez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T06:17:23Z
user: login: daveycodez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T21:48:36Z
user: login: daveycodez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T21:50:52Z

Review comments:
diff_hunk: @@ -278,6 +303,11 @@ export type AuthUIProviderProps = {
      * ADVANCED: Custom mutators for updating auth data
      */
     mutators?: Partial<AuthMutators>
+
+    /**
+     * Customize the password validation
+     */
+    passwordValidation?: PasswordValidation
path: src/lib/auth-ui-provider.tsx
user: login: daveycodez
body: this can be removed here, it's already included from AuthUIContextType
created_at: 2025-05-08T09:57:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,10 +94,12 @@ export function AuthCard({
         signUp,
         viewPaths,
         replace,
-        Link
+        Link,
+        passwordValidation: contextPasswordValidation
path: src/components/auth/auth-card.tsx
user: login: daveycodez
body: We no longer need to check contextPasswordValidation in AuthCard and AuthForm, we only need to check the context in the actual consumers
created_at: 2025-05-08T10:02:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,10 +94,12 @@ export function AuthCard({
         signUp,
         viewPaths,
         replace,
-        Link
+        Link,
+        passwordValidation: contextPasswordValidation
path: src/components/auth/auth-card.tsx
user: login: daveycodez
body: Since here it is just a pass-through prop
created_at: 2025-05-08T10:03:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,20 +36,40 @@ export function ResetPasswordForm({ className, classNames, localization }: Reset
         localization: contextLocalization,
         viewPaths,
         navigate,
-        toast
+        toast,
+        passwordValidation: contextPasswordValidation
     } = useContext(AuthUIContext)
 
     localization = { ...contextLocalization, ...localization }
+    passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
+
+    const passwordSchema = (defaultErrorMessage?: string) => {
+        let schema = passwordValidation?.minLength
+            ? z.string().min(passwordValidation.minLength, {
+                message: localization.passwordTooShort
+            })
+            : z.string().min(1, {
+                message: defaultErrorMessage
+            })
path: src/components/auth/forms/reset-password-form.tsx
user: login: daveycodez
body: This is where it should start with schema .min(1, {}) instead of conditionally checking. Check for min 1 first, then check for minLength additionally if it is set
created_at: 2025-05-08T10:04:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,20 +36,40 @@ export function ResetPasswordForm({ className, classNames, localization }: Reset
         localization: contextLocalization,
         viewPaths,
         navigate,
-        toast
+        toast,
+        passwordValidation: contextPasswordValidation
     } = useContext(AuthUIContext)
 
     localization = { ...contextLocalization, ...localization }
+    passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
+
+    const passwordSchema = (defaultErrorMessage?: string) => {
+        let schema = passwordValidation?.minLength
+            ? z.string().min(passwordValidation.minLength, {
+                message: localization.passwordTooShort
+            })
+            : z.string().min(1, {
+                message: defaultErrorMessage
+            })
path: src/components/auth/forms/reset-password-form.tsx
user: login: daveycodez
body: Maybe passwordSchema should be made its own ts file in lib folder, password-schema.ts ? and have it accept defaultError, localization, passwordValidation as parameters to the function. So we can re-use the code here
created_at: 2025-05-08T10:05:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,10 +94,12 @@ export function AuthCard({
         signUp,
         viewPaths,
         replace,
-        Link
+        Link,
+        passwordValidation: contextPasswordValidation
path: src/components/auth/auth-card.tsx
user: login: k9982874
body: In case, the developer cann't use the AuthCard without AuthUIProvider.
This design will force AuthCard to be tied to AuthUIProvider.

created_at: 2025-05-09T06:12:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,10 +94,12 @@ export function AuthCard({
         signUp,
         viewPaths,
         replace,
-        Link
+        Link,
+        passwordValidation: contextPasswordValidation
path: src/components/auth/auth-card.tsx
user: login: daveycodez
body: It's not possible to use anything without AuthUIProvider atm. I might change this in the future when I work on the shadcn registry port
created_at: 2025-05-09T06:16:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,10 +94,12 @@ export function AuthCard({
         signUp,
         viewPaths,
         replace,
-        Link
+        Link,
+        passwordValidation: contextPasswordValidation
path: src/components/auth/auth-card.tsx
user: login: daveycodez
body: The point is though that AuthCard doesn't care about checking the context for the passwordValidation prop, the children cards are the ones that check the context
created_at: 2025-05-09T06:17:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +38,26 @@ const defaultToast: RenderToast = ({ variant = "default", message }) => {
     }
 }
 
+/**
+* Customize the password validation
+*/
+export type PasswordValidation = {
+    /**
+     * Minimum password length
+     */
+    maxLength?: number
+
path: src/lib/auth-ui-provider.tsx
user: login: daveycodez
body: Comment is wrong here
created_at: 2025-05-09T21:48:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -278,6 +303,11 @@ export type AuthUIProviderProps = {
      * ADVANCED: Custom mutators for updating auth data
      */
     mutators?: Partial<AuthMutators>
+
+    /**
+     * Customize the password validation
+     */
+    passwordValidation?: PasswordValidation
path: src/lib/auth-ui-provider.tsx
user: login: daveycodez
body: This still needs to be removed here, and only defined in the Context type
created_at: 2025-05-09T21:50:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: daveycodez
created_at: 2025-05-07T10:00:03Z
body: This is cool, but I think these variables should live on AuthUIProvider Context so you can set it once and it applies it to AuthCard and ResetPasswordCard. Maybe a passwordValidation prop on AuthUIProvider?
author_association: CONTRIBUTOR
user: login: daveycodez
created_at: 2025-05-07T10:01:55Z
body: ```ts
<AuthUIProvider
  passwordValidation={{ minLength: 6, maxLength: 10, regex: asfdadfasdf }}
>
```
author_association: CONTRIBUTOR
user: login: k9982874
created_at: 2025-05-07T11:11:33Z
body: > ```ts
>   passwordValidation={{ minLength: 6, maxLength: 10, regex: asfdadfasdf }}
> ```

Hi @daveycodez, I did a refactoring. Add passwordValidation field into AuthUIProvider as your advice.
Kindly review and feedback
author_association: CONTRIBUTOR
user: login: daveycodez
created_at: 2025-05-07T20:33:16Z
body: Yea so the main thing here is that passwordValidation shouldn't be a prop anymore on any of the components and just come directly from useContext on the consumers. For example, SettingsCards doesn't need to accept and pass through passwordValidation since it can be obtained at the context level
author_association: CONTRIBUTOR
user: login: daveycodez
created_at: 2025-05-07T20:34:45Z
body: SettingsCards and AuthForm, for example, should have no changes for this PR, since they are simply passing through a variable that already exists in useContext
author_association: CONTRIBUTOR
user: login: daveycodez
created_at: 2025-05-07T20:36:52Z
body: Let's also rename this AuthLocalization key from invalidPassword to passwordInvalid and update its references

```ts
    invalidPassword: "Invalid password",
```
author_association: CONTRIBUTOR
user: login: k9982874
created_at: 2025-05-08T01:06:34Z
body: > Yea so the main thing here is that passwordValidation shouldn't be a prop anymore on any of the components and just come directly from useContext on the consumers. For example, SettingsCards doesn't need to accept and pass through passwordValidation since it can be obtained at the context level

From my perspective the components should be work individually in some cases.
The behavior of passwordValidation option should be like localization, providing a way to the developer to set up the components without AuthUIProvider.

> Let's also rename this AuthLocalization key from invalidPassword to passwordInvalid and update its references
> 
> ```ts
>     invalidPassword: "Invalid password",
> ```

Updated
author_association: CONTRIBUTOR
user: login: daveycodez
created_at: 2025-05-08T05:48:21Z
body: If you really need them as props I guess that's fine but they still need to fallback to useContext when the prop isn't provided, specifically on the components that use it
author_association: CONTRIBUTOR
user: login: k9982874
created_at: 2025-05-08T08:25:44Z
body: > If you really need them as props I guess that's fine but they still need to fallback to useContext when the prop isn't provided, specifically on the components that use it

updated, fallback was added
author_association: CONTRIBUTOR
user: login: daveycodez
created_at: 2025-05-08T09:56:33Z
body: The code comments for minLength / maxLength on AuthUIProvider props are backwards, reviewing the rest now
author_association: CONTRIBUTOR
user: login: daveycodez
created_at: 2025-05-08T10:00:17Z
body: I think it should still show the Password is required error with minLength 1 option even if you set a custom minLength. Empty password should say Password is required regardless
author_association: CONTRIBUTOR
user: login: daveycodez
created_at: 2025-05-15T10:51:31Z
body: Hey I'm planning on merging this sometime in the next few days, just a heads up. Just merged the Captcha PR and want to make sure there's no overlap
author_association: CONTRIBUTOR
user: login: daveycodez
created_at: 2025-05-16T08:52:12Z
body: This just went live with 1.6.4
author_association: CONTRIBUTOR