Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add password validation callback #174

Merged
merged 3 commits into from
Feb 3, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
add password validation callback
  • Loading branch information
lukeshay committed Jan 29, 2025
commit 191bd2ee655cb7d679c97f52745ba19513b857e8
5 changes: 5 additions & 0 deletions examples/issuer/bun/issuer.ts
Original file line number Diff line number Diff line change
@@ -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"
}
},
}),
),
},
37 changes: 37 additions & 0 deletions packages/openauth/src/provider/password.ts
Original file line number Diff line number Diff line change
@@ -125,6 +125,21 @@ export interface PasswordConfig {
* ```
*/
sendCode: (email: string, code: string) => Promise<void>
/**
* 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> | 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,
55 changes: 35 additions & 20 deletions packages/openauth/src/ui/password.tsx
Original file line number Diff line number Diff line change
@@ -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<PasswordConfig, "sendCode" | "validatePassword"> {
/**
* 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<Response> => {
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 = (
<Layout>
<form data-component="form" method="post">
<FormAlert message={error?.type && copy?.[`error_${error.type}`]} />
<FormAlert
message={
error?.type
? error.type === "validation_error"
? (error.message ?? copy?.[`error_${error.type}`])
: copy?.[`error_${error.type}`]
: undefined
}
/>
{state.type === "start" && (
<>
<input type="hidden" name="action" value="register" />
@@ -292,13 +297,23 @@ export function PasswordUI(input: PasswordUIOptions): PasswordConfig {
})
},
change: async (_req, state, form, error): Promise<Response> => {
const passwordError = ["invalid_password", "password_mismatch"].includes(
error?.type || "",
)
const passwordError = [
"invalid_password",
"password_mismatch",
"validation_error",
].includes(error?.type || "")
const jsx = (
<Layout>
<form data-component="form" method="post" replace>
<FormAlert message={error?.type && copy?.[`error_${error.type}`]} />
<FormAlert
message={
error?.type
? error.type === "validation_error"
? (error.message ?? copy?.[`error_${error.type}`])
: copy?.[`error_${error.type}`]
: undefined
}
/>
{state.type === "start" && (
<>
<input type="hidden" name="action" value="code" />
21 changes: 20 additions & 1 deletion www/src/content/docs/docs/provider/password.mdx
Original file line number Diff line number Diff line change
@@ -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. |
</Segment>
## PasswordChangeState
<Segment>
@@ -103,6 +104,7 @@ The state of the password change flow.
- <p>[<code class="key">login</code>](#passwordconfig.login) <code class="primitive">(req: <code class="type">Request</code>, form?: <code class="type">FormData</code>, error?: [<code class="type">PasswordLoginError</code>](/docs/provider/password#passwordloginerror)) => <code class="primitive">Promise</code><code class="symbol">&lt;</code><code class="type">Response</code><code class="symbol">&gt;</code></code></p>
- <p>[<code class="key">register</code>](#passwordconfig.register) <code class="primitive">(req: <code class="type">Request</code>, state: [<code class="type">PasswordRegisterState</code>](/docs/provider/password#passwordregisterstate), form?: <code class="type">FormData</code>, error?: [<code class="type">PasswordRegisterError</code>](/docs/provider/password#passwordregistererror)) => <code class="primitive">Promise</code><code class="symbol">&lt;</code><code class="type">Response</code><code class="symbol">&gt;</code></code></p>
- <p>[<code class="key">sendCode</code>](#passwordconfig.sendcode) <code class="primitive">(email: <code class="primitive">string</code>, code: <code class="primitive">string</code>) => <code class="primitive">Promise</code><code class="symbol">&lt;</code><code class="primitive">void</code><code class="symbol">&gt;</code></code></p>
- <p>[<code class="key">validatePassword</code>](#passwordconfig.sendcode) <code class="primitive">(password: <code class="primitive">string</code>) => <code class="primitive">Promise</code><code class="symbol">&lt;</code><code class="primitive">string</code><code class="symbol">|</code><code class="primitive">void</code><code class="symbol">&gt;</code><code class="symbol">|</code><code class="primitive">string</code><code class="symbol">|</code><code class="primitive">void</code></code></p>
</Section>
</Segment>
<NestedTitle id="passwordconfig.change" Tag="h4" parent="PasswordConfig.">change</NestedTitle>
@@ -175,6 +177,22 @@ Callback to send the confirmation pin code to the user.
}
```
</Segment>
<NestedTitle id="passwordconfig.sendcode" Tag="h4" parent="PasswordConfig.">validatePassword</NestedTitle>
<Segment>
<Section type="parameters">
<InlineSection>
**Type** <code class="primitive">(password: <code class="primitive">string</code>) => <code class="primitive">Promise</code><code class="symbol">&lt;</code><code class="primitive">string</code><code class="symbol">|</code><code class="primitive">void</code><code class="symbol">&gt;</code><code class="symbol">|</code><code class="primitive">string</code><code class="symbol">|</code><code class="primitive">void</code></code>
</InlineSection>
</Section>
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
}
}
```
</Segment>
## PasswordLoginError
<Segment>
<Section type="parameters">
@@ -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. |
</Segment>
## PasswordRegisterState
<Segment>
@@ -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. |
</Segment>
</div>
</div>