@netlify/identity 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,27 @@
1
1
  # @netlify/identity
2
2
 
3
- A lightweight, no-config headless authentication library for projects using Netlify Identity. Works in both browser and server contexts.
4
- This is NOT the Netlify Identity Widget. This library exports standalone async functions (e.g., import { login, getUser } from '@netlify/identity'). There is no class to instantiate and no .init() call. Just import the functions you need and call them.
3
+ A lightweight, no-config headless authentication library for projects using Netlify Identity. Works in both browser and
4
+ server contexts. This is NOT the Netlify Identity Widget. This library exports standalone async functions (e.g., import
5
+ { login, getUser } from '@netlify/identity'). There is no class to instantiate and no .init() call. Just import the
6
+ functions you need and call them.
5
7
 
6
8
  **Prerequisites:**
7
9
 
8
- - [Netlify Identity](https://docs.netlify.com/security/secure-access-to-sites/identity/) must be enabled on your Netlify project. This happens automatically when running within a [Netlify Agent Runner](https://docs.netlify.com/agent-runner/overview/)
9
- - **Server-side** functions (`getUser`, `login`, `admin.*`, etc.) require [Netlify Functions](https://docs.netlify.com/build/functions/get-started/) (modern/v2, with `export default`) or [Edge Functions](https://docs.netlify.com/edge-functions/overview/). [Lambda-compatible functions](https://docs.netlify.com/build/functions/lambda-compatibility/) (v1, with `export { handler }`) are **not supported**
10
- - For local development, use [`netlify dev`](https://docs.netlify.com/cli/local-development/) so the Identity endpoint is available
10
+ - [Netlify Identity](https://docs.netlify.com/security/secure-access-to-sites/identity/) must be enabled on your Netlify
11
+ project. This happens automatically when running within a
12
+ [Netlify Agent Runner](https://docs.netlify.com/agent-runner/overview/)
13
+ - **Server-side** functions (`getUser`, `login`, `admin.*`, etc.) require
14
+ [Netlify Functions](https://docs.netlify.com/build/functions/get-started/) (modern/v2, with `export default`) or
15
+ [Edge Functions](https://docs.netlify.com/edge-functions/overview/).
16
+ [Lambda-compatible functions](https://docs.netlify.com/build/functions/lambda-compatibility/) (v1, with
17
+ `export { handler }`) are **not supported**
18
+ - For local development, use [`netlify dev`](https://docs.netlify.com/cli/local-development/) so the Identity endpoint
19
+ is available
11
20
 
12
21
  ## How this library relates to other Netlify auth packages
13
22
 
14
- `@netlify/identity` is the recommended library for all new projects. It works in both browser and server contexts, handles cookie management, and normalizes the user object.
23
+ `@netlify/identity` is the recommended library for all new projects. It works in both browser and server contexts,
24
+ handles cookie management, and normalizes the user object.
15
25
 
16
26
  You may encounter two older packages in existing code or documentation:
17
27
 
@@ -20,17 +30,22 @@ You may encounter two older packages in existing code or documentation:
20
30
  | [`netlify-identity-widget`](https://github.com/netlify/netlify-identity-widget) | Not recommended for new projects | Pre-built login/signup modal with built-in UI |
21
31
  | [`gotrue-js`](https://github.com/netlify/gotrue-js) | Not recommended for new projects | Low-level GoTrue HTTP client (browser only) |
22
32
 
23
- If you need a pre-built login UI, the widget still works. For everything else (custom UI, server-side auth, admin operations, framework integration), use `@netlify/identity`.
33
+ If you need a pre-built login UI, the widget still works. For everything else (custom UI, server-side auth, admin
34
+ operations, framework integration), use `@netlify/identity`.
24
35
 
25
36
  ## Table of contents
26
37
 
27
38
  - [Installation](#installation)
28
39
  - [Quick start](#quick-start)
29
40
  - [API](#api)
30
- - [Functions](#functions) -- `getUser`, `login`, `signup`, `logout`, `oauthLogin`, `handleAuthCallback`, `onAuthChange`, `hydrateSession`, `refreshSession`, and more
31
- - [Admin Operations](#admin-operations) -- `admin.listUsers`, `admin.getUser`, `admin.createUser`, `admin.updateUser`, `admin.deleteUser`
32
- - [Types](#types) -- `User`, `AuthEvent`, `CallbackResult`, `Settings`, `Admin`, `ListUsersOptions`, `CreateUserParams`, etc.
41
+ - [Functions](#functions) -- `getUser`, `login`, `signup`, `logout`, `oauthLogin`, `handleAuthCallback`,
42
+ `onAuthChange`, `hydrateSession`, `refreshSession`, `verifyRequestOrigin`, and more
43
+ - [Admin Operations](#admin-operations) -- `admin.listUsers`, `admin.getUser`, `admin.createUser`, `admin.updateUser`,
44
+ `admin.deleteUser`
45
+ - [Types](#types) -- `User`, `AuthEvent`, `CallbackResult`, `Settings`, `Admin`, `ListUsersOptions`,
46
+ `CreateUserParams`, `VerifyRequestOriginOptions`, etc.
33
47
  - [Errors](#errors) -- `AuthError`, `MissingIdentityError`
48
+ - [Security: CSRF protection](#security-csrf-protection)
34
49
  - [Framework integration](#framework-integration) -- Next.js, Remix, TanStack Start, Astro, SvelteKit
35
50
  - [Guides](#guides)
36
51
  - [React `useAuth` hook](#react-useauth-hook)
@@ -98,9 +113,16 @@ export default async (req: Request, context: Context) => {
98
113
  getUser(): Promise<User | null>
99
114
  ```
100
115
 
101
- Returns the current authenticated user, or `null` if not logged in. Returns the best available normalized `User` from the current context. When the Identity API is reachable, most persisted and profile fields are populated, but state-dependent fields (invite, recovery, email-change) may still be `undefined` if the user is not in that state. When falling back to JWT claims (e.g., Identity API unreachable), only `id`, `email`, `provider`, `name`, `pictureUrl`, `roles`, `userMetadata`, and `appMetadata` are available. Never throws.
116
+ Returns the current authenticated user, or `null` if not logged in. Returns the best available normalized `User` from
117
+ the current context. When the Identity API is reachable, most persisted and profile fields are populated, but
118
+ state-dependent fields (invite, recovery, email-change) may still be `undefined` if the user is not in that state. When
119
+ falling back to JWT claims (e.g., Identity API unreachable), only `id`, `email`, `provider`, `name`, `pictureUrl`,
120
+ `roles`, `userMetadata`, and `appMetadata` are available. Never throws.
102
121
 
103
- > **Next.js note:** Calling `getUser()` in a Server Component opts the page into [dynamic rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-rendering) because it reads cookies. This is expected and correct for authenticated pages. Next.js handles the internal dynamic rendering signal automatically.
122
+ > **Next.js note:** Calling `getUser()` in a Server Component opts the page into
123
+ > [dynamic rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-rendering)
124
+ > because it reads cookies. This is expected and correct for authenticated pages. Next.js handles the internal dynamic
125
+ > rendering signal automatically.
104
126
 
105
127
  #### `isAuthenticated`
106
128
 
@@ -116,7 +138,8 @@ Returns `true` if a user is currently authenticated. Equivalent to `(await getUs
116
138
  getIdentityConfig(): IdentityConfig | null
117
139
  ```
118
140
 
119
- Returns the Identity endpoint URL (and operator token on the server), or `null` if Identity is not available. Never throws.
141
+ Returns the Identity endpoint URL (and operator token on the server), or `null` if Identity is not available. Never
142
+ throws.
120
143
 
121
144
  #### `getSettings`
122
145
 
@@ -136,9 +159,11 @@ login(email: string, password: string): Promise<User>
136
159
 
137
160
  Logs in with email and password. Works in both browser and server contexts.
138
161
 
139
- In the browser, emits a `'login'` event. On the server (Netlify Functions, Edge Functions), calls the Identity API directly and sets the `nf_jwt` cookie via the Netlify runtime.
162
+ In the browser, emits a `'login'` event. On the server (Netlify Functions, Edge Functions), calls the Identity API
163
+ directly and sets the `nf_jwt` cookie via the Netlify runtime.
140
164
 
141
- **Throws:** `AuthError` on invalid credentials or network failure. In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
165
+ **Throws:** `AuthError` on invalid credentials or network failure. In the browser, `MissingIdentityError` if Identity is
166
+ not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
142
167
 
143
168
  #### `signup`
144
169
 
@@ -148,11 +173,16 @@ signup(email: string, password: string, data?: SignupData): Promise<User>
148
173
 
149
174
  Creates a new account. Works in both browser and server contexts.
150
175
 
151
- If autoconfirm is enabled in your Identity settings, the user is logged in immediately: cookies are set and a `'login'` event is emitted. If autoconfirm is **disabled** (the default), the user receives a confirmation email and must click the link before they can log in. In that case, no cookies are set and no auth event is emitted.
176
+ If autoconfirm is enabled in your Identity settings, the user is logged in immediately: cookies are set and a `'login'`
177
+ event is emitted. If autoconfirm is **disabled** (the default), the user receives a confirmation email and must click
178
+ the link before they can log in. In that case, no cookies are set and no auth event is emitted.
152
179
 
153
- The optional `data` parameter sets user metadata (e.g., `{ full_name: 'Jane Doe' }`), stored in the user's `user_metadata` field.
180
+ The optional `data` parameter sets user metadata (e.g., `{ full_name: 'Jane Doe' }`), stored in the user's
181
+ `user_metadata` field.
154
182
 
155
- **Throws:** `AuthError` on failure (e.g., email already registered, signup disabled). In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
183
+ **Throws:** `AuthError` on failure (e.g., email already registered, signup disabled). In the browser,
184
+ `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not
185
+ available.
156
186
 
157
187
  #### `logout`
158
188
 
@@ -162,9 +192,11 @@ logout(): Promise<void>
162
192
 
163
193
  Logs out the current user and clears the session. Works in both browser and server contexts.
164
194
 
165
- In the browser, emits a `'logout'` event. On the server, calls the Identity `/logout` endpoint with the JWT from the `nf_jwt` cookie, then deletes the cookie. Auth cookies are always cleared, even if the server call fails.
195
+ In the browser, emits a `'logout'` event. On the server, calls the Identity `/logout` endpoint with the JWT from the
196
+ `nf_jwt` cookie, then deletes the cookie. Auth cookies are always cleared, even if the server call fails.
166
197
 
167
- **Throws:** In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
198
+ **Throws:** In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the
199
+ Netlify Functions runtime is not available.
168
200
 
169
201
  #### `oauthLogin`
170
202
 
@@ -174,7 +206,8 @@ oauthLogin(provider: string): never
174
206
 
175
207
  Redirects to an OAuth provider. The page navigates away, so this function never returns normally. Browser only.
176
208
 
177
- The `provider` argument should be one of the `AuthProvider` values: `'google'`, `'github'`, `'gitlab'`, `'bitbucket'`, or `'facebook'`.
209
+ The `provider` argument should be one of the `AuthProvider` values: `'google'`, `'github'`, `'gitlab'`, `'bitbucket'`,
210
+ or `'facebook'`.
178
211
 
179
212
  **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if called on the server.
180
213
 
@@ -184,7 +217,8 @@ The `provider` argument should be one of the `AuthProvider` values: `'google'`,
184
217
  handleAuthCallback(): Promise<CallbackResult | null>
185
218
  ```
186
219
 
187
- Processes the URL hash after an OAuth redirect, email confirmation, password recovery, invite acceptance, or email change. Call on page load. Returns `null` if the hash contains no auth parameters. Browser only.
220
+ Processes the URL hash after an OAuth redirect, email confirmation, password recovery, invite acceptance, or email
221
+ change. Call on page load. Returns `null` if the hash contains no auth parameters. Browser only.
188
222
 
189
223
  **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if token exchange fails.
190
224
 
@@ -194,7 +228,9 @@ Processes the URL hash after an OAuth redirect, email confirmation, password rec
194
228
  onAuthChange(callback: AuthCallback): () => void
195
229
  ```
196
230
 
197
- Subscribes to auth state changes (login, logout, token refresh, user updates, and recovery). Returns an unsubscribe function. Also fires on cross-tab session changes. No-op on the server. The `'recovery'` event fires when `handleAuthCallback()` processes a password recovery token; listen for it to redirect users to a password reset form.
231
+ Subscribes to auth state changes (login, logout, token refresh, user updates, and recovery). Returns an unsubscribe
232
+ function. Also fires on cross-tab session changes. No-op on the server. The `'recovery'` event fires when
233
+ `handleAuthCallback()` processes a password recovery token; listen for it to redirect users to a password reset form.
198
234
 
199
235
  #### `hydrateSession`
200
236
 
@@ -202,9 +238,13 @@ Subscribes to auth state changes (login, logout, token refresh, user updates, an
202
238
  hydrateSession(): Promise<User | null>
203
239
  ```
204
240
 
205
- Bootstraps the browser-side session from server-set auth cookies (`nf_jwt`, `nf_refresh`). Returns the hydrated `User`, or `null` if no auth cookies are present. No-op on the server.
241
+ Bootstraps the browser-side session from server-set auth cookies (`nf_jwt`, `nf_refresh`). Returns the hydrated `User`,
242
+ or `null` if no auth cookies are present. No-op on the server.
206
243
 
207
- **When to use:** After a server-side login (e.g., via a Netlify Function or Server Action), the `nf_jwt` cookie is set but no browser session exists yet. `getUser()` calls `hydrateSession()` automatically, but account operations like `updateUser()` or `verifyEmailChange()` require a live browser session. Call `hydrateSession()` explicitly if you need the session ready before calling those operations.
244
+ **When to use:** After a server-side login (e.g., via a Netlify Function or Server Action), the `nf_jwt` cookie is set
245
+ but no browser session exists yet. `getUser()` calls `hydrateSession()` automatically, but account operations like
246
+ `updateUser()` or `verifyEmailChange()` require a live browser session. Call `hydrateSession()` explicitly if you need
247
+ the session ready before calling those operations.
208
248
 
209
249
  If a browser session already exists (e.g., from a browser-side login), this is a no-op and returns the existing user.
210
250
 
@@ -224,13 +264,22 @@ await updateUser({ data: { full_name: 'Jane Doe' } })
224
264
  refreshSession(): Promise<string | null>
225
265
  ```
226
266
 
227
- Refreshes an expired or near-expired session. Returns the new access token on success, or `null` if no refresh is needed or the refresh token is invalid/missing.
267
+ Refreshes an expired or near-expired session. Returns the new access token on success, or `null` if no refresh is needed
268
+ or the refresh token is invalid/missing.
228
269
 
229
- **Browser:** Checks if the current access token is near expiry and refreshes it if needed, syncing the new token to the `nf_jwt` cookie. Note: the library automatically refreshes tokens in the background after any browser flow that establishes a session (`login()`, `signup()`, `hydrateSession()`, `handleAuthCallback()`, `confirmEmail()`, `recoverPassword()`, `acceptInvite()`), so you typically don't need to call this manually. `getUser()` also restarts the refresh timer when it finds an existing session. Browser-side errors return `null`, not an `AuthError`.
270
+ **Browser:** Checks if the current access token is near expiry and refreshes it if needed, syncing the new token to the
271
+ `nf_jwt` cookie. Note: the library automatically refreshes tokens in the background after any browser flow that
272
+ establishes a session (`login()`, `signup()`, `hydrateSession()`, `handleAuthCallback()`, `confirmEmail()`,
273
+ `recoverPassword()`, `acceptInvite()`), so you typically don't need to call this manually. `getUser()` also restarts the
274
+ refresh timer when it finds an existing session. Browser-side errors return `null`, not an `AuthError`.
230
275
 
231
- **Server:** Reads the `nf_jwt` and `nf_refresh` cookies. If the access token is expired or within 60 seconds of expiry, exchanges the refresh token for a new access token via the Identity `/token` endpoint and updates both cookies on the response. Call this in framework middleware or at the start of server-side request handlers to ensure the JWT is valid for downstream processing.
276
+ **Server:** Reads the `nf_jwt` and `nf_refresh` cookies. If the access token is expired or within 60 seconds of expiry,
277
+ exchanges the refresh token for a new access token via the Identity `/token` endpoint and updates both cookies on the
278
+ response. Call this in framework middleware or at the start of server-side request handlers to ensure the JWT is valid
279
+ for downstream processing.
232
280
 
233
- **Throws:** `AuthError` on network failure or if the Identity endpoint URL cannot be determined. Does **not** throw for invalid/expired refresh tokens (returns `null` instead).
281
+ **Throws:** `AuthError` on network failure or if the Identity endpoint URL cannot be determined. Does **not** throw for
282
+ invalid/expired refresh tokens (returns `null` instead).
234
283
 
235
284
  ```ts
236
285
  // Example: Astro middleware
@@ -242,6 +291,24 @@ export async function onRequest(context, next) {
242
291
  }
243
292
  ```
244
293
 
294
+ #### `verifyRequestOrigin`
295
+
296
+ ```ts
297
+ verifyRequestOrigin(request: Request, options?: VerifyRequestOriginOptions): void
298
+ ```
299
+
300
+ CSRF protection helper for server-side endpoints that call `login()`, `signup()`, or `logout()`. Compares the request's
301
+ `Origin` header against the request's own origin (or an explicit allowlist via `options.allowedOrigins`) and throws if
302
+ they don't match. Server-only.
303
+
304
+ The check runs unconditionally on every call: any HTTP method, with or without an `Origin` header. If you don't want the
305
+ check on a particular route, don't call the helper there.
306
+
307
+ **Throws:** `AuthError` with status `403` when the request has no `Origin` header. `AuthError` with status `403` when
308
+ the request's `Origin` is not in the allowed origins.
309
+
310
+ See [Security: CSRF protection](#security-csrf-protection) for the full threat model and per-framework guidance.
311
+
245
312
  #### `requestPasswordRecovery`
246
313
 
247
314
  ```ts
@@ -298,13 +365,16 @@ Redeems a recovery token and sets a new password. Logs the user in on success.
298
365
  updateUser(updates: UserUpdates): Promise<User>
299
366
  ```
300
367
 
301
- Updates the current user's metadata or credentials. Requires an active session. Pass `email` or `password` to change credentials, or `data` to update user metadata (e.g., `{ data: { full_name: 'New Name' } }`).
368
+ Updates the current user's metadata or credentials. Requires an active session. Pass `email` or `password` to change
369
+ credentials, or `data` to update user metadata (e.g., `{ data: { full_name: 'New Name' } }`).
302
370
 
303
- **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if no user is logged in, or the update fails.
371
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if no user is logged in, or the update
372
+ fails.
304
373
 
305
374
  ### Admin Operations
306
375
 
307
- The `admin` namespace provides server-only user management functions. Admin methods use the operator token from the Netlify runtime, which is automatically available in Netlify Functions and Edge Functions.
376
+ The `admin` namespace provides server-only user management functions. Admin methods use the operator token from the
377
+ Netlify runtime, which is automatically available in Netlify Functions and Edge Functions.
308
378
 
309
379
  Calling any admin method from a browser environment throws an `AuthError`.
310
380
 
@@ -362,7 +432,9 @@ Gets a single user by ID.
362
432
  admin.createUser(params: CreateUserParams): Promise<User>
363
433
  ```
364
434
 
365
- Creates a new user. The user is auto-confirmed. Optional `data` forwards allowed fields (`role`, `app_metadata`, `user_metadata`) to the request body. Other keys are silently ignored. `data` cannot override `email`, `password`, or `confirm`.
435
+ Creates a new user. The user is auto-confirmed. Optional `data` forwards allowed fields (`role`, `app_metadata`,
436
+ `user_metadata`) to the request body. Other keys are silently ignored. `data` cannot override `email`, `password`, or
437
+ `confirm`.
366
438
 
367
439
  **Throws:** `AuthError` if called from a browser, the email already exists, or the operator token is missing.
368
440
 
@@ -372,7 +444,8 @@ Creates a new user. The user is auto-confirmed. Optional `data` forwards allowed
372
444
  admin.updateUser(userId: string, attributes: AdminUserUpdates): Promise<User>
373
445
  ```
374
446
 
375
- Updates an existing user by ID. Only typed `AdminUserUpdates` fields are forwarded (e.g., `{ email: 'new@example.com' }`, `{ role: 'editor' }`).
447
+ Updates an existing user by ID. Only typed `AdminUserUpdates` fields are forwarded (e.g.,
448
+ `{ email: 'new@example.com' }`, `{ role: 'editor' }`).
376
449
 
377
450
  **Throws:** `AuthError` if called from a browser, the user is not found, or the update fails.
378
451
 
@@ -464,7 +537,8 @@ interface AdminUserUpdates {
464
537
  }
465
538
  ```
466
539
 
467
- Fields accepted by `admin.updateUser()`. Unlike `UserUpdates`, admin updates can set `role`, force-confirm a user, and write to `app_metadata`. Only these typed fields are forwarded.
540
+ Fields accepted by `admin.updateUser()`. Unlike `UserUpdates`, admin updates can set `role`, force-confirm a user, and
541
+ write to `app_metadata`. Only these typed fields are forwarded.
468
542
 
469
543
  #### `SignupData`
470
544
 
@@ -505,7 +579,8 @@ interface CreateUserParams {
505
579
  }
506
580
  ```
507
581
 
508
- Parameters for `admin.createUser()`. Optional `data` forwards allowed fields (`role`, `app_metadata`, `user_metadata`) to the request body. Other keys are silently ignored.
582
+ Parameters for `admin.createUser()`. Optional `data` forwards allowed fields (`role`, `app_metadata`, `user_metadata`)
583
+ to the request body. Other keys are silently ignored.
509
584
 
510
585
  #### `Admin`
511
586
 
@@ -565,9 +640,23 @@ interface CallbackResult {
565
640
  }
566
641
  ```
567
642
 
568
- The `token` field is only present for `invite` callbacks, where the user hasn't set a password yet. Pass `token` to `acceptInvite(token, password)` to finish.
643
+ The `token` field is only present for `invite` callbacks, where the user hasn't set a password yet. Pass `token` to
644
+ `acceptInvite(token, password)` to finish.
645
+
646
+ For all other types (`oauth`, `confirmation`, `recovery`, `email_change`), the user is logged in directly and `token` is
647
+ not set.
648
+
649
+ #### `VerifyRequestOriginOptions`
569
650
 
570
- For all other types (`oauth`, `confirmation`, `recovery`, `email_change`), the user is logged in directly and `token` is not set.
651
+ ```ts
652
+ interface VerifyRequestOriginOptions {
653
+ allowedOrigins?: string[]
654
+ }
655
+ ```
656
+
657
+ Options for [`verifyRequestOrigin`](#verifyrequestorigin). When `allowedOrigins` is set, the list replaces the default
658
+ same-origin check, so include the request's own origin if you still want it allowed. Each value is a full origin string
659
+ with scheme and host (`'https://example.com'`).
571
660
 
572
661
  ### Errors
573
662
 
@@ -588,6 +677,59 @@ class MissingIdentityError extends Error {}
588
677
 
589
678
  Thrown when Identity is not configured in the current environment.
590
679
 
680
+ ## Security: CSRF protection
681
+
682
+ If you expose server-side `login()`, `signup()`, or `logout()` through an HTTP endpoint, that endpoint needs Cross-Site
683
+ Request Forgery (CSRF) protection. The library cannot enforce this itself because it only sees the email and password
684
+ arguments handed to it, not the incoming request.
685
+
686
+ **Why it matters.** A specific flavor called _login CSRF_ lets an attacker trick a victim's browser into logging into
687
+ the attacker's account. The victim then performs actions inside that session (saving payment info, linking third-party
688
+ services, uploading content), and the attacker harvests the result later by signing in with the credentials they always
689
+ controlled. `SameSite=Lax` cookies do not catch this attack because the session is being created on the victim's
690
+ browser, not ridden from an existing one.
691
+
692
+ ### `verifyRequestOrigin`
693
+
694
+ `verifyRequestOrigin(request, options?)` compares the request's `Origin` header against the request's own origin (or an
695
+ explicit allowlist) and throws `AuthError` with status 403 on mismatch. Call it at the start of any handler that
696
+ performs an auth mutation.
697
+
698
+ ```ts
699
+ // netlify/functions/login.ts
700
+ import { login, verifyRequestOrigin } from '@netlify/identity'
701
+ import type { Context } from '@netlify/functions'
702
+
703
+ export default async (req: Request, context: Context) => {
704
+ verifyRequestOrigin(req)
705
+ const { email, password } = await req.json()
706
+ await login(email, password)
707
+ return new Response(null, { status: 302, headers: { Location: '/dashboard' } })
708
+ }
709
+ ```
710
+
711
+ The helper runs unconditionally on every call. It checks any HTTP method, with or without an `Origin` header. If you
712
+ don't want the check on a particular route, don't call the helper there.
713
+
714
+ ### Custom allowed origins
715
+
716
+ By default, the helper accepts only the request's own origin. Pass `allowedOrigins` to allow additional trusted origins
717
+ (for example, a separate frontend domain that POSTs to an API on another domain). The list replaces the default, so
718
+ include the request's own origin if you still want it allowed:
719
+
720
+ ```ts
721
+ verifyRequestOrigin(req, {
722
+ allowedOrigins: ['https://app.example.com', 'https://www.example.com'],
723
+ })
724
+ ```
725
+
726
+ ### When to call the helper
727
+
728
+ Some frameworks check the request's `Origin` on state-changing requests by default; others don't. Check your framework's
729
+ documentation. If same-origin enforcement is already on by default for the endpoint where you invoke `login()` /
730
+ `signup()` / `logout()`, calling `verifyRequestOrigin` yourself is redundant. If it isn't, call
731
+ `verifyRequestOrigin(request)` at the start of the handler before invoking the auth function.
732
+
591
733
  ## Framework integration
592
734
 
593
735
  ### Recommended pattern for SSR frameworks
@@ -597,9 +739,13 @@ For SSR frameworks (Next.js, Remix, Astro, TanStack Start), the recommended patt
597
739
  - **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
598
740
  - **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
599
741
 
600
- Browser-side auth mutations call the Identity API directly from the browser, set the `nf_jwt` cookie, and emit `onAuthChange` events. This keeps the client UI in sync immediately. Server-side reads work because the cookie is sent with every request.
742
+ Browser-side auth mutations call the Identity API directly from the browser, set the `nf_jwt` cookie, and emit
743
+ `onAuthChange` events. This keeps the client UI in sync immediately. Server-side reads work because the cookie is sent
744
+ with every request.
601
745
 
602
- The library also supports server-side mutations (`login()`, `signup()`, `logout()` inside Netlify Functions), but these require the Netlify Functions runtime to set cookies. After a server-side mutation, you need a full page navigation so the browser sends the new cookie.
746
+ The library also supports server-side mutations (`login()`, `signup()`, `logout()` inside Netlify Functions), but these
747
+ require the Netlify Functions runtime to set cookies. After a server-side mutation, you need a full page navigation so
748
+ the browser sends the new cookie.
603
749
 
604
750
  ### Next.js (App Router)
605
751
 
@@ -653,7 +799,10 @@ export default async function Dashboard() {
653
799
  }
654
800
  ```
655
801
 
656
- Use `window.location.href` instead of Next.js `redirect()` after server-side auth mutations. Next.js `redirect()` triggers a soft navigation via the Router, which may not include the newly-set auth cookie. A full page load ensures the cookie is sent and the server sees the updated auth state. Reading auth state with `getUser()` in Server Components works normally, and `redirect()` is fine for auth gates (where no cookie was just set).
802
+ Use `window.location.href` instead of Next.js `redirect()` after server-side auth mutations. Next.js `redirect()`
803
+ triggers a soft navigation via the Router, which may not include the newly-set auth cookie. A full page load ensures the
804
+ cookie is sent and the server sees the updated auth state. Reading auth state with `getUser()` in Server Components
805
+ works normally, and `redirect()` is fine for auth gates (where no cookie was just set).
657
806
 
658
807
  ### Remix
659
808
 
@@ -661,11 +810,12 @@ Use `window.location.href` instead of Next.js `redirect()` after server-side aut
661
810
 
662
811
  ```tsx
663
812
  // app/routes/login.tsx
664
- import { login } from '@netlify/identity'
813
+ import { login, verifyRequestOrigin } from '@netlify/identity'
665
814
  import { redirect, json } from '@remix-run/node'
666
815
  import type { ActionFunctionArgs } from '@remix-run/node'
667
816
 
668
817
  export async function action({ request }: ActionFunctionArgs) {
818
+ verifyRequestOrigin(request)
669
819
  const formData = await request.formData()
670
820
  const email = formData.get('email') as string
671
821
  const password = formData.get('password') as string
@@ -691,7 +841,13 @@ export async function loader() {
691
841
  }
692
842
  ```
693
843
 
694
- Remix `redirect()` works after server-side `login()` because Remix actions return real HTTP responses. The browser receives a 302 with the `Set-Cookie` header already applied, so the next request includes the auth cookie. This is different from Next.js, where `redirect()` in a Server Action triggers a client-side (soft) navigation that may not include newly-set cookies.
844
+ Remix `redirect()` works after server-side `login()` because Remix actions return real HTTP responses. The browser
845
+ receives a 302 with the `Set-Cookie` header already applied, so the next request includes the auth cookie. This is
846
+ different from Next.js, where `redirect()` in a Server Action triggers a client-side (soft) navigation that may not
847
+ include newly-set cookies.
848
+
849
+ > The example calls [`verifyRequestOrigin`](#verifyrequestorigin) at the top of the action. See
850
+ > [Security: CSRF protection](#security-csrf-protection) for when this is needed.
695
851
 
696
852
  ### TanStack Start
697
853
 
@@ -758,7 +914,8 @@ function Dashboard() {
758
914
  }
759
915
  ```
760
916
 
761
- Use `window.location.href` instead of TanStack Router's `navigate()` after auth changes. This ensures the browser sends the updated cookie on the next request.
917
+ Use `window.location.href` instead of TanStack Router's `navigate()` after auth changes. This ensures the browser sends
918
+ the updated cookie on the next request.
762
919
 
763
920
  ### Astro (SSR)
764
921
 
@@ -840,7 +997,9 @@ export async function load() {
840
997
 
841
998
  ### Handling OAuth callbacks in SPAs
842
999
 
843
- All SPA frameworks need a callback handler that runs on page load to process OAuth redirects, email confirmations, and password recovery tokens. Use a **wrapper component** that blocks page content while processing tokens. This prevents a flash of unauthenticated content that occurs when the page renders before the callback completes.
1000
+ All SPA frameworks need a callback handler that runs on page load to process OAuth redirects, email confirmations, and
1001
+ password recovery tokens. Use a **wrapper component** that blocks page content while processing tokens. This prevents a
1002
+ flash of unauthenticated content that occurs when the page renders before the callback completes.
844
1003
 
845
1004
  ```tsx
846
1005
  // React component (works with Next.js, Remix, TanStack Start)
@@ -893,7 +1052,8 @@ Wrap your page content with this component in your **root layout** so it runs on
893
1052
  </CallbackHandler>
894
1053
  ```
895
1054
 
896
- If you only mount it on a `/callback` route, OAuth redirects and email confirmation links that land on other pages will not be processed.
1055
+ If you only mount it on a `/callback` route, OAuth redirects and email confirmation links that land on other pages will
1056
+ not be processed.
897
1057
 
898
1058
  ## Guides
899
1059
 
@@ -927,7 +1087,8 @@ function NavBar() {
927
1087
 
928
1088
  ### Listening for auth changes
929
1089
 
930
- Use `onAuthChange` to keep your UI in sync with auth state. It fires on login, logout, token refresh, user updates, and recovery. It also detects session changes in other browser tabs (via `localStorage`).
1090
+ Use `onAuthChange` to keep your UI in sync with auth state. It fires on login, logout, token refresh, user updates, and
1091
+ recovery. It also detects session changes in other browser tabs (via `localStorage`).
931
1092
 
932
1093
  ```ts
933
1094
  import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
@@ -982,11 +1143,15 @@ if (result?.type === 'oauth') {
982
1143
  }
983
1144
  ```
984
1145
 
985
- `handleAuthCallback()` exchanges the token in the URL hash, logs the user in, clears the hash, and emits an auth event via `onAuthChange` (`'login'` for OAuth/confirmation, `'recovery'` for password recovery).
1146
+ `handleAuthCallback()` exchanges the token in the URL hash, logs the user in, clears the hash, and emits an auth event
1147
+ via `onAuthChange` (`'login'` for OAuth/confirmation, `'recovery'` for password recovery).
986
1148
 
987
1149
  ### Password recovery
988
1150
 
989
- Password recovery is a two-step flow. The library handles the token exchange automatically via `handleAuthCallback()`, which logs the user in and returns `{type: 'recovery', user}`. A `'recovery'` event (not `'login'`) is emitted via `onAuthChange`, so event-based listeners can also detect this flow. You then show a "set new password" form and call `updateUser()` to save it.
1151
+ Password recovery is a two-step flow. The library handles the token exchange automatically via `handleAuthCallback()`,
1152
+ which logs the user in and returns `{type: 'recovery', user}`. A `'recovery'` event (not `'login'`) is emitted via
1153
+ `onAuthChange`, so event-based listeners can also detect this flow. You then show a "set new password" form and call
1154
+ `updateUser()` to save it.
990
1155
 
991
1156
  **Step by step:**
992
1157
 
@@ -1022,7 +1187,9 @@ onAuthChange((event, user) => {
1022
1187
 
1023
1188
  ### Invite acceptance
1024
1189
 
1025
- When an admin invites a user, they receive an email with an invite link. Clicking it redirects to your site with an `invite_token` in the URL hash. Unlike other callback types, the user is not logged in automatically because they need to set a password first.
1190
+ When an admin invites a user, they receive an email with an invite link. Clicking it redirects to your site with an
1191
+ `invite_token` in the URL hash. Unlike other callback types, the user is not logged in automatically because they need
1192
+ to set a password first.
1026
1193
 
1027
1194
  **Step by step:**
1028
1195
 
@@ -1048,25 +1215,46 @@ Sessions are managed by Netlify Identity on the server side. The library stores
1048
1215
  - **`nf_jwt`**: A short-lived JWT access token (default: 1 hour).
1049
1216
  - **`nf_refresh`**: A long-lived refresh token used to obtain new access tokens without re-authenticating.
1050
1217
 
1051
- **Browser auto-refresh:** After any session-establishing flow (`login()`, `signup()`, `hydrateSession()`, `handleAuthCallback()`, `confirmEmail()`, `recoverPassword()`, `acceptInvite()`), the library automatically schedules a background refresh 60 seconds before the access token expires. `getUser()` also restarts the refresh timer when it finds an existing session (e.g., after a page reload). When the refresh fires, it obtains a new access token, syncs it to the `nf_jwt` cookie, and emits a `TOKEN_REFRESH` event. This keeps the cookie fresh as long as the user has the tab open. If the refresh fails (e.g., the refresh token was revoked), the timer stops and the user will need to log in again.
1218
+ **Browser auto-refresh:** After any session-establishing flow (`login()`, `signup()`, `hydrateSession()`,
1219
+ `handleAuthCallback()`, `confirmEmail()`, `recoverPassword()`, `acceptInvite()`), the library automatically schedules a
1220
+ background refresh 60 seconds before the access token expires. `getUser()` also restarts the refresh timer when it finds
1221
+ an existing session (e.g., after a page reload). When the refresh fires, it obtains a new access token, syncs it to the
1222
+ `nf_jwt` cookie, and emits a `TOKEN_REFRESH` event. This keeps the cookie fresh as long as the user has the tab open. If
1223
+ the refresh fails (e.g., the refresh token was revoked), the timer stops and the user will need to log in again.
1052
1224
 
1053
- **Server-side refresh:** On the server, the access token in the `nf_jwt` cookie is validated as-is. If it has expired and no refresh happens, `getUser()` returns `null`. To handle this, call `refreshSession()` in your framework middleware or request handler. This checks if the token is near expiry, exchanges the refresh token for a new one, and updates the cookies on the response.
1225
+ **Server-side refresh:** On the server, the access token in the `nf_jwt` cookie is validated as-is. If it has expired
1226
+ and no refresh happens, `getUser()` returns `null`. To handle this, call `refreshSession()` in your framework middleware
1227
+ or request handler. This checks if the token is near expiry, exchanges the refresh token for a new one, and updates the
1228
+ cookies on the response.
1054
1229
 
1055
1230
  Session lifetime is configured in your Netlify Identity settings, not in this library.
1056
1231
 
1057
1232
  ### Caching and authenticated content
1058
1233
 
1059
- Pages that display user-specific data (names, emails, roles, account settings) should not be served from a shared cache. If a cache stores an authenticated response and serves it to a different user, that user sees someone else's data. This applies to any authentication system, not just Netlify Identity.
1234
+ Pages that display user-specific data (names, emails, roles, account settings) should not be served from a shared cache.
1235
+ If a cache stores an authenticated response and serves it to a different user, that user sees someone else's data. This
1236
+ applies to any authentication system, not just Netlify Identity.
1060
1237
 
1061
1238
  **Next.js App Router** has multiple caching layers that are active by default:
1062
1239
 
1063
- - **Static rendering:** Server Components are statically rendered at build time unless they call a [Dynamic API](https://nextjs.org/docs/app/guides/caching#dynamic-rendering) like `cookies()`. This library's `getUser()` already calls `headers()` internally to opt the route into dynamic rendering, but if you check auth state without calling `getUser()` (e.g., reading the `nf_jwt` cookie directly), the page may still be statically cached. Always use `getUser()` rather than reading cookies directly.
1064
- - **ISR (Incremental Static Regeneration):** Do not use ISR for pages that display user-specific content. ISR regenerates the page for the first visitor after the revalidation window and caches the result for all subsequent visitors.
1065
- - **`use cache` / `unstable_cache`:** These directives cannot access `cookies()` or `headers()` directly. If you need to cache part of an authenticated page, read cookies outside the cache scope and pass relevant values as arguments.
1066
-
1067
- > **Note:** Next.js caching defaults have changed across versions. For example, [Next.js 15 changed `fetch` requests, `GET` Route Handlers, and the client Router Cache to be uncached by default](https://nextjs.org/blog/next-15#caching-semantics), reversing the previous opt-out model. Check the [caching guide](https://nextjs.org/docs/app/guides/caching) for your specific Next.js version.
1068
-
1069
- **Other SSR frameworks (Remix, Astro, SvelteKit, TanStack Start):** These frameworks do not cache SSR responses by default. If you add caching headers to improve performance, exclude routes that call `getUser()` or read auth cookies.
1240
+ - **Static rendering:** Server Components are statically rendered at build time unless they call a
1241
+ [Dynamic API](https://nextjs.org/docs/app/guides/caching#dynamic-rendering) like `cookies()`. This library's
1242
+ `getUser()` already calls `headers()` internally to opt the route into dynamic rendering, but if you check auth state
1243
+ without calling `getUser()` (e.g., reading the `nf_jwt` cookie directly), the page may still be statically cached.
1244
+ Always use `getUser()` rather than reading cookies directly.
1245
+ - **ISR (Incremental Static Regeneration):** Do not use ISR for pages that display user-specific content. ISR
1246
+ regenerates the page for the first visitor after the revalidation window and caches the result for all subsequent
1247
+ visitors.
1248
+ - **`use cache` / `unstable_cache`:** These directives cannot access `cookies()` or `headers()` directly. If you need to
1249
+ cache part of an authenticated page, read cookies outside the cache scope and pass relevant values as arguments.
1250
+
1251
+ > **Note:** Next.js caching defaults have changed across versions. For example,
1252
+ > [Next.js 15 changed `fetch` requests, `GET` Route Handlers, and the client Router Cache to be uncached by default](https://nextjs.org/blog/next-15#caching-semantics),
1253
+ > reversing the previous opt-out model. Check the [caching guide](https://nextjs.org/docs/app/guides/caching) for your
1254
+ > specific Next.js version.
1255
+
1256
+ **Other SSR frameworks (Remix, Astro, SvelteKit, TanStack Start):** These frameworks do not cache SSR responses by
1257
+ default. If you add caching headers to improve performance, exclude routes that call `getUser()` or read auth cookies.
1070
1258
 
1071
1259
  ## License
1072
1260