@netlify/identity 1.1.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,16 +30,20 @@ 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`, `verifyRequestOrigin`, 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`, `VerifyRequestOriginOptions`, 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`
34
48
  - [Security: CSRF protection](#security-csrf-protection)
35
49
  - [Framework integration](#framework-integration) -- Next.js, Remix, TanStack Start, Astro, SvelteKit
@@ -99,9 +113,16 @@ export default async (req: Request, context: Context) => {
99
113
  getUser(): Promise<User | null>
100
114
  ```
101
115
 
102
- 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.
103
121
 
104
- > **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.
105
126
 
106
127
  #### `isAuthenticated`
107
128
 
@@ -117,7 +138,8 @@ Returns `true` if a user is currently authenticated. Equivalent to `(await getUs
117
138
  getIdentityConfig(): IdentityConfig | null
118
139
  ```
119
140
 
120
- 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.
121
143
 
122
144
  #### `getSettings`
123
145
 
@@ -137,9 +159,11 @@ login(email: string, password: string): Promise<User>
137
159
 
138
160
  Logs in with email and password. Works in both browser and server contexts.
139
161
 
140
- 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.
141
164
 
142
- **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.
143
167
 
144
168
  #### `signup`
145
169
 
@@ -149,11 +173,16 @@ signup(email: string, password: string, data?: SignupData): Promise<User>
149
173
 
150
174
  Creates a new account. Works in both browser and server contexts.
151
175
 
152
- 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.
153
179
 
154
- 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.
155
182
 
156
- **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.
157
186
 
158
187
  #### `logout`
159
188
 
@@ -163,9 +192,11 @@ logout(): Promise<void>
163
192
 
164
193
  Logs out the current user and clears the session. Works in both browser and server contexts.
165
194
 
166
- 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.
167
197
 
168
- **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.
169
200
 
170
201
  #### `oauthLogin`
171
202
 
@@ -175,7 +206,8 @@ oauthLogin(provider: string): never
175
206
 
176
207
  Redirects to an OAuth provider. The page navigates away, so this function never returns normally. Browser only.
177
208
 
178
- 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'`.
179
211
 
180
212
  **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if called on the server.
181
213
 
@@ -185,7 +217,8 @@ The `provider` argument should be one of the `AuthProvider` values: `'google'`,
185
217
  handleAuthCallback(): Promise<CallbackResult | null>
186
218
  ```
187
219
 
188
- 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.
189
222
 
190
223
  **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if token exchange fails.
191
224
 
@@ -195,7 +228,9 @@ Processes the URL hash after an OAuth redirect, email confirmation, password rec
195
228
  onAuthChange(callback: AuthCallback): () => void
196
229
  ```
197
230
 
198
- 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.
199
234
 
200
235
  #### `hydrateSession`
201
236
 
