You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
| Flow | Request → verify code/link → update | Request → verify link → link account |
16
+
| Use case | User changes email address | User adds email sign-in to OAuth-only account |
17
+
18
+
Change email uses `change_email` verification type; link email uses `link_email`. Both use the same 6-digit + link UX and 15-minute TTL.
19
+
20
+
## OAuth provider linking
21
+
22
+
Logged-in users can link additional OAuth providers from Profile (Settings). The backend:
23
+
24
+
- Uses `oauth_link_state` (distinct from `oauth_state` for sign-in)
25
+
- Binds the link to `meta.userId` so the exchange attaches to the correct account
26
+
- Returns 409 `PROVIDER_ALREADY_LINKED` when the provider is linked to another user
27
+
- Returns `redirectTo` so the client can return the user to the settings page after success
28
+
29
+
## Provider trust
30
+
31
+
OAuth providers are trusted for identity on first link. Subsequent links require re-authorization; existing tokens are replaced on each successful link.
32
+
33
+
## Guardrail: last sign-in method
34
+
35
+
Users must keep at least one sign-in method. Unlinking (wallet, OAuth, or other methods) fails with 400 `LAST_SIGN_IN_METHOD` if it would leave no way to sign in.
36
+
37
+
## Verify modes (change email)
38
+
39
+
Verification accepts exactly one of:
40
+
41
+
-`{ token, email }` — code entry flow; email used as identifier
Copy file name to clipboardExpand all lines: apps/docu/content/docs/architecture/authentication.mdx
+36-5Lines changed: 36 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -93,6 +93,21 @@ Magic link sign-in issues a **6-digit login code** over email, then exchanges it
93
93
3.**Option A (manual):** User types the 6-digit code in the login form → `POST /auth/magiclink/verify` with `{ token: code }` → JWTs returned
94
94
4.**Option B (link):** User clicks the button in email → callback page receives `?token={code}` → verify → cookies set, redirect
95
95
96
+
### Change email (implemented)
97
+
98
+
Change email follows the same 6-digit + link pattern as magic link. User requests change, receives code and link in email, verifies via code entry or link click.
99
+
100
+
**Flow:**
101
+
1. User enters new email → `POST /account/email/change/request` (Bearer, body: `{ email, callbackUrl }`) → rate limited 3/hour per user
102
+
2. Email contains 6-digit code and link; identifier `userId:newEmail`; 15 min TTL
103
+
3.**Code entry:**`POST /account/email/change/verify` with `{ token, email }` (Bearer)
104
+
4.**Link click:** User hits `/auth/callback/change-email?token=&verificationId=` → callback verifies with Bearer, sets cookies, redirects `/settings?email_changed=ok`
105
+
5. On success: `users.email` updated, session rotated, old-email notification sent (fire-and-forget)
106
+
107
+
**Verify payload modes:** Exactly one of `{ token, email }` or `{ token, verificationId }`. Auth attempts type `change_email` (5 fail → 15 min lock).
108
+
109
+
**Callback without session:** Redirect to `/auth/login?redirect=...`; after login, user returns to callback with session and verifies.
110
+
96
111
### OAuth (GitHub)
97
112
98
113
OAuth sign-in uses provider authorization + callback flows and issues access + refresh JWTs. Provider accounts are stored in `account` (provider id, account id, and encrypted tokens at rest).
@@ -116,6 +131,17 @@ OAuth sign-in uses provider authorization + callback flows and issues access + r
116
131
117
132
When a provider is not configured (503 `OAUTH_NOT_CONFIGURED`), the client shows a Sonner toast instead of redirecting.
118
133
134
+
### OAuth account linking (implemented)
135
+
136
+
Logged-in users can link additional OAuth providers from Profile (Settings). Flow: `GET /auth/oauth/{provider}/link-authorize-url` (session/JWT required, no API key) → redirect to provider → callback → exchange with `oauth_link_state` → link account, return `{ token, refreshToken, redirectTo: "/settings?linked=ok" }`.
137
+
138
+
-**Link-authorize-url:** Session/JWT required, no API key; rate limit 10/hour per user. Stores `oauth_link_state` with `meta.userId`.
139
+
-**Exchange:** State lookup branches on `oauth_link_state` vs `oauth_state`. Link mode uses `meta.userId`; if provider already linked to another user → 409 `PROVIDER_ALREADY_LINKED`.
140
+
-**Unlink:**`DELETE /account/link/oauth/:providerId` (Bearer). Guardrail: cannot unlink if it would leave no sign-in method (400 `LAST_SIGN_IN_METHOD`).
See [Account linking](/docs/architecture/account-linking) for change email vs link email, provider trust, and guardrails.
144
+
119
145
### Web3 (SIWE / SIWS)
120
146
121
147
Web3 sign-in follows a **nonce → sign → verify** flow and returns access + refresh JWTs. Nonce and verify requests go **directly to Fastify**; the **callback** (token → cookie exchange) goes through the Next.js backend when cookies are needed.
@@ -290,13 +316,18 @@ All endpoints below are served by `apps/fastify`. The API does not set cookies;
**Callback pages** (`/auth/callback/magiclink`, `/auth/callback/oauth/github`, `/auth/callback/web3`, `/auth/callback/passkey`) exchange credentials with Fastify, set cookies, and redirect. **Logout** (`/auth/logout`) calls Fastify to revoke the session and clears cookies.
361
+
**Callback pages** (`/auth/callback/magiclink`, `/auth/callback/change-email`, `/auth/callback/oauth/github`, `/auth/callback/oauth/facebook`, `/auth/callback/oauth/twitter`, `/auth/callback/web3`, `/auth/callback/passkey`) exchange credentials with Fastify, set cookies, and redirect. **Logout** (`/auth/logout`) calls Fastify to revoke the session and clears cookies.
331
362
332
363
**API routes** (cookie updates only):
333
364
-`POST /api/auth/update-tokens`: accepts `{ token, refreshToken }`, sets single cookie. Used after 401 refresh (core's `onTokensRefreshed`) and after link email or profile edit.
@@ -443,7 +474,7 @@ sequenceDiagram
443
474
The auth system relies on these tables:
444
475
445
476
-**`users`**: user identity (email-based for magic link)
446
-
-**`verification`**: single-use tokens (stored hashed, with TTL); `type` = `magic_link` | `link_email` | `oauth_state`(link_email reserved for Phase 1b)
0 commit comments