Password Reset

The three-step password reset flow using magic links, from initiation through link verification to new password submission.

The password reset flow allows users to change their password when they have forgotten it. It follows a three-step magic link pattern: the user requests a reset, clicks the link in their email, and submits a new password along with the 7-digit verification code.

All three routes are registered by magicLinksRouter and require no custom server code.


Route registration

Call magicLinksRouter with your router or app instance during startup.

import { createRouter } from 'h3'
import { magicLinksRouter } from 'auth-h3client/v1'

const router = createRouter()
magicLinksRouter(router, 'api')

When using the Nuxt module, all magic link routes are registered automatically by defineAuthConfiguration in your Nitro plugin. See the Nuxt Module setup for details.

With the 'api' prefix, the following routes are registered:

MethodPathHandlerPurpose
POST/api/auth/password-resetrestartPasswordHandlerSends the reset email
GET/api/auth/reset-passwordverifyTempPasswordLinkValidates the magic link
POST/api/auth/reset-passwordsendNewPasswordHandlerSubmits the new password with the verification code

Step 1: request a reset

The user submits their email address to POST /api/auth/password-reset. The route applies CSRF verification, Content-Type: application/json validation, and a 1 KB body limit before the handler runs.

The handler proxies the email to the IAM /auth/forgot-password endpoint. The IAM service:

  1. Looks up the user by email.
  2. Generates a magic link JWT with purpose: "PASSWORD_RESET" and a 20-minute TTL.
  3. Generates a 7-digit verification code.
  4. Sends both to the user's email address.
  5. Returns a generic success message regardless of whether the email exists, to prevent email enumeration.

The gateway forwards the response to the client. The frontend should show a "check your email" message.

Error responses

IAM statusGateway response
200200 with success message
429429 with Retry-After header
500500

When the user clicks the magic link in their email, the bounce route redirects them to your frontend verification page with query parameters (token, random, reason, visitor). The frontend code then needs to detect that the reason is "PASSWORD_RESET" and sends a GET request to /api/auth/reset-password with those parameters, and all cookies identifiers.

The handler:

  1. Sets Cache-Control: no-store.
  2. Validates the canary_id cookie and token query parameter.
  3. Proxies the request to the IAM service to verify the link signature and expiry.

On success, the handler returns the verification result from the IAM service:

{
  "ok": true,
  "date": "2026-04-12T10:00:00.000Z",
  "data": {
    "reason": "PASSWORD_RESET",
    "link": "Password Reset"
  }
}

If the link is invalid or expired, the handler returns a 404 error response.

The useMagicLink composable takes care of sending the GET request with the right parameters.


Step 3: submit the new password

The user enters a new password and the 7-digit code from their email. The frontend submits both to POST /api/auth/reset-password with the magic link query parameters.

The route middleware stack:

  1. Link verification: re-validates the magic link parameters.
  2. CSRF verification: validates the X-CSRF-Token header.
  3. Content-Type: requires application/json.
  4. Body limit: 1 KB maximum.

The handler proxies the new password and verification code to the IAM service, which:

  1. Validates the magic link JWT and ensures purpose is PASSWORD_RESET.
  2. Verifies the 7-digit code.
  3. Checks the new password against the Have I Been Pwned API.
  4. Hashes the new password with Argon2 and updates the database.
  5. Sends a security notification email.
  6. Returns success.

The password must meet the same policy enforced by the IAM service: at least 12 characters with one uppercase letter, one lowercase letter, one digit, and one special character.

Error responses

IAM statusGateway responseMeaning
200200Password updated
400400Invalid code, weak password, or validation error
404404Invalid or expired link
429429 with Retry-AfterRate limited
500500Server error

Client-side integration

The useMagicLink composable handles routing automatically. When the reason query parameter is PASSWORD_RESET, it sends the GET request to /api/auth/reset-password and returns the verified data.

Your verification page checks the returned reason and renders the password reset form:

<script setup lang="ts">
const data = await useMagicLink()

// data.reason === 'PASSWORD_RESET'
// Render password reset form with code input
</script>

The form submits both the new password and the 7-digit code to POST /api/auth/reset-password, passing the original query parameters (token, random, reason, visitor) as query string values.

See Client-Side MFA for the full page implementation pattern.

Logo