# Reset and Change Passwords | Creght

> Implement email-code password reset and signed-in password change in Creght Func with ctx.users.find / checkPassword / setPassword: full code, three rules against account enumeration, and an error reference.

[![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)API for AI](/)

[View llms.txt](/llms.txt)

Overview

- [Creght API for AI](/api.md)

Discoverability

- [How to optimize llms.txt](/api/optimize-llms-txt.md)

Site configuration

- [Implement domain-based locale routing](/api/domain-locale-routing.md)

Backend

- [Build site backend workflows with Func](/api/func-backend.md)
- [Integrate Alipay PC Web Payment with Func](/api/func-alipay-payment.md)

Integrations

- [Send Email and Verification Codes with Integrations](/api/func-email-integration.md)

Auth

- [Require a Verified Email to Sign Up](/api/auth-verified-registration.md)
- [Reset and Change Passwords](/api/auth-password-reset.md)

On this page

- [Before you start](#prepare)
- [Files and call names](#files)
- [ctx.auth for the caller, ctx.users for anyone else](#namespace)
- [Flow 1: signed out, reset with an email code](#reset)
- [Three rules you have to follow](#rules)
- [Flow 2: signed in, change the password](#change)
- [Page code](#page)
- [Do not reimplement these](#dont)
- [Error reference](#errors)
- [Testing it](#test)

Auth/Reset and Change Passwords

# Reset and Change Passwords

Build "forgot password" and "change password" in Func: ctx.users.find decides whether to send a code, ctx.email.verifyCode checks it, ctx.users.setPassword changes it. Complete code for both flows, the three rules that fail silently if you skip them, and what you should not reimplement.

Copy Markdown link

Building "forgot password" and "change password" on your site: Creght has **no** built-in
password-reset endpoint, and no `resetPassword()` in the client SDK. You write these two methods in Func
and call them from the page with `invoke()`. Password hashing, single-use codes and their rate limits,
revoking old sessions, and brute-force lockout are handled by the platform — you write what makes the change
allowed.

**Scope**

Covers `ctx.users.find`,
`ctx.users.checkPassword` and `ctx.users.setPassword`, together with
`ctx.email.sendCode` / `verifyCode`, for two flows: **resetting a password with an email**
**code while signed out**, and **changing it while signed in**. For verification during signup see
[Require a Verified Email to Sign Up](/api/auth-verified-registration.md).

## Before you start

1. Connect an email integration to the project (editor: **Backend → Integrations**), see
    [Send email and verification codes through an integration](/api/func-email-integration.md).
    Without it no code can be sent.
2. `talizen` client 0.2.37 or later, which only affects type hints in the editor.

**Unrelated to your registration settings**: whatever you chose for open registration, required email
verification, or Func-only registration, password reset works the same. Leave those alone.

## Files and call names

Funcs are site source files. The path without its extension is the call key, and anything after
`.` is the method name:

| File | How the page calls it |
| --- | --- |
| `/backend/func/auth/reset-password.ts` | `invoke('auth/reset-password.requestCode', …)`<br>`invoke('auth/reset-password.confirm', …)` |
| `/backend/func/auth/change-password.ts` | `invoke('auth/change-password', …)` (no method name needed when the export is `main`) |

## ctx.auth for the caller, ctx.users for anyone else

| What you want | Use |
| --- | --- |
| Read who is signed in | `ctx.auth.currentUser()` / `ctx.auth.requireUser()` |
| Look a user up by email / account / id | `ctx.users.find({ email })`, returns `null` when there is none |
| Check a user's current password | `ctx.users.checkPassword({ userId, password })` |
| Change a user's password | `ctx.users.setPassword({ email, password })` |

All three `ctx.users` methods can point at **any user in the project**, so pointing at the
wrong one changes the wrong account. Give **exactly one** of `email`, `account` or
`userId`; passing two is an error.

## Flow 1: signed out, reset with an email code

Two requests: ask for a code, then trade the code for a new password.

```typescript
// /backend/func/auth/reset-password.ts
import type { TalizenFuncContext } from 'talizen/func-runtime'

const SCENE = 'reset_password'
const MIN_PASSWORD = 8

// Step 1: ask for a code
export function requestCode(input: { email?: string }, ctx: TalizenFuncContext) {
  const email = (input.email ?? '').trim()
  if (!email) throw new Error('email is required')

  // Check whether the account exists before deciding to send anything
  const user = ctx.users.find({ email })
  if (user) {
    try {
      ctx.email.sendCode({ to: email, scene: SCENE })
    } catch (err) {
      // Send failures (rate limit, integration trouble) must not reach the
      // browser either. Log them and read the logs yourself
      console.error('send reset code failed', String(err))
    }
  }

  // Same result whether or not the account exists
  return { ok: true }
}

// Step 2: verify the code and set the password, in one request
export function confirm(
  input: { email?: string; code?: string; password?: string },
  ctx: TalizenFuncContext,
) {
  const email = (input.email ?? '').trim()
  const code = (input.code ?? '').trim()
  const password = input.password ?? ''

  if (!email || !code) throw new Error('email and code are required')
  if (password.length < MIN_PASSWORD) throw new Error('password must be at least 8 characters')

  // Check existence before verifying: a matching code is consumed, so verifying
  // against a non-existent address burns one of a real user's attempts
  const user = ctx.users.find({ email })
  if (!user) throw new Error('the code is wrong or has expired')

  if (!ctx.email.verifyCode({ to: email, scene: SCENE, code }))
    throw new Error('the code is wrong or has expired')

  // Only verified requests reach this line. setPassword takes no code: the lines
  // above are what decides whether the change is allowed
  ctx.users.setPassword({ email, password })
  return { ok: true }
}
```

`scene` keeps codes for different purposes apart. It accepts letters, digits, underscore and dash,
1–32 characters. A code the user received for signing in cannot be spent on a password reset.

## Three rules you have to follow

None of these three raises an error. Skip one and the site still works, but you have a real problem:

| Do this | Or else |
| --- | --- |
| Call `ctx.users.find` **before** sending a code | Anyone can use your "forgot password" form to make your domain mail arbitrary addresses — your money, and your sending reputation |
| Return the **same result** whether or not the account exists | Returning `{ found: false }` hands out a lookup endpoint: anyone can probe which addresses are registered on your site |
| Catch `setPassword`'s errors and replace them with one fixed message | Same as above: a 404 "user not found" reaching the browser gives away exactly the same thing |

The page has to cooperate: **show one fixed message regardless of what the call returns** — something
like "if that address has an account, we sent a verification code" — and never branch on the result.

## Flow 2: signed in, change the password

Confirm the current password with `ctx.users.checkPassword`, then change it:

```typescript
// /backend/func/auth/change-password.ts
import type { TalizenFuncContext } from 'talizen/func-runtime'

export function main(
  input: { oldPassword: string; password: string },
  ctx: TalizenFuncContext,
) {
  const user = ctx.auth.requireUser()
  if (input.password.length < 8) throw new Error('password must be at least 8 characters')

  if (!ctx.users.checkPassword({ userId: user.id, password: input.oldPassword }))
    throw new Error('current password is incorrect')

  ctx.users.setPassword({ userId: user.id, password: input.password })
  return { ok: true }
}
```

**userId must come from requireUser()**

Never write
`ctx.users.setPassword({ userId: input.userId, … })`. That lets the browser choose whose password changes,
so any signed-in visitor could change somebody else's. It is the one way to get this badly wrong. For the same
reason, send codes to `user.email`, never to an address from `input`.

`checkPassword` returns a boolean: a wrong password gives `false` rather than an error. It
also returns `false` for accounts created through Google or another provider that never set a password —
for those, use the email-code flow above instead, sending to
`ctx.auth.requireUser().email`.

> After too many wrong attempts in a row, `checkPassword` throws **429** and
> locks for an hour. That allowance is shared with the login endpoint, so somebody who just failed a few sign-ins gets
> locked sooner here. Give 429 its own message ("too many attempts, try again later") instead of folding it into
> "current password is incorrect", or the user will keep retrying until fully locked out.

## Page code

```tsx
import { useState } from 'react'
import { invoke } from 'talizen/func'
import { useAuth } from 'talizen/auth'

export function ForgotPasswordForm() {
  const [step, setStep] = useState<'email' | 'code'>('email')
  const [email, setEmail] = useState('')
  const [code, setCode] = useState('')
  const [password, setPassword] = useState('')
  const [hint, setHint] = useState('')

  async function requestCode() {
    await invoke('auth/reset-password.requestCode', { email })
    // Fixed text: never infer from the result whether this address is registered
    setHint('If that address has an account, we sent a verification code')
    setStep('code')
  }

  async function confirm() {
    try {
      await invoke('auth/reset-password.confirm', { email, code, password })
      window.location.href = '/login'
    } catch (err) {
      setHint(err instanceof Error ? err.message : 'Reset failed, please try again')
    }
  }

  // ...form markup omitted
}

// Changing the password while signed in
async function changePassword(oldPassword: string, password: string) {
  await invoke('auth/change-password', { oldPassword, password })
  await useAuth().logout()          // the session is already void server-side
  window.location.href = '/login'
}
```

**Changing a password signs the user out**

`setPassword`
revokes **every** session that user has, including the current one. Both flows must send the visitor to
the login page on success rather than carrying on with the old session.

## Do not reimplement these

The platform already guarantees the following; rewriting any of it in Func only makes it weaker:

| Do not write | What already happens |
| --- | --- |
| Codes from `Math.random()`, or storing codes in `ctx.db` / `ctx.cache` | `sendCode` issues a 6-digit code valid for 10 minutes, consumed on match, compared in constant time (length and expiry are integration settings) |
| Your own "wrong code" counter | A code is voided after 5 wrong guesses |
| Your own "how soon can they ask again" limit | At most 5 mails per recipient per 10 minutes, and 500 per project per day |
| Hashing passwords yourself, or storing them in a JSON table | `setPassword` uses bcrypt; plaintext never reaches the database or the logs |
| Clearing sessions yourself after a change | `setPassword` revokes all of that user's sessions (and you have no access to the session table anyway) |
| Your own failed-password counter and lockout | `checkPassword` shares the login endpoint's allowance: 5 failures, locked for an hour |

Three things are genuinely yours: **password length and strength rules** (the platform only rejects an
empty password), **what makes the change allowed** (a code, the old password, or your own risk checks),
and **the wording and redirects**.

## Error reference

| Case | Behaviour |
| --- | --- |
| `find` matches nobody | Returns `null`, does not throw |
| Wrong code, expired code, or no code ever sent | `verifyCode` returns `false` in all three cases, undistinguished |
| One recipient asking repeatedly | `sendCode` throws (rate limited) |
| No email integration on the project | `sendCode` throws and points at the integrations panel |
| `setPassword` on an unknown user | Throws 404. Catch it; do not pass it to the browser |
| Account only signs in through a provider and has no password | `setPassword` throws 404, `checkPassword` returns `false`. The platform will not add a password sign-in to it |
| Disabled account | Throws 403. A new password would not let them in, so it fails outright |
| One address, several accounts | `setPassword({ email })` throws 409 instead of changing "the first one". Use `account` or `userId` |
| Too many `checkPassword` failures | Throws 429, locked for an hour, sharing the login endpoint's allowance |
| More than one of the three ref fields given | Throws 400. Pass exactly one of `email`, `account`, `userId` |

## Testing it

No need to publish the site first. Run the method with sample input from the Func editor, or from the CLI:

```bash
echo '{"email":"you@example.com"}' > input.json

creght func run --site_id=<project_id>/<site_id> \
  --key=auth/reset-password.requestCode \
  --input=input.json
```

Run `requestCode` once with an address that really has an account and confirm the code arrives, then
once with an address that does not: the response must be **byte-for-byte the same** and no mail should
arrive. That is the second rule above, verified.

See also: [Require a Verified Email to Sign Up](/api/auth-verified-registration.md),
[Send email and verification codes through an integration](/api/func-email-integration.md),
[Build a site backend with Func](/api/func-backend.md).

![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)

This website is built with [Creght](/)

[Discord](https://discord.gg/Qvq2nmZNnb)

## Links

- [Pricing](/price.md)
- [Solutions](/solution.md)
- [Customers](/customers.md)
- [Help Center](/help.md)
- [Contact Us](/contact.md)
- [Update Logs & Blogs](/blogs.md)
- [Refund Policy](/tuikuan.md)

## Resources

- [All Resources](/resources.md)
- [Templates](/templates.md)
- [Components](https://creghtlib.site.creght.com)
- [Animations](/effects.md)
- [Figma to Creght](/figma2creght.md)
- [API for AI](/api.md)

## Terms

- [Terms of Service](/legal/terms.md)
- [Privacy Policy](/legal/privacy.md)
- [Acceptable Use Policy](/legal/acceptable-use.md)

## Social Media

- [Twitter](https://twitter.com)
- [Facebook](https://www.facebook.com)
- [LinkedIn](https://www.linkedin.com)

[蜀ICP备2023038192号-2](https://beian.miit.gov.cn)