@@ -203,9 +238,13 @@ Subscribes to auth state changes (login, logout, token refresh, user updates, an
203
238
  hydrateSession(): Promise<User | null>
204
239
  ```
205
240
 
206
- 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.
207
243
 
208
- **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.
209
248
 
210
249
  If a browser session already exists (e.g., from a browser-side login), this is a no-op and returns the existing user.
211
250
 
@@ -225,13 +264,22 @@ await updateUser({ data: { full_name: 'Jane Doe' } })
225
264
  refreshSession(): Promise<string | null>
226
265
  ```
227
266
 
228
- 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.
229
269
 
230
- **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`.
231
275
 
232
- **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.
233
280
 
234
- **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).
235
283
 
236
284
  ```ts
237
285
  // Example: Astro middleware
@@ -249,11 +297,15 @@ export async function onRequest(context, next) {
249
297
  verifyRequestOrigin(request: Request, options?: VerifyRequestOriginOptions): void
250
298
  ```
251
299
 
252
- CSRF protection helper for server-side endpoints that call `login()`, `signup()`, or `logout()`. Compares the request's `Origin` header against the request's own origin (or an explicit allowlist via `options.allowedOrigins`) and throws if they don't match. Server-only.
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.
253
303
 
254
- The check runs unconditionally on every call: any HTTP method, with or without an `Origin` header. If you don't want the check on a particular route, don't call the helper there.
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.
255
306
 
256
- **Throws:** `AuthError` with status `403` when the request has no `Origin` header. `AuthError` with status `403` when the request's `Origin` is not in the allowed origins.
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.
257
309
 
258
310
  See [Security: CSRF protection](#security-csrf-protection) for the full threat model and per-framework guidance.
259
311
 
@@ -313,13 +365,16 @@ Redeems a recovery token and sets a new password. Logs the user in on success.
313
365
  updateUser(updates: UserUpdates): Promise<User>
314
366
  ```
315
367
 
316
- 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' } }`).
317
370
 
318
- **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.
319
373
 
320
374
  ### Admin Operations
321
375
 
322
- 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.
323
378
 
324
379
  Calling any admin method from a browser environment throws an `AuthError`.
325
380
 
@@ -377,7 +432,9 @@ Gets a single user by ID.
377
432
  admin.createUser(params: CreateUserParams): Promise<User>
378
433
  ```
379
434
 
380
- 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`.
381
438
 
382
439
  **Throws:** `AuthError` if called from a browser, the email already exists, or the operator token is missing.
383
440
 
@@ -387,7 +444,8 @@ Creates a new user. The user is auto-confirmed. Optional `data` forwards allowed
387
444
  admin.updateUser(userId: string, attributes: AdminUserUpdates): Promise<User>
388
445
  ```
389
446
 
390
- 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' }`).
391
449
 
392
450
  **Throws:** `AuthError` if called from a browser, the user is not found, or the update fails.
393
451
 
@@ -479,7 +537,8 @@ interface AdminUserUpdates {
479
537
  }
480
538
  ```
481
539
 
482
- 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.
483
542
 
484
543
  #### `SignupData`
485
544
 
@@ -520,7 +579,8 @@ interface CreateUserParams {
520
579
  }
521
580
  ```
522
581
 
523
- 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.
524
584
 
525
585
  #### `Admin`
526
586
 
@@ -580,9 +640,11 @@ interface CallbackResult {
580
640
  }
581
641
  ```
582
642
 
583
- 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.
584
645
 
585
- For all other types (`oauth`, `confirmation`, `recovery`, `email_change`), the user is logged in directly and `token` is not set.
646
+ For all other types (`oauth`, `confirmation`, `recovery`, `email_change`), the user is logged in directly and `token` is
647
+ not set.
586
648
 
587
649
  #### `VerifyRequestOriginOptions`
588
650
 
@@ -592,7 +654,9 @@ interface VerifyRequestOriginOptions {
592
654
  }
593
655
  ```
594
656
 
595
- Options for [`verifyRequestOrigin`](#verifyrequestorigin). When `allowedOrigins` is set, the list replaces the default same-origin check, so include the request's own origin if you still want it allowed. Each value is a full origin string with scheme and host (`'https://example.com'`).
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'`).
596
660
 
597
661
  ### Errors
598
662
 
@@ -615,13 +679,21 @@ Thrown when Identity is not configured in the current environment.
615
679
 
616
680
  ## Security: CSRF protection
617
681
 
618
- If you expose server-side `login()`, `signup()`, or `logout()` through an HTTP endpoint, that endpoint needs Cross-Site Request Forgery (CSRF) protection. The library cannot enforce this itself because it only sees the email and password arguments handed to it, not the incoming request.
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.
619
685
 
620
- **Why it matters.** A specific flavor called _login CSRF_ lets an attacker trick a victim's browser into logging into the attacker's account. The victim then performs actions inside that session (saving payment info, linking third-party services, uploading content), and the attacker harvests the result later by signing in with the credentials they always controlled. `SameSite=Lax` cookies do not catch this attack because the session is being created on the victim's browser, not ridden from an existing one.
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.
621
691
 
622
692
  ### `verifyRequestOrigin`
623
693
 
624
- `verifyRequestOrigin(request, options?)` compares the request's `Origin` header against the request's own origin (or an explicit allowlist) and throws `AuthError` with status 403 on mismatch. Call it at the start of any handler that performs an auth mutation.
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.
625
697
 
626
698
  ```ts
627
699
  // netlify/functions/login.ts
@@ -636,11 +708,14 @@ export default async (req: Request, context: Context) => {
636
708
  }
637
709
  ```
638
710
 
639
- The helper runs unconditionally on every call. It checks any HTTP method, with or without an `Origin` header. If you don't want the check on a particular route, don't call the helper there.
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.
640
713
 
641
714
  ### Custom allowed origins
642
715
 
643
- By default, the helper accepts only the request's own origin. Pass `allowedOrigins` to allow additional trusted origins (for example, a separate frontend domain that POSTs to an API on another domain). The list replaces the default, so include the request's own origin if you still want it allowed:
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:
644
719
 
645
720
  ```ts
646
721
  verifyRequestOrigin(req, {
@@ -650,7 +725,10 @@ verifyRequestOrigin(req, {
650
725
 
651
726
  ### When to call the helper
652
727
 
653
- Some frameworks check the request's `Origin` on state-changing requests by default; others don't. Check your framework's documentation. If same-origin enforcement is already on by default for the endpoint where you invoke `login()` / `signup()` / `logout()`, calling `verifyRequestOrigin` yourself is redundant. If it isn't, call `verifyRequestOrigin(request)` at the start of the handler before invoking the auth function.
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.
654
732
 
655
733
  ## Framework integration
656
734
 
@@ -661,9 +739,13 @@ For SSR frameworks (Next.js, Remix, Astro, TanStack Start), the recommended patt
661
739
  - **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
662
740
  - **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
663
741
 
664
- 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.
665
745
 
666
- 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.
667
749
 
668
750
  ### Next.js (App Router)
669
751
 
@@ -717,7 +799,10 @@ export default async function Dashboard() {
717
799
  }
718
800
  ```
719
801
 
720
- 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).
721
806
 
722
807
  ### Remix
723
808
 
@@ -756,9 +841,13 @@ export async function loader() {
756
841
  }
757
842
  ```
758
843
 
759
- 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.
760
848
 
761
- > The example calls [`verifyRequestOrigin`](#verifyrequestorigin) at the top of the action. See [Security: CSRF protection](#security-csrf-protection) for when this is needed.
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.
762
851
 
763
852
  ### TanStack Start
764
853
 
@@ -825,7 +914,8 @@ function Dashboard() {
825
914
  }
826
915
  ```
827
916
 
828
- 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.
829
919
 
830
920
  ### Astro (SSR)
831
921
 
@@ -907,7 +997,9 @@ export async function load() {
907
997
 
908
998
  ### Handling OAuth callbacks in SPAs
909
999
 
910
- 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.
911
1003
 
912
1004
  ```tsx
913
1005
  // React component (works with Next.js, Remix, TanStack Start)
@@ -960,7 +1052,8 @@ Wrap your page content with this component in your **root layout** so it runs on
960
1052
  </CallbackHandler>
961
1053
  ```
962
1054
 
963
- 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.
964
1057
 
965
1058
  ## Guides
966
1059
 
@@ -994,7 +1087,8 @@ function NavBar() {
994
1087
 
995
1088
  ### Listening for auth changes
996
1089
 
997
- 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`).
998
1092
 
999
1093
  ```ts
1000
1094
  import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
@@ -1049,11 +1143,15 @@ if (result?.type === 'oauth') {
1049
1143
  }
1050
1144
  ```
1051
1145
 
1052
- `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).
1053
1148
 
1054
1149
  ### Password recovery
1055
1150
 
1056
- 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.
1057
1155
 
1058
1156
  **Step by step:**
1059
1157
 
@@ -1089,7 +1187,9 @@ onAuthChange((event, user) => {
1089
1187
 
1090
1188
  ### Invite acceptance
1091
1189
 
1092
- 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.
1093
1193
 
1094
1194
  **Step by step:**
1095
1195
 
@@ -1115,25 +1215,46 @@ Sessions are managed by Netlify Identity on the server side. The library stores
1115
1215
  - **`nf_jwt`**: A short-lived JWT access token (default: 1 hour).
1116
1216
  - **`nf_refresh`**: A long-lived refresh token used to obtain new access tokens without re-authenticating.
1117
1217
 
1118
- **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.
1119
1224
 
1120
- **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.
1121
1229
 
1122
1230
  Session lifetime is configured in your Netlify Identity settings, not in this library.
1123
1231
 
1124
1232
  ### Caching and authenticated content
1125
1233
 
1126
- 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.
1127
1237
 
1128
1238
  **Next.js App Router** has multiple caching layers that are active by default:
1129
1239
 
1130
- - **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.
1131
- - **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.
1132
- - **`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.
1133
-
1134
- > **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.
1135
-
1136
- **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.
1137
1258
 
1138
1259
  ## License
1139
1260