@netlify/agent-runner-cli 2.16.0-server.2 → 2.16.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/dist/bin-local.js +257 -263
- package/dist/bin.js +321 -327
- package/dist/index.js +256 -262
- package/dist/interactions-mcp-server.js +1 -1
- package/dist/skills/netlify-identity/SKILL.md +246 -568
- package/dist/skills/netlify-identity/references/advanced-patterns.md +104 -0
- package/dist/skills/netlify-identity/references/authorization-and-sessions.md +24 -0
- package/dist/skills/netlify-server@netlifyserver/SKILL.md +31 -83
- package/package.json +2 -1
- package/scripts/build-skills.js +40 -18
- package/dist/skills/general-backend@backend/SKILL.md +0 -78
- package/dist/skills/netlify-functions/SKILL@backend.md +0 -610
- package/dist/skills/netlify-server@backend/SKILL.md +0 -225
|
@@ -1,682 +1,360 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: netlify-identity
|
|
3
|
-
description:
|
|
3
|
+
description: Add user authentication to a Netlify site with @netlify/identity, covering signup/login/logout, Google/GitHub/GitLab/Bitbucket OAuth, server-side getUser() checks, role-based access control, and Identity event functions. Use it when a task involves adding a login or signup form, gating content to members or roles, "auth middleware" or verifying users in Netlify Functions or Edge Functions, handling OAuth or email-confirmation callbacks, accepting user invites, assigning roles at signup, customizing Identity emails, or finding out why a specific user can't sign up or log in. For locking a whole site to your company or employees-only access, use netlify-access-control instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Netlify Identity
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
access control. It is built on [GoTrue](https://github.com/netlify/gotrue) and issues JSON Web Tokens (JWTs).
|
|
10
|
-
|
|
11
|
-
## Enable Feature
|
|
12
|
-
|
|
13
|
-
After implementing Netlify Identity, you **must** run the enable script to activate the feature on the deploy. The script
|
|
14
|
-
is located in this skill's `scripts/` directory:
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
node scripts/enable.cjs
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
**NEVER use `netlify-identity-widget` or `gotrue-js`. Always use `@netlify/identity`.** It provides a unified, headless
|
|
21
|
-
TypeScript API that works in both browser and server contexts (Netlify Functions, Edge Functions, SSR frameworks). It
|
|
22
|
-
replaces all previous Identity client libraries.
|
|
23
|
-
|
|
24
|
-
## Setup
|
|
8
|
+
Use `@netlify/identity` (npm). For new projects it replaces the legacy `netlify-identity-widget` and `gotrue-js`. Do not reach for those.
|
|
25
9
|
|
|
26
10
|
```bash
|
|
27
11
|
npm install @netlify/identity
|
|
28
12
|
```
|
|
29
13
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **Registration** - Open (anyone can sign up). Change to Invite only in **Project configuration > Identity** if needed.
|
|
33
|
-
- **Autoconfirm** - Off (new signups require email confirmation). Enable in **Project configuration > Identity** to skip
|
|
34
|
-
confirmation during development.
|
|
35
|
-
|
|
36
|
-
For local development, use `netlify dev` so the Identity endpoint is available.
|
|
14
|
+
Framework examples (Next.js/Astro/Remix/SvelteKit) and the full API reference are in the [`@netlify/identity` README on npm](https://www.npmjs.com/package/@netlify/identity).
|
|
37
15
|
|
|
38
|
-
|
|
16
|
+
> **Identity does not run under `netlify dev`.** Test all auth flows on a deploy. Deploy Previews work. Local dev will not complete signup/login/OAuth.
|
|
39
17
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
import { login, getUser } from '@netlify/identity'
|
|
18
|
+
> **Identity config is dashboard-only. There is no public API for Identity settings.** Never curl `api.netlify.com` to flip or read Identity settings, never read tokens from local Netlify config, and never probe undocumented endpoints. The one exception is reading the site's `identity_instance_id` and a single user's account with the lookup described under Additional requirements. Configure Identity at `https://app.netlify.com/projects/{site_name}/identity`. Enable it using this skill's `scripts/enable.cjs`, as described under Additional requirements.
|
|
44
19
|
|
|
45
|
-
|
|
46
|
-
console.log(`Hello, ${user.name}`)
|
|
47
|
-
|
|
48
|
-
// Later, check auth state
|
|
49
|
-
const currentUser = await getUser()
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Protect a Netlify Function:
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
// netlify/functions/protected.mts
|
|
56
|
-
import { getUser } from '@netlify/identity'
|
|
57
|
-
import type { Context } from '@netlify/functions'
|
|
58
|
-
|
|
59
|
-
export default async (req: Request, context: Context) => {
|
|
60
|
-
const user = await getUser()
|
|
61
|
-
if (!user) return new Response('Unauthorized', { status: 401 })
|
|
62
|
-
return Response.json({ id: user.id, email: user.email })
|
|
63
|
-
}
|
|
64
|
-
```
|
|
20
|
+
> **Never build a from-scratch OAuth flow alongside Identity.** No provider app registration in code, no `client_id`/`secret` in source, and no custom callback token exchange. Use `oauthLogin()` + `handleAuthCallback()`. Raw OAuth beside Identity is the most common source of rework.
|
|
65
21
|
|
|
66
|
-
##
|
|
22
|
+
## Client auth (browser)
|
|
67
23
|
|
|
68
|
-
|
|
24
|
+
```ts
|
|
25
|
+
import { signup, login, logout, getUser, oauthLogin, handleAuthCallback } from '@netlify/identity'
|
|
69
26
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- **`MissingIdentityError`** - Thrown when Identity is not configured in the current environment (site doesn't have
|
|
73
|
-
Identity enabled, or not running via `netlify dev`).
|
|
27
|
+
// Register — confirmation email sent by default (unless autoconfirm is on)
|
|
28
|
+
const user = await signup('jane@example.com', 'securepassword', { full_name: 'Jane Doe' })
|
|
74
29
|
|
|
75
|
-
|
|
30
|
+
// Log in / out
|
|
31
|
+
await login('jane@example.com', 'securepassword')
|
|
32
|
+
await logout()
|
|
76
33
|
|
|
77
|
-
|
|
34
|
+
// Current user or null
|
|
35
|
+
const current = await getUser()
|
|
36
|
+
if (current) console.log(`Logged in as ${current.email}`)
|
|
78
37
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const user = await login(email, password)
|
|
84
|
-
} catch (error) {
|
|
85
|
-
if (error instanceof MissingIdentityError) {
|
|
86
|
-
// Identity not configured - show setup instructions
|
|
87
|
-
showError('Identity is not enabled on this site.')
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
if (error instanceof AuthError) {
|
|
91
|
-
switch (error.status) {
|
|
92
|
-
case 401:
|
|
93
|
-
showError('Invalid email or password.')
|
|
94
|
-
break
|
|
95
|
-
case 403:
|
|
96
|
-
showError('Signups are not allowed for this site.')
|
|
97
|
-
break
|
|
98
|
-
case 422:
|
|
99
|
-
showError('Invalid input. Check your email and password.')
|
|
100
|
-
break
|
|
101
|
-
case 404:
|
|
102
|
-
showError('User not found.')
|
|
103
|
-
break
|
|
104
|
-
default:
|
|
105
|
-
showError(error.message)
|
|
106
|
-
}
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
throw error
|
|
110
|
-
}
|
|
38
|
+
// External provider — redirects the browser; provider is one of
|
|
39
|
+
// 'google' | 'github' | 'gitlab' | 'bitbucket'
|
|
40
|
+
oauthLogin('github')
|
|
111
41
|
```
|
|
112
42
|
|
|
113
|
-
|
|
43
|
+
> **`handleAuthCallback()` is mandatory on your landing page.** Without it, OAuth redirects, email-confirmation links, password-recovery links, and invite links never complete. Call it on page load.
|
|
114
44
|
|
|
115
|
-
|
|
116
|
-
|--------|---------|
|
|
117
|
-
| 401 | Invalid credentials or expired token |
|
|
118
|
-
| 403 | Action not allowed (e.g., signups disabled) |
|
|
119
|
-
| 422 | Validation error (e.g., weak password, malformed email) |
|
|
120
|
-
| 404 | User or resource not found |
|
|
45
|
+
> **Invites are NOT logged in by `handleAuthCallback()`.** For an invite it returns `type === 'invite'` with the token in `result.token` and `result.user === null`. The account does not exist yet. You must collect a password and call `acceptInvite(result.token, password)`. If you skip this step, invited users never get an account.
|
|
121
46
|
|
|
122
|
-
|
|
47
|
+
```ts
|
|
48
|
+
import { handleAuthCallback, acceptInvite } from '@netlify/identity'
|
|
123
49
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
const user = await login(email, password)
|
|
135
|
-
showSuccess(`Welcome back, ${user.name ?? user.email}`)
|
|
136
|
-
} catch (error) {
|
|
137
|
-
if (error instanceof AuthError) {
|
|
138
|
-
showError(error.status === 401 ? 'Invalid email or password.' : error.message)
|
|
139
|
-
}
|
|
140
|
-
} finally {
|
|
141
|
-
loading = false
|
|
142
|
-
}
|
|
50
|
+
const result = await handleAuthCallback() // falsy if no token in URL hash
|
|
51
|
+
if (result?.type === 'invite' && result.token) {
|
|
52
|
+
// Invited user has no account yet. Ask them to choose a password first.
|
|
53
|
+
const password = (document.getElementById('password') as HTMLInputElement).value
|
|
54
|
+
const user = await acceptInvite(result.token, password)
|
|
55
|
+
console.log('Account created:', user.email)
|
|
56
|
+
} else if (result) {
|
|
57
|
+
// OAuth, email confirmation, recovery, or email change
|
|
58
|
+
console.log(result.type, result.user?.email)
|
|
143
59
|
}
|
|
144
60
|
```
|
|
145
61
|
|
|
146
|
-
|
|
62
|
+
For recovery tokens you can call `recoverPassword()` directly instead. Refresh a session with `refreshSession()`.
|
|
147
63
|
|
|
148
|
-
|
|
149
|
-
to confirm their email first.
|
|
64
|
+
Don't hard-code which providers exist. Call `getSettings()` at startup and render the signup form and OAuth buttons from what it returns.
|
|
150
65
|
|
|
151
|
-
|
|
152
|
-
import { signup, AuthError } from '@netlify/identity'
|
|
153
|
-
|
|
154
|
-
async function handleSignup(email: string, password: string, name: string) {
|
|
155
|
-
try {
|
|
156
|
-
const user = await signup(email, password, { full_name: name })
|
|
157
|
-
if (user.emailVerified) {
|
|
158
|
-
// Autoconfirm is ON - user is logged in
|
|
159
|
-
showSuccess('Account created. You are now logged in.')
|
|
160
|
-
} else {
|
|
161
|
-
// Autoconfirm is OFF - confirmation email sent, user is NOT logged in
|
|
162
|
-
showSuccess('Check your email to confirm your account.')
|
|
163
|
-
}
|
|
164
|
-
} catch (error) {
|
|
165
|
-
if (error instanceof AuthError) {
|
|
166
|
-
showError(error.status === 403 ? 'Signups are not allowed.' : error.message)
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
```
|
|
66
|
+
## Server-side auth (Netlify Functions & Edge Functions)
|
|
171
67
|
|
|
172
|
-
|
|
173
|
-
`#confirmation_token=<token>` in the URL hash. `handleAuthCallback()` processes this automatically - it calls
|
|
174
|
-
`confirmEmail()` under the hood, logs the user in, and returns `{ type: 'confirmation', user }`. This is why
|
|
175
|
-
`handleAuthCallback()` must be called on page load (see the OAuth section below for the full switch).
|
|
68
|
+
Server-side `getUser()`/`login()`/`admin.*` require modern **v2 functions** (`export default`). The v1 `export { handler }` form is not supported.
|
|
176
69
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
OAuth is a two-step flow: `oauthLogin(provider)` redirects away from the site, then `handleAuthCallback()` processes
|
|
180
|
-
the redirect when the user returns.
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
import { oauthLogin } from '@netlify/identity'
|
|
184
|
-
|
|
185
|
-
// Step 1: Redirect to OAuth provider (this navigates away - never returns)
|
|
186
|
-
function handleOAuthClick(provider: 'google' | 'github' | 'gitlab' | 'bitbucket') {
|
|
187
|
-
oauthLogin(provider)
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
import { handleAuthCallback, AuthError } from '@netlify/identity'
|
|
193
|
-
|
|
194
|
-
// Step 2: Process the redirect on page load
|
|
195
|
-
async function processCallback() {
|
|
196
|
-
try {
|
|
197
|
-
const result = await handleAuthCallback()
|
|
198
|
-
if (!result) return // No callback hash present - normal page load
|
|
199
|
-
|
|
200
|
-
switch (result.type) {
|
|
201
|
-
case 'oauth':
|
|
202
|
-
showSuccess(`Logged in as ${result.user?.email}`)
|
|
203
|
-
break
|
|
204
|
-
case 'confirmation':
|
|
205
|
-
showSuccess('Email confirmed. You are now logged in.')
|
|
206
|
-
break
|
|
207
|
-
case 'recovery':
|
|
208
|
-
// User is authenticated but must set a new password
|
|
209
|
-
showPasswordResetForm(result.user)
|
|
210
|
-
break
|
|
211
|
-
case 'invite':
|
|
212
|
-
// User must set a password to accept the invite
|
|
213
|
-
showInviteAcceptForm(result.token)
|
|
214
|
-
break
|
|
215
|
-
case 'email_change':
|
|
216
|
-
showSuccess('Email address updated.')
|
|
217
|
-
break
|
|
218
|
-
}
|
|
219
|
-
} catch (error) {
|
|
220
|
-
if (error instanceof AuthError) {
|
|
221
|
-
showError(error.message)
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
```
|
|
70
|
+
`getUser()` works in both runtimes. **`admin.*` runs ONLY in Netlify Functions. It does not run in the browser or in Edge Functions.**
|
|
226
71
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
Three-step flow: request recovery email, handle the callback, then set a new password.
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
import { requestPasswordRecovery, handleAuthCallback, updateUser, AuthError } from '@netlify/identity'
|
|
236
|
-
|
|
237
|
-
// Step 1: Send recovery email
|
|
238
|
-
async function handleForgotPassword(email: string) {
|
|
239
|
-
try {
|
|
240
|
-
await requestPasswordRecovery(email)
|
|
241
|
-
showSuccess('Check your email for a password reset link.')
|
|
242
|
-
} catch (error) {
|
|
243
|
-
if (error instanceof AuthError) showError(error.message)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Step 2: handleAuthCallback() returns { type: 'recovery', user } - show password reset form
|
|
248
|
-
// (See the handleAuthCallback switch above)
|
|
249
|
-
|
|
250
|
-
// Step 3: Set new password
|
|
251
|
-
async function handlePasswordReset(newPassword: string) {
|
|
252
|
-
try {
|
|
253
|
-
await updateUser({ password: newPassword })
|
|
254
|
-
showSuccess('Password updated.')
|
|
255
|
-
} catch (error) {
|
|
256
|
-
if (error instanceof AuthError) showError(error.message)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Note: The recovery callback fires a `'recovery'` auth event, not `'login'`. The user is authenticated but should be
|
|
262
|
-
prompted to set a new password before navigating away.
|
|
263
|
-
|
|
264
|
-
### Invite Acceptance
|
|
265
|
-
|
|
266
|
-
When a user clicks an invite link, `handleAuthCallback()` returns `{ type: 'invite', user: null, token }`. Use the
|
|
267
|
-
token to accept the invite and set a password.
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
import { acceptInvite, AuthError } from '@netlify/identity'
|
|
271
|
-
|
|
272
|
-
async function handleAcceptInvite(token: string, password: string) {
|
|
273
|
-
try {
|
|
274
|
-
const user = await acceptInvite(token, password)
|
|
275
|
-
showSuccess(`Welcome, ${user.email}! Your account is ready.`)
|
|
276
|
-
} catch (error) {
|
|
277
|
-
if (error instanceof AuthError) showError(error.message)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### Email Change
|
|
283
|
-
|
|
284
|
-
When a user verifies an email change, `handleAuthCallback()` returns `{ type: 'email_change', user }`. This requires an
|
|
285
|
-
active browser session - the user must be logged in when clicking the verification link.
|
|
286
|
-
|
|
287
|
-
```typescript
|
|
288
|
-
import { verifyEmailChange, AuthError } from '@netlify/identity'
|
|
289
|
-
|
|
290
|
-
// If you need to verify programmatically with a token:
|
|
291
|
-
async function handleEmailChangeVerification(token: string) {
|
|
292
|
-
try {
|
|
293
|
-
const user = await verifyEmailChange(token)
|
|
294
|
-
showSuccess(`Email updated to ${user.email}`)
|
|
295
|
-
} catch (error) {
|
|
296
|
-
if (error instanceof AuthError) showError(error.message)
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### Session Hydration
|
|
302
|
-
|
|
303
|
-
`hydrateSession()` bridges server-set cookies to the browser session. Call it on page load when using server-side login
|
|
304
|
-
(e.g., login inside a Netlify Function followed by a redirect).
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
import { hydrateSession } from '@netlify/identity'
|
|
72
|
+
```ts
|
|
73
|
+
// netlify/functions/me.ts — verify user
|
|
74
|
+
import { getUser } from '@netlify/identity'
|
|
75
|
+
import type { Context } from '@netlify/functions'
|
|
308
76
|
|
|
309
|
-
|
|
310
|
-
const user = await
|
|
311
|
-
if (user) {
|
|
312
|
-
|
|
77
|
+
export default async (req: Request, context: Context) => {
|
|
78
|
+
const user = await getUser()
|
|
79
|
+
if (!user) return new Response('Unauthorized', { status: 401 })
|
|
80
|
+
return Response.json({ id: user.id, email: user.email })
|
|
313
81
|
}
|
|
314
82
|
```
|
|
315
83
|
|
|
316
|
-
|
|
317
|
-
is only needed when you want to restore the full session (including token refresh timers) after a server-side login.
|
|
318
|
-
|
|
319
|
-
## Auth Events
|
|
320
|
-
|
|
321
|
-
Subscribe to auth state changes with `onAuthChange`. Returns an unsubscribe function. No-op on server.
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
|
|
325
|
-
|
|
326
|
-
const unsubscribe = onAuthChange((event, user) => {
|
|
327
|
-
switch (event) {
|
|
328
|
-
case AUTH_EVENTS.LOGIN:
|
|
329
|
-
console.log('User logged in:', user?.email)
|
|
330
|
-
break
|
|
331
|
-
case AUTH_EVENTS.LOGOUT:
|
|
332
|
-
console.log('User logged out')
|
|
333
|
-
break
|
|
334
|
-
case AUTH_EVENTS.TOKEN_REFRESH:
|
|
335
|
-
// Token auto-refreshed in background
|
|
336
|
-
break
|
|
337
|
-
case AUTH_EVENTS.USER_UPDATED:
|
|
338
|
-
console.log('User profile updated:', user?.email)
|
|
339
|
-
break
|
|
340
|
-
case AUTH_EVENTS.RECOVERY:
|
|
341
|
-
// Recovery token processed - prompt for new password
|
|
342
|
-
console.log('Password recovery initiated')
|
|
343
|
-
break
|
|
344
|
-
}
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
// Later: unsubscribe()
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
Auth events are automatically detected across browser tabs via the storage event listener - no extra setup needed.
|
|
351
|
-
|
|
352
|
-
## Settings-Driven UI
|
|
353
|
-
|
|
354
|
-
Fetch the project's Identity settings to conditionally render signup forms and OAuth buttons.
|
|
355
|
-
|
|
356
|
-
```typescript
|
|
357
|
-
import { getSettings } from '@netlify/identity'
|
|
84
|
+
The Edge Function form is identical but imports `Context` from `@netlify/edge-functions`.
|
|
358
85
|
|
|
359
|
-
|
|
360
|
-
// settings.autoconfirm - boolean, whether email confirmation is skipped
|
|
361
|
-
// settings.disableSignup - boolean, whether registration is closed
|
|
362
|
-
// settings.providers - Record<AuthProvider, boolean>, e.g. { google: true, github: true, ... }
|
|
86
|
+
### Role checks
|
|
363
87
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
88
|
+
```ts
|
|
89
|
+
// netlify/functions/admin-users.ts
|
|
90
|
+
import { getUser, admin } from '@netlify/identity'
|
|
91
|
+
import type { Context } from '@netlify/functions'
|
|
368
92
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
93
|
+
export default async (req: Request, context: Context) => {
|
|
94
|
+
const user = await getUser()
|
|
95
|
+
if (!user) return new Response('Unauthorized', { status: 401 })
|
|
96
|
+
if (!user.roles.includes('admin')) return new Response('Forbidden', { status: 403 })
|
|
97
|
+
const users = await admin.listUsers()
|
|
98
|
+
return Response.json({ users })
|
|
374
99
|
}
|
|
375
100
|
```
|
|
376
101
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
For the complete API reference - all function signatures, type definitions, OAuth helpers, admin operations, session
|
|
380
|
-
management, auth events, and framework-specific integration examples - read the package README:
|
|
381
|
-
|
|
382
|
-
```
|
|
383
|
-
node_modules/@netlify/identity/README.md
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
The README is shipped with the npm package and is always in sync with the installed version.
|
|
387
|
-
|
|
388
|
-
## SSR Integration Patterns
|
|
102
|
+
### CSRF: required for server-side auth endpoints
|
|
389
103
|
|
|
390
|
-
|
|
104
|
+
> Any endpoint that runs `login()`, `signup()`, or `logout()` server-side **must** call `verifyRequestOrigin(req)` at the top of the handler. It throws a 403 on origin mismatch.
|
|
391
105
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
Browser-side auth mutations set the `nf_jwt` cookie and localStorage, and emit `onAuthChange` events. The
|
|
396
|
-
server reads the cookie on the next request.
|
|
397
|
-
|
|
398
|
-
The library also supports server-side mutations (`login()`, `signup()`, `logout()` inside Netlify Functions), but these
|
|
399
|
-
require the Netlify Functions runtime to set cookies. After a server-side mutation, use a full page navigation so the
|
|
400
|
-
browser sends the new cookie.
|
|
401
|
-
|
|
402
|
-
Always use `window.location.href` (not framework router navigation) after server-side auth mutations in Next.js,
|
|
403
|
-
TanStack Start, and SvelteKit. Remix `redirect()` is safe because Remix actions return real HTTP responses.
|
|
404
|
-
|
|
405
|
-
## Netlify SSO Users (`context.user`)
|
|
406
|
-
|
|
407
|
-
Netlify Identity is for your site's own end users. Separately, on private projects and sites protected by Netlify team
|
|
408
|
-
login, Netlify Functions, Edge Functions, and Netlify Server code can read the Netlify team member who signed in through
|
|
409
|
-
Netlify's SSO from the request context as `context.user` (`id`, `email`, `expiresAt`). Netlify verifies the visitor at
|
|
410
|
-
the edge and sets it itself, so it can't be spoofed with request headers.
|
|
411
|
-
|
|
412
|
-
Use `context.user` instead of Netlify Identity when the only people who should reach the site are members of the
|
|
413
|
-
Netlify team (internal dashboards, audit logs, approval workflows): there is no signup, login UI, or `@netlify/identity`
|
|
414
|
-
setup to build, and you don't run the enable script for it. Use `getUser()` from `@netlify/identity` when the site has
|
|
415
|
-
its own user accounts.
|
|
416
|
-
|
|
417
|
-
```typescript
|
|
418
|
-
// netlify/functions/whoami.mts
|
|
106
|
+
```ts
|
|
107
|
+
// netlify/functions/login.ts
|
|
108
|
+
import { login, verifyRequestOrigin } from '@netlify/identity'
|
|
419
109
|
import type { Context } from '@netlify/functions'
|
|
420
110
|
|
|
421
111
|
export default async (req: Request, context: Context) => {
|
|
422
|
-
|
|
423
|
-
|
|
112
|
+
verifyRequestOrigin(req)
|
|
113
|
+
const { email, password } = await req.json()
|
|
114
|
+
await login(email, password)
|
|
115
|
+
return new Response(null, { status: 302, headers: { Location: '/dashboard' } })
|
|
424
116
|
}
|
|
425
117
|
```
|
|
426
118
|
|
|
427
|
-
|
|
428
|
-
**netlify-functions**, **netlify-edge-functions**, and **netlify-server** skills for runtime-specific details.
|
|
119
|
+
## Identity event functions
|
|
429
120
|
|
|
430
|
-
|
|
121
|
+
The platform calls your handler when an Identity event occurs. Export a default object with a method per event. File: `netlify/functions/identity.mts`.
|
|
431
122
|
|
|
432
|
-
|
|
433
|
-
**netlify-functions** skill for the full event-handler pattern.
|
|
123
|
+
> Typed handlers (`UserSignupEvent`, `event.deny()`) require `@netlify/functions` ≥ 5.2.0. Older installs must use the legacy filename convention (`identity-signup.ts`, etc.). See `references/authorization-and-sessions.md`.
|
|
434
124
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
| Handler | Trigger |
|
|
125
|
+
| Handler | Fires when |
|
|
438
126
|
|---|---|
|
|
439
|
-
| `userValidate` |
|
|
440
|
-
| `userSignup` |
|
|
441
|
-
| `userLogin` |
|
|
442
|
-
| `userModified` |
|
|
443
|
-
| `userDeleted` |
|
|
444
|
-
|
|
445
|
-
Each handler receives a typed event with a parsed `user` object (camelCase fields: `appMetadata`, `userMetadata`,
|
|
446
|
-
`confirmedAt`, etc.).
|
|
127
|
+
| `userValidate` | Signup attempt, before account creation. Block bad signups here. |
|
|
128
|
+
| `userSignup` | Signup completes (after email confirmation if enabled). Assign roles, sync, welcome. |
|
|
129
|
+
| `userLogin` | User logs in. Track/last-seen/block. |
|
|
130
|
+
| `userModified` | Profile updated. |
|
|
131
|
+
| `userDeleted` | User deleted (notification only). |
|
|
447
132
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
Return `{ user: ... }` to substitute the user record before it's persisted.
|
|
133
|
+
Event `user` fields are camelCase (`appMetadata`, `userMetadata`, `confirmedAt`).
|
|
451
134
|
|
|
452
135
|
```typescript
|
|
453
|
-
// netlify/functions/identity.mts
|
|
454
|
-
import type {
|
|
136
|
+
// netlify/functions/identity.mts — deny a signup
|
|
137
|
+
import type { UserValidateEvent } from "@netlify/functions"
|
|
455
138
|
|
|
456
139
|
export default {
|
|
457
|
-
|
|
458
|
-
return
|
|
459
|
-
user: {
|
|
460
|
-
...event.user,
|
|
461
|
-
appMetadata: {
|
|
462
|
-
...event.user.appMetadata,
|
|
463
|
-
roles: ['member'],
|
|
464
|
-
},
|
|
465
|
-
},
|
|
466
|
-
}
|
|
140
|
+
userValidate(event: UserValidateEvent) {
|
|
141
|
+
if (!event.user.email?.endsWith("@example.com")) return event.deny()
|
|
467
142
|
},
|
|
468
143
|
}
|
|
469
144
|
```
|
|
470
145
|
|
|
471
|
-
### Deny an Action
|
|
472
|
-
|
|
473
|
-
Call `event.deny()` to reject a signup, login, validation, or modification. The end user receives a 401. Do NOT throw —
|
|
474
|
-
`event.deny()` is the canonical denial mechanism and does not produce an error in observability.
|
|
475
|
-
|
|
476
146
|
```typescript
|
|
477
|
-
|
|
147
|
+
// netlify/functions/identity.mts — assign roles at signup
|
|
148
|
+
import type { UserSignupEvent } from "@netlify/functions"
|
|
478
149
|
|
|
479
150
|
export default {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
return event.deny()
|
|
483
|
-
}
|
|
151
|
+
userSignup(event: UserSignupEvent) {
|
|
152
|
+
return { user: { ...event.user, appMetadata: { ...event.user.appMetadata, roles: ["member"] } } }
|
|
484
153
|
},
|
|
485
154
|
}
|
|
486
155
|
```
|
|
487
156
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
**Legacy filename convention:** The previous syntax — files named `identity-validate.ts`, `identity-signup.ts`, or
|
|
493
|
-
`identity-login.ts` (with a `-background` suffix for background mode), exporting `handler` and signaling denial via a
|
|
494
|
-
non-2xx response — still works. New functions should prefer the typed handler syntax above.
|
|
495
|
-
|
|
496
|
-
## Roles and Authorization
|
|
157
|
+
- `event.deny()` rejects the action. The end user gets a `401`, and no error appears in observability. The first handler to call it aborts the chain, and later subscribers are not invoked. Legacy filename functions signal denial with a non-2xx `Response` instead.
|
|
158
|
+
- Return `{ user: {...} }` to modify the record before it is persisted. This is the canonical way to set roles at signup. Roles ride in the JWT, so a role change takes effect on the user's **next login or token refresh, not immediately**. See Roles & the JWT below.
|
|
159
|
+
- Background mode: `export const config: Config = { background: true }`. The action completes immediately and the handler runs asynchronously.
|
|
497
160
|
|
|
498
|
-
|
|
161
|
+
## Roles & the JWT
|
|
499
162
|
|
|
500
|
-
|
|
163
|
+
- `user.roles` is read from `app_metadata.roles` and carried in the JWT (cookie `nf_jwt`; refresh via `nf_refresh`).
|
|
164
|
+
- `user_metadata` holds the user-editable profile (`full_name`, `email`). `app_metadata` holds app data, including `roles`, and is not user-editable.
|
|
501
165
|
|
|
502
|
-
|
|
503
|
-
2. Click **Invite users** and enter the admin user's email address
|
|
504
|
-
3. After the user accepts the invite, click the user in the Identity list to open their detail page
|
|
505
|
-
4. In the **Roles** field, add the `admin` role and save
|
|
166
|
+
> **Role changes are NOT immediate.** They take effect on next login or token refresh. Changing roles does not invalidate the current JWT. Force a refresh with `refreshSession()`.
|
|
506
167
|
|
|
507
|
-
|
|
168
|
+
Set roles for existing users via `admin.updateUser()` in a Netlify Function. Set them at signup via the `userSignup` event handler above.
|
|
508
169
|
|
|
509
|
-
|
|
170
|
+
Deep guides for SSR/session hydration and authorization live in `references/advanced-patterns.md` and `references/authorization-and-sessions.md`.
|
|
510
171
|
|
|
511
|
-
|
|
512
|
-
Do not allow users to set their own roles.
|
|
513
|
-
- **`user_metadata`** - User-controlled. Users can update this via `updateUser({ data: { ... } })`.
|
|
172
|
+
## CDN-edge RBAC (redirect rules)
|
|
514
173
|
|
|
515
|
-
|
|
174
|
+
Enforced at the edge with no origin round trip. A mismatched role gets a 404 unless you add a fallback. **Always pair a role-gated rule with a fallback.**
|
|
516
175
|
|
|
517
|
-
|
|
176
|
+
`_redirects`:
|
|
177
|
+
```
|
|
178
|
+
/admin/* /admin/:splat 200! Role=admin
|
|
179
|
+
/admin/* /login 401!
|
|
180
|
+
# Multiple roles chained with commas:
|
|
181
|
+
/private/* /private/:splat 200! Role=editor,admin
|
|
182
|
+
```
|
|
518
183
|
|
|
184
|
+
`netlify.toml`:
|
|
519
185
|
```toml
|
|
520
186
|
[[redirects]]
|
|
521
187
|
from = "/admin/*"
|
|
522
188
|
to = "/admin/:splat"
|
|
189
|
+
force = true
|
|
523
190
|
status = 200
|
|
524
|
-
conditions = {
|
|
525
|
-
|
|
526
|
-
[[redirects]]
|
|
527
|
-
from = "/admin/*"
|
|
528
|
-
to = "/"
|
|
529
|
-
status = 302
|
|
191
|
+
conditions = {Role = ["editor", "admin"]}
|
|
530
192
|
```
|
|
531
193
|
|
|
532
|
-
|
|
533
|
-
to the second rule and is redirected away.
|
|
194
|
+
Use redirect rules for path-based gating. Use function-based `user.roles` checks for custom authorization logic.
|
|
534
195
|
|
|
535
|
-
|
|
536
|
-
cookie, role-based redirects will not work.
|
|
196
|
+
## Configuration (dashboard-only)
|
|
537
197
|
|
|
538
|
-
|
|
198
|
+
Base: `https://app.netlify.com/projects/{site_name}/identity`. Enable Identity using this skill's `scripts/enable.cjs`, as described under Additional requirements. Identity requires HTTPS, so set up SSL before integrating on a custom domain.
|
|
539
199
|
|
|
540
|
-
|
|
200
|
+
- **Registration** (`?tab=registration#registration-preferences`): **Open** (default; anyone can sign up) or **Invite only** (all users, including external-provider logins, must be invited first).
|
|
201
|
+
- **Confirmation / autoconfirm** (`?tab=emails#confirmation-template`): check the box to skip email verification.
|
|
202
|
+
- **External providers** (`?tab=registration#external-providers`): Google/GitHub/GitLab/Bitbucket. For branded OAuth (your app name instead of "Netlify Identity"), register your app with the provider, get a client ID and secret, and enter them **in the Netlify settings UI**, not in code.
|
|
203
|
+
- **Invitations** (`?tab=users`): enter addresses to send invites. The link carries `invite_token`. Complete it with `handleAuthCallback()` + `acceptInvite(token, password)` as shown above.
|
|
204
|
+
- **Password recovery**: on the user page, use **Send reset password email**. The link carries `recovery_token`.
|
|
541
205
|
|
|
542
|
-
|
|
206
|
+
### Emails (Pro plans or higher)
|
|
543
207
|
|
|
544
|
-
**
|
|
208
|
+
The default sender is `no-reply@netlify.com`. A custom SMTP sender and custom templates both require **Pro plans or higher**.
|
|
545
209
|
|
|
546
|
-
|
|
547
|
-
2. Or invite users from the Identity tab in the Netlify UI
|
|
210
|
+
Template variables (Go syntax): `{{ .Email }}`, `{{ .NewEmail }}` (email-change only), `{{ .SiteURL }}`, `{{ .ConfirmationURL }}`, `{{ .Token }}`.
|
|
548
211
|
|
|
549
|
-
|
|
550
|
-
if (error instanceof AuthError && error.status === 403) {
|
|
551
|
-
showError('Signups are disabled. Contact the site admin for an invite.')
|
|
552
|
-
}
|
|
212
|
+
Custom-link hash fragments per action:
|
|
553
213
|
```
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
**Fix:** Show a user-facing error and let the user retry. Do not reveal whether the email or password was wrong.
|
|
560
|
-
|
|
561
|
-
```typescript
|
|
562
|
-
if (error instanceof AuthError && error.status === 401) {
|
|
563
|
-
showError('Invalid email or password.')
|
|
564
|
-
}
|
|
214
|
+
{{ .SiteURL }}/path/#invite_token={{ .Token }}
|
|
215
|
+
{{ .SiteURL }}/path/#confirmation_token={{ .Token }}
|
|
216
|
+
{{ .SiteURL }}/path/#recovery_token={{ .Token }}
|
|
217
|
+
{{ .SiteURL }}/path/#email_change_token={{ .Token }}
|
|
565
218
|
```
|
|
566
219
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
220
|
+
Custom template constraints:
|
|
221
|
+
- Inline CSS only.
|
|
222
|
+
- Absolute image links.
|
|
223
|
+
- **No `<html>`/`<head>`/`<body>` tags.**
|
|
224
|
+
- Ensure your build doesn't alter Go template variables.
|
|
225
|
+
|
|
226
|
+
### Audit log (Pro plans or higher)
|
|
227
|
+
|
|
228
|
+
`?tab=audit-log`. Search with a scoped term: `author:[string]` or `action:[string]`.
|
|
229
|
+
|
|
230
|
+
Action names: `login`, `logout`, `user_signedup`, `user_deleted`, `user_modified`, `token_revoked`, `token_refreshed`, `user_recovery_requested`, `user_invited`.
|
|
231
|
+
|
|
232
|
+
## External JWT providers (Enterprise)
|
|
233
|
+
|
|
234
|
+
Available on **Enterprise plans**. You may use Netlify Identity OR an external JWT provider, **not both at once**. You cannot authenticate third-party JWTs while Netlify Identity is enabled.
|
|
235
|
+
|
|
236
|
+
- Roles path: Netlify Identity uses `app_metadata.roles`; an external provider uses `app_metadata.authorization.roles`. For a custom path, contact support.
|
|
237
|
+
- The JWT header must be `{"alg": "HS256", "typ": "JWT"}` (HS256 required). The payload `exp` is required and must be a future Unix Epoch time.
|
|
238
|
+
- Set the JWT secret at `Project configuration > General > Visitor access > JWT secret`. Project-level settings override team-level defaults.
|
|
239
|
+
|
|
240
|
+
## On failure — stop, don't guess
|
|
241
|
+
|
|
242
|
+
If callbacks 404, `/.netlify/identity/*` is unreachable, or an OAuth flow never returns:
|
|
243
|
+
1. Surface the error.
|
|
244
|
+
2. Give the dashboard URL (`https://app.netlify.com/projects/{site_name}/identity`).
|
|
245
|
+
3. Name the setting to check (registration preference, external provider config, confirmation/autoconfirm).
|
|
246
|
+
4. Then stop. Do not invent recovery commands.
|
|
247
|
+
|
|
248
|
+
Remember: Identity does not work under `netlify dev`. Confirm you are testing on a deploy.
|
|
249
|
+
|
|
250
|
+
Site-gating requests ("lock this site to my company", employees-only) route to the netlify-access-control skill first. Identity is the app-level user layer only. Restricting who can view the whole project uses [project visibility](https://docs.netlify.com/manage/security/secure-access-to-sites/project-visibility) or [Password Protection](https://docs.netlify.com/manage/security/secure-access-to-sites/password-protection), not Identity.
|
|
251
|
+
|
|
252
|
+
<!-- gap: getSettings() signature/return shape is not documented in the intermediate. -->
|
|
253
|
+
<!-- gap: handleAuthCallback() result.type strings other than 'invite' are not enumerated. -->
|
|
254
|
+
|
|
255
|
+
<!-- system: agent-context/identity/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->
|
|
256
|
+
# Netlify house rules (identity)
|
|
257
|
+
|
|
258
|
+
These are org conventions, not docs facts — merged into the rendered skill by
|
|
259
|
+
ctx-gen and never generated. Owned by the skills maintainer.
|
|
260
|
+
|
|
261
|
+
1. Deep guides live in this skill: `references/advanced-patterns.md`
|
|
262
|
+
(SSR/session hydration) and `references/authorization-and-sessions.md`.
|
|
263
|
+
2. Identity does not work under `netlify dev` — test auth flows on deploys
|
|
264
|
+
(Deploy Previews work).
|
|
265
|
+
3. Identity configuration has no public API — it is dashboard-only. Never curl
|
|
266
|
+
`api.netlify.com` to flip or inspect Identity settings, never read auth
|
|
267
|
+
tokens from `~/Library/Preferences/netlify/config.json`, never probe for
|
|
268
|
+
undocumented endpoints. The one exception is reading the site's
|
|
269
|
+
`identity_instance_id` and a single user's account with the lookup described
|
|
270
|
+
under Additional requirements.
|
|
271
|
+
4. On failure (callback 404s, `/.netlify/identity/*` unreachable, OAuth flow
|
|
272
|
+
doesn't return), surface the error, the dashboard URL, and the setting to
|
|
273
|
+
check — then stop. Do not invent recovery commands.
|
|
274
|
+
5. Never build a from-scratch third-party OAuth flow when Identity is in play —
|
|
275
|
+
no provider app registration, no `client_id`/`secret` in code, no custom
|
|
276
|
+
callback token exchange. Use `oauthLogin()` + `handleAuthCallback()`;
|
|
277
|
+
raw OAuth beside Identity is the single most common source of rework.
|
|
278
|
+
6. Server-side `getUser()`/`login()`/`admin.*` require modern v2 functions
|
|
279
|
+
(`export default`) — v1 `export { handler }` is not supported. Typed
|
|
280
|
+
Identity event handlers (`UserSignupEvent`, `event.deny()`) require
|
|
281
|
+
`@netlify/functions` ≥ 5.2.0; older installs use the legacy filenames.
|
|
282
|
+
7. Don't hard-code which auth providers exist — call `getSettings()` at
|
|
283
|
+
startup and render the signup form and OAuth buttons from what it returns.
|
|
284
|
+
8. Site-gating requests ("lock this site to my company", employees-only)
|
|
285
|
+
route to the netlify-access-control skill first — Identity is the
|
|
286
|
+
app-level user layer only.
|
|
287
|
+
9. Any answer that assigns or changes roles — at signup, via `admin.*`, or in
|
|
288
|
+
the dashboard — must say the change takes effect on the user's next login
|
|
289
|
+
or token refresh, not immediately. Keep that sentence next to the code that
|
|
290
|
+
sets the role, not only in a separate JWT section: an agent answering a
|
|
291
|
+
signup question reads the signup example and stops, and it has shipped
|
|
292
|
+
answers that omit the delay.
|
|
293
|
+
10. Before setting up Identity for an internal tool, ask who signs in. If
|
|
294
|
+
they can already pass the project's Netlify login (a private project or
|
|
295
|
+
team login protection), the app has a verified user without Identity:
|
|
296
|
+
read `context.user` in functions and edge functions and skip the second
|
|
297
|
+
login. Identity is for users outside that Netlify login. The two never
|
|
298
|
+
merge: `context.user` has no roles or `app_metadata`, and `getUser()`
|
|
299
|
+
never returns it.
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
## Additional requirements
|
|
303
|
+
|
|
304
|
+
After implementing Netlify Identity, you MUST run `scripts/enable.cjs` from this skill using Node. Use the absolute path
|
|
305
|
+
to the script and keep the project's directory as your working directory:
|
|
575
306
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
**Fix:**
|
|
579
|
-
|
|
580
|
-
1. Always use `getUser()` before authenticated requests - it auto-hydrates from cookies
|
|
581
|
-
2. In the browser, the library auto-refreshes tokens via `startTokenRefresh()`
|
|
582
|
-
3. On the server, call `refreshSession()` in middleware to handle near-expiry tokens
|
|
583
|
-
|
|
584
|
-
```typescript
|
|
585
|
-
const newToken = await refreshSession()
|
|
586
|
-
if (newToken) {
|
|
587
|
-
// Token was refreshed - retry the request
|
|
588
|
-
}
|
|
307
|
+
```bash
|
|
308
|
+
node /absolute/path/to/netlify-identity/scripts/enable.cjs
|
|
589
309
|
```
|
|
590
310
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
**Fix:**
|
|
596
|
-
|
|
597
|
-
1. Use the typed handler syntax: default export an object with `userSignup`, `userLogin`, `userValidate`, `userModified`,
|
|
598
|
-
or `userDeleted` methods
|
|
599
|
-
2. Place the function in `netlify/functions/` with `.mts` or `.mjs` extension
|
|
600
|
-
3. If using the legacy filename convention, verify the filename matches the event exactly (`identity-signup.mts`,
|
|
601
|
-
`identity-validate.mts`, or `identity-login.mts`) and that the file exports a named `handler`
|
|
602
|
-
|
|
603
|
-
### `MissingIdentityError`
|
|
311
|
+
Replace the example path with the installed location of this skill. The script creates
|
|
312
|
+
`.netlify/features/netlify-identity` in the project's repository. The deployment workflow uses that marker to request
|
|
313
|
+
Identity enablement before deploying. Do not ask the user to enable Identity in the Netlify UI. Registration settings,
|
|
314
|
+
external providers, and other Identity configuration can still be managed in the dashboard.
|
|
604
315
|
|
|
605
|
-
|
|
316
|
+
### Look up one user's account
|
|
606
317
|
|
|
607
|
-
|
|
318
|
+
When the user reports that a specific person can't sign up or log in, read that person's Identity account with the
|
|
319
|
+
Netlify API before guessing. You can look up one user by email or by ID. Listing users, searching, changing users, and
|
|
320
|
+
the Identity audit log are not available to you; those calls return 401 or 404.
|
|
608
321
|
|
|
609
|
-
|
|
610
|
-
|
|
322
|
+
`$SITE_ID` is the project ID from your instructions. First read the project's Identity instance ID from the
|
|
323
|
+
`identity_instance_id` field of the site object into `IDENTITY_ID`. If it prints `null`, Identity is not enabled on the
|
|
324
|
+
project; stop here.
|
|
611
325
|
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
326
|
+
```bash
|
|
327
|
+
IDENTITY_ID=$(curl -s -H "Authorization: Bearer $NETLIFY_AUTH_TOKEN" "https://api.netlify.com/api/v1/sites/$SITE_ID" \
|
|
328
|
+
| node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).identity_instance_id')
|
|
329
|
+
echo "$IDENTITY_ID"
|
|
616
330
|
```
|
|
617
331
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
**Cause:** Server-side `login()`, `signup()`, or `logout()` require the Netlify Functions runtime to set cookies.
|
|
621
|
-
|
|
622
|
-
**Fix:**
|
|
332
|
+
Look up by email (matching ignores case and trailing spaces; partial emails return 404):
|
|
623
333
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
**Cause:** OAuth provider is not enabled for the project.
|
|
630
|
-
|
|
631
|
-
**Fix:**
|
|
632
|
-
|
|
633
|
-
1. Enable the provider in **Project configuration > Identity > External providers**
|
|
634
|
-
2. Users are created automatically on first OAuth login
|
|
635
|
-
|
|
636
|
-
### Account operations fail after server-side login
|
|
637
|
-
|
|
638
|
-
**Cause:** Browser-side session is not bootstrapped from server-set cookies.
|
|
639
|
-
|
|
640
|
-
**Fix:** Call `hydrateSession()` on page load to bridge server-set cookies to the browser session. Then use
|
|
641
|
-
`updateUser()`, `verifyEmailChange()`, or other account operations.
|
|
642
|
-
|
|
643
|
-
```typescript
|
|
644
|
-
import { hydrateSession, updateUser } from '@netlify/identity'
|
|
645
|
-
|
|
646
|
-
// On page load after server-side login
|
|
647
|
-
await hydrateSession()
|
|
648
|
-
|
|
649
|
-
// Now account operations work
|
|
650
|
-
await updateUser({ data: { full_name: 'Jane' } })
|
|
334
|
+
```bash
|
|
335
|
+
curl -s -X POST -H "Authorization: Bearer $NETLIFY_AUTH_TOKEN" -H "Content-Type: application/json" \
|
|
336
|
+
-d '{"email": "jane@example.com"}' \
|
|
337
|
+
"https://api.netlify.com/api/v1/sites/$SITE_ID/identity/$IDENTITY_ID/users/lookup"
|
|
651
338
|
```
|
|
652
339
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
**Cause:** `verifyEmailChange()` was called without an active session. The user must be logged in when clicking the
|
|
656
|
-
email change verification link.
|
|
657
|
-
|
|
658
|
-
**Fix:** Ensure the user is logged in before processing the `email_change` callback. If the session expired, prompt
|
|
659
|
-
the user to log in first.
|
|
660
|
-
|
|
661
|
-
### "No user is currently logged in"
|
|
340
|
+
Look up by ID:
|
|
662
341
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
```typescript
|
|
668
|
-
const user = await getUser()
|
|
669
|
-
if (!user) {
|
|
670
|
-
redirectToLogin()
|
|
671
|
-
return
|
|
672
|
-
}
|
|
673
|
-
await updateUser({ data: { full_name: 'Jane' } })
|
|
342
|
+
```bash
|
|
343
|
+
curl -s -H "Authorization: Bearer $NETLIFY_AUTH_TOKEN" \
|
|
344
|
+
"https://api.netlify.com/api/v1/sites/$SITE_ID/identity/$IDENTITY_ID/users/$USER_ID"
|
|
674
345
|
```
|
|
675
346
|
|
|
676
|
-
|
|
347
|
+
Once the site read has returned an `identity_instance_id`, a 404 from these lookups means no user with that email or ID
|
|
348
|
+
exists on this project. The response contains:
|
|
677
349
|
|
|
678
|
-
|
|
679
|
-
|
|
350
|
+
| Field | What it tells you |
|
|
351
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
352
|
+
| `confirmed_at` | Empty: the user never confirmed their email or accepted their invite, so login fails. |
|
|
353
|
+
| `invited_at` | Set: the user was invited. With an empty `confirmed_at`, the invite is still pending. |
|
|
354
|
+
| `app_metadata.provider` | `email` for password signups; `google`, `github`, etc. for OAuth. An OAuth user has no password, so the password form rejects them. |
|
|
355
|
+
| `app_metadata.roles` | Compare with the roles your redirects or code require. |
|
|
356
|
+
| `last_sign_in_at` | When the user last logged in successfully. |
|
|
357
|
+
| `recovery_sent_at` | When a password reset email was last requested. |
|
|
358
|
+
| `email_change_sent_at` | When an email change was last requested. |
|
|
680
359
|
|
|
681
|
-
|
|
682
|
-
check for `null` before using the user object.
|
|
360
|
+
These reads don't show whether the login flow in the code works. Check the code and the preview for that.
|