From 191bd2ee655cb7d679c97f52745ba19513b857e8 Mon Sep 17 00:00:00 2001 From: Luke Shay Date: Wed, 29 Jan 2025 10:35:21 -0600 Subject: [PATCH 1/3] add password validation callback --- examples/issuer/bun/issuer.ts | 5 ++ packages/openauth/src/provider/password.ts | 37 +++++++++++++ packages/openauth/src/ui/password.tsx | 55 ++++++++++++------- .../content/docs/docs/provider/password.mdx | 21 ++++++- 4 files changed, 97 insertions(+), 21 deletions(-) diff --git a/examples/issuer/bun/issuer.ts b/examples/issuer/bun/issuer.ts index e9021331..55d4c86d 100644 --- a/examples/issuer/bun/issuer.ts +++ b/examples/issuer/bun/issuer.ts @@ -21,6 +21,11 @@ export default issuer({ sendCode: async (email, code) => { console.log(email, code) }, + validatePassword: (password) => { + if (password.length < 8) { + return "Password must be at least 8 characters" + } + }, }), ), }, diff --git a/packages/openauth/src/provider/password.ts b/packages/openauth/src/provider/password.ts index 90c57372..74b95623 100644 --- a/packages/openauth/src/provider/password.ts +++ b/packages/openauth/src/provider/password.ts @@ -125,6 +125,21 @@ export interface PasswordConfig { * ``` */ sendCode: (email: string, code: string) => Promise + /** + * Callback to validate the password on sign up and password reset. + * + * @example + * ```ts + * { + * validatePassword: (password) => { + * return password.length < 8 ? "Password must be at least 8 characters" : undefined + * } + * } + * ``` + */ + validatePassword?: ( + password: string, + ) => Promise | string | undefined } /** @@ -173,6 +188,10 @@ export type PasswordRegisterError = | { type: "password_mismatch" } + | { + type: "validation_error" + message?: string + } /** * The state of the password change flow. @@ -223,6 +242,10 @@ export type PasswordChangeError = | { type: "password_mismatch" } + | { + type: "validation_error" + message: string + } /** * The errors that can happen on the login screen. @@ -321,6 +344,20 @@ export function PasswordProvider( return transition(provider, { type: "invalid_password" }) if (password !== repeat) return transition(provider, { type: "password_mismatch" }) + if (config.validatePassword) { + let validationError: string | undefined + try { + validationError = await config.validatePassword(password) + } catch (error) { + validationError = + error instanceof Error ? error.message : undefined + } + if (validationError) + return transition(provider, { + type: "validation_error", + message: validationError, + }) + } const existing = await Storage.get(ctx.storage, [ "email", email, diff --git a/packages/openauth/src/ui/password.tsx b/packages/openauth/src/ui/password.tsx index 4dbb34f5..360f28da 100644 --- a/packages/openauth/src/ui/password.tsx +++ b/packages/openauth/src/ui/password.tsx @@ -55,6 +55,10 @@ const DEFAULT_COPY = { * Error message when the passwords do not match. */ error_password_mismatch: "Passwords do not match.", + /** + * Error message when the user enters a password that fails validation. + */ + error_validation_error: "Password does not meet requirements.", /** * Title of the register page. */ @@ -136,18 +140,8 @@ type PasswordUICopy = typeof DEFAULT_COPY /** * Configure the password UI. */ -export interface PasswordUIOptions { - /** - * Callback to send the confirmation code to the user. - * - * @example - * ```ts - * async (email, code) => { - * // Send an email with the code - * } - * ``` - */ - sendCode: PasswordConfig["sendCode"] +export interface PasswordUIOptions + extends Pick { /** * Custom copy for the UI. */ @@ -164,6 +158,7 @@ export function PasswordUI(input: PasswordUIOptions): PasswordConfig { ...input.copy, } return { + validatePassword: input.validatePassword, sendCode: input.sendCode, login: async (_req, form, error): Promise => { const jsx = ( @@ -214,13 +209,23 @@ export function PasswordUI(input: PasswordUIOptions): PasswordConfig { const emailError = ["invalid_email", "email_taken"].includes( error?.type || "", ) - const passwordError = ["invalid_password", "password_mismatch"].includes( - error?.type || "", - ) + const passwordError = [ + "invalid_password", + "password_mismatch", + "validation_error", + ].includes(error?.type || "") const jsx = (
- + {state.type === "start" && ( <> @@ -292,13 +297,23 @@ export function PasswordUI(input: PasswordUIOptions): PasswordConfig { }) }, change: async (_req, state, form, error): Promise => { - const passwordError = ["invalid_password", "password_mismatch"].includes( - error?.type || "", - ) + const passwordError = [ + "invalid_password", + "password_mismatch", + "validation_error", + ].includes(error?.type || "") const jsx = ( - + {state.type === "start" && ( <> diff --git a/www/src/content/docs/docs/provider/password.mdx b/www/src/content/docs/docs/provider/password.mdx index 81248a86..416ec010 100644 --- a/www/src/content/docs/docs/provider/password.mdx +++ b/www/src/content/docs/docs/provider/password.mdx @@ -80,6 +80,7 @@ The errors that can happen on the change password screen. | `invalid_code` | The code is invalid. | | `invalid_password` | The password is invalid. | | `password_mismatch` | The passwords do not match. | +| `validation_error` | The password does not meet requirements. | ## PasswordChangeState @@ -103,6 +104,7 @@ The state of the password change flow. -

[login](#passwordconfig.login) (req: Request, form?: FormData, error?: [PasswordLoginError](/docs/provider/password#passwordloginerror)) => Promise<Response>

-

[register](#passwordconfig.register) (req: Request, state: [PasswordRegisterState](/docs/provider/password#passwordregisterstate), form?: FormData, error?: [PasswordRegisterError](/docs/provider/password#passwordregistererror)) => Promise<Response>

-

[sendCode](#passwordconfig.sendcode) (email: string, code: string) => Promise<void>

+-

[validatePassword](#passwordconfig.sendcode) (password: string) => Promise<string|void>|string|void

change @@ -175,6 +177,22 @@ Callback to send the confirmation pin code to the user. } ``` +validatePassword + +
+ +**Type** (password: string) => Promise<string|void>|string|void + +
+Callback to validate the password on sign up and password reset. +```ts +{ + validatePassword: (password) => { + return password.length < 8 ? "Password must be at least 8 characters" : undefined + } +} +``` +
## PasswordLoginError
@@ -205,6 +223,7 @@ The errors that can happen on the register screen. | `invalid_code` | The code is invalid. | | `invalid_password` | The password is invalid. | | `password_mismatch` | The passwords do not match. | +| `validation_error` | The password does not meet requirements. | ## PasswordRegisterState @@ -220,4 +239,4 @@ The states that can happen on the register screen. | `start` | The user is asked to enter their email address and password to start the flow. | | `code` | The user needs to enter the pin code to verify their email. | - \ No newline at end of file + From 0a64ca4ddd73a104bd10b66f975e58060a485a7c Mon Sep 17 00:00:00 2001 From: Luke Shay Date: Sun, 2 Feb 2025 13:38:00 -0600 Subject: [PATCH 2/3] support standard schema --- packages/openauth/src/provider/password.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/openauth/src/provider/password.ts b/packages/openauth/src/provider/password.ts index 74b95623..ea6be351 100644 --- a/packages/openauth/src/provider/password.ts +++ b/packages/openauth/src/provider/password.ts @@ -41,6 +41,7 @@ import { UnknownStateError } from "../error.js" import { Storage } from "../storage/storage.js" import { Provider } from "./provider.js" import { generateUnbiasedDigits, timingSafeCompare } from "../random.js" +import { v1 } from "@standard-schema/spec" /** * @internal @@ -137,9 +138,9 @@ export interface PasswordConfig { * } * ``` */ - validatePassword?: ( - password: string, - ) => Promise | string | undefined + validatePassword?: + | v1.StandardSchema + | ((password: string) => Promise | string | undefined) } /** @@ -347,7 +348,18 @@ export function PasswordProvider( if (config.validatePassword) { let validationError: string | undefined try { - validationError = await config.validatePassword(password) + if (typeof config.validatePassword === "function") { + validationError = await config.validatePassword(password) + } else { + const res = + await config.validatePassword["~standard"].validate(password) + + if (res.issues?.length) { + throw new Error( + res.issues.map((issue) => issue.message).join(", "), + ) + } + } } catch (error) { validationError = error instanceof Error ? error.message : undefined From e686a78bba90b1b9b7a99b66a224690856a11e2a Mon Sep 17 00:00:00 2001 From: Dax Date: Mon, 3 Feb 2025 16:38:13 -0500 Subject: [PATCH 3/3] Create real-coats-clap.md --- .changeset/real-coats-clap.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/real-coats-clap.md diff --git a/.changeset/real-coats-clap.md b/.changeset/real-coats-clap.md new file mode 100644 index 00000000..413b6796 --- /dev/null +++ b/.changeset/real-coats-clap.md @@ -0,0 +1,6 @@ +--- +"@openauthjs/openauth": patch +"@openauthjs/example-issuer-bun": patch +--- + +Add password validation callback