Email Change
The email change flow allows authenticated users to update their email address. It follows the same magic link pattern as the other MFA flows: the user initiates the change, verifies their identity through the magic link and code, and submits the new email.
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')
import { H3 } from 'h3'
import { magicLinksRouter } from 'auth-h3client/v2'
const app = new H3()
magicLinksRouter(app, '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:
| Method | Path | Handler | Purpose |
|---|---|---|---|
| POST | /api/auth/change-email | initEmailChangeFlow | Sends the verification email to the current address |
| GET | /api/auth/update-email | changeEmailGetAPI | Validates the magic link |
| POST | /api/auth/update-email | updateNewEmail | Submits the new email with the verification code |
Step 1: initiate the change
The user requests an email change by calling POST /api/auth/change-email. The request requires valid session cookies and a CSRF token. The body must be Content-Type: application/json with a single field:
{ "init": "1" }
The gateway validates the session, generates a 128-byte random token, and calls the IAM service via askForMfaFlow. The IAM service:
- Validates the user's session.
- Generates a magic link JWT with
purpose: "change_email"and a 20-minute TTL. - Generates a 7-digit verification code.
- Sends both to the user's current email address.
- Returns success.
The gateway returns { ok: true, data: "Please check your email to complete the action." } to the client. The frontend should show a "check your email" message.
If the session has anomalies, the IAM may require the user to complete the built-in MFA flow first. In that case the gateway returns { ok: false, code: "MFA_REQUIRED", reason: "..." }.
Step 2: verify the link
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 "change_email" and sends a GET request to /api/auth/update-email with those parameters, and all cookies identifiers.
The handler:
- Sets
Cache-Control: no-store. - Issues a CSRF cookie for the subsequent POST.
- Validates the magic link with the IAM service.
On success, the handler returns the verification result from the IAM service:
{
"ok": true,
"date": "2026-04-12T10:00:00.000Z",
"data": {
"reason": "change_email",
"link": "Custom MFA"
}
}
The frontend needs to renders the email change form with fields for the 7-digit code, the current email, and the new email address.
If the link is invalid or expired, the handler returns a 404 error response.
The useMagicLink composable detects reason: "change_email" and sends a GET request to /api/auth/update-email automatically.
Step 3: submit the new email
The user enters the 7-digit code from their email, their current email, the new email address, and their password. The frontend submits all of these to POST /api/auth/update-email with the magic link query parameters.
The handler proxies the request to the IAM service, which:
- Verifies the magic link and the 7-digit code.
- Validates the new email (MX lookup, disposable email check).
- Confirms the password.
- Updates the email address in the database.
- Rotates the session tokens.
- Returns the new credentials.
The gateway applies the token rotation and returns success to the client.
Error responses
| IAM status | Gateway response | Meaning |
|---|---|---|
| 200 | 200 | Email updated, tokens rotated |
| 400 | 400 | Invalid code, validation error, or email already in use |
| 401 | 401 | Invalid credentials |
| 429 | 429 with Retry-After | Rate limited |
| 500 | 500 | Server error |
Client-side integration
The useMagicLink composable routes reason: "change_email" to /api/auth/update-email automatically. Your verification page checks the returned reason and renders the email change form:
<script setup lang="ts">
const data = await useMagicLink()
// data.reason === 'change_email'
// Render email change form with: code, current email, new email, password
</script>
The form submits to POST /api/auth/update-email using executeRequest, passing the original query parameters as query string values.
See Client-Side MFA for the full page implementation pattern.