@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 +191 -70
- package/dist/{index.cjs → main.cjs} +37 -28
- package/dist/{index.js → main.js} +33 -24
- package/package.json +30 -41
- package/LICENSE +0 -21
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- /package/dist/{index.d.cts → main.d.cts} +0 -0
- /package/dist/{index.d.ts → main.d.ts} +0 -0
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
|
|
4
|
-
This is NOT the Netlify Identity Widget. This library exports standalone async functions (e.g., import
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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,
|
|
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
|
|
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`,
|
|
31
|
-
|
|
32
|
-
- [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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'`
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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'`,
|
|
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
|
|
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
|
|
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`,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`,
|
|
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.,
|
|
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
|
|
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`)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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()`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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()`,
|
|
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
|
|
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()`,
|
|
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
|
|
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.
|
|
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
|
|
1131
|
-
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
|