{"id":27255,"plugin_id":"plugin_asdk_app_691f1f8f72408191afdbbdf8242bdf86","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-07T00:02:43.625Z","digest":"c5e6e9723ca9ac756aba8fd0f8a6b09b7e89ed69c6cc7c10dfdcc8b2854e3a8e","against":24999,"payload":{"description":"Add user authentication to a Netlify site with @netlify/identity — 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, assigning roles at signup, or customizing Identity emails. For locking a whole site to your company or employees-only access, use netlify-access-control instead.","included_files":[{"relative_path":"references/advanced-patterns.md","size_in_bytes":4428},{"relative_path":"references/authorization-and-sessions.md","size_in_bytes":3384}],"name":"netlify-identity","skill_md_contents":"---\nname: netlify-identity\ndescription: Add user authentication to a Netlify site with @netlify/identity — 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, assigning roles at signup, or customizing Identity emails. For locking a whole site to your company or employees-only access, use netlify-access-control instead.\n---\n\n# Netlify Identity\n\nUse `@netlify/identity` (npm). For new projects it replaces the legacy `netlify-identity-widget` and `gotrue-js` — do not reach for those.\n\n```bash\nnpm install @netlify/identity\n```\n\nFramework 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).\n\n> **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.\n\n> **Identity config is dashboard-only — there is no public API.** Never curl `api.netlify.com` to flip or read Identity settings, never read tokens from local Netlify config, never probe undocumented endpoints. Enable and configure Identity at `https://app.netlify.com/projects/{site_name}/identity`.\n\n> **Never build a from-scratch OAuth flow alongside Identity.** No provider app registration in code, no `client_id`/`secret` in source, no custom callback token exchange. Use `oauthLogin()` + `handleAuthCallback()`. Raw OAuth beside Identity is the most common source of rework.\n\n## Client auth (browser)\n\n```ts\nimport { signup, login, logout, getUser, oauthLogin, handleAuthCallback } from '@netlify/identity'\n\n// Register — confirmation email sent by default (unless autoconfirm is on)\nconst user = await signup('jane@example.com', 'securepassword', { full_name: 'Jane Doe' })\n\n// Log in / out\nawait login('jane@example.com', 'securepassword')\nawait logout()\n\n// Current user or null\nconst current = await getUser()\nif (current) console.log(`Logged in as ${current.email}`)\n\n// External provider — redirects the browser; provider is one of\n// 'google' | 'github' | 'gitlab' | 'bitbucket'\noauthLogin('github')\n```\n\n> **`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:\n\n```ts\nimport { handleAuthCallback } from '@netlify/identity'\n\nconst result = await handleAuthCallback() // falsy if no token in URL hash\nif (result) console.log(result.type, result.user.email) // confirmation | invite | recovery | email change\n```\n\nAlternatives for a single token type: `recoverPassword()` (recovery), `acceptInvite()` (invite). Refresh a session with `refreshSession()`.\n\nDon't hard-code which providers exist. Call `getSettings()` at startup and render the signup form and OAuth buttons from what it returns.\n\n## Server-side auth (Netlify Functions & Edge Functions)\n\nServer-side `getUser()`/`login()`/`admin.*` require modern **v2 functions** (`export default`). The v1 `export { handler }` form is not supported.\n\n`getUser()` works in both runtimes. **`admin.*` runs ONLY in Netlify Functions — not the browser, not Edge Functions.**\n\n```ts\n// netlify/functions/me.ts — verify user\nimport { getUser } from '@netlify/identity'\nimport type { Context } from '@netlify/functions'\n\nexport default async (req: Request, context: Context) => {\n  const user = await getUser()\n  if (!user) return new Response('Unauthorized', { status: 401 })\n  return Response.json({ id: user.id, email: user.email })\n}\n```\n\nEdge Function form is identical but imports `Context` from `@netlify/edge-functions`.\n\n### Role checks\n\n```ts\n// netlify/functions/admin-users.ts\nimport { getUser, admin } from '@netlify/identity'\nimport type { Context } from '@netlify/functions'\n\nexport default async (req: Request, context: Context) => {\n  const user = await getUser()\n  if (!user) return new Response('Unauthorized', { status: 401 })\n  if (!user.roles.includes('admin')) return new Response('Forbidden', { status: 403 })\n  const users = await admin.listUsers()\n  return Response.json({ users })\n}\n```\n\n### CSRF: required for server-side auth endpoints\n\n> 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.\n\n```ts\n// netlify/functions/login.ts\nimport { login, verifyRequestOrigin } from '@netlify/identity'\nimport type { Context } from '@netlify/functions'\n\nexport default async (req: Request, context: Context) => {\n  verifyRequestOrigin(req)\n  const { email, password } = await req.json()\n  await login(email, password)\n  return new Response(null, { status: 302, headers: { Location: '/dashboard' } })\n}\n```\n\n## Identity event functions\n\nThe platform calls your handler when an Identity event occurs. Export a default object with a method per event. File: `netlify/functions/identity.mts`.\n\n> 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`.\n\n| Handler | Fires when |\n|---|---|\n| `userValidate` | Signup attempt, before account creation. Block bad signups here. |\n| `userSignup` | Signup completes (after email confirmation if enabled). Assign roles, sync, welcome. |\n| `userLogin` | User logs in. Track/last-seen/block. |\n| `userModified` | Profile updated. |\n| `userDeleted` | User deleted (notification only). |\n\nEvent `user` fields are camelCase (`appMetadata`, `userMetadata`, `confirmedAt`).\n\n```typescript\n// netlify/functions/identity.mts — deny a signup\nimport type { UserValidateEvent } from \"@netlify/functions\"\n\nexport default {\n  userValidate(event: UserValidateEvent) {\n    if (!event.user.email?.endsWith(\"@example.com\")) return event.deny()\n  },\n}\n```\n\n```typescript\n// netlify/functions/identity.mts — assign roles at signup\nimport type { UserSignupEvent } from \"@netlify/functions\"\n\nexport default {\n  userSignup(event: UserSignupEvent) {\n    return { user: { ...event.user, appMetadata: { ...event.user.appMetadata, roles: [\"member\"] } } }\n  },\n}\n```\n\n- `event.deny()` — rejects the action; end user gets `401`, no observability error. First handler to call it aborts the chain; later subscribers are not invoked. (Legacy filename functions signal denial with a non-2xx `Response` instead.)\n- Return `{ user: {...} }` to modify the record before persistence (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.\n- Background mode: `export const config: Config = { background: true }` — action completes immediately, handler runs async.\n\n## Roles & the JWT\n\n- `user.roles` is read from `app_metadata.roles`, carried in the JWT (cookie `nf_jwt`; refresh via `nf_refresh`).\n- `user_metadata` — user-editable profile (`full_name`, `email`). `app_metadata` — app data incl. `roles`, not user-editable.\n\n> **Role changes are NOT immediate.** They take effect on next login or token refresh. Changing roles does not invalidate the current JWT. Force it with `refreshSession()`.\n\nSet roles for existing users via `admin.updateUser()` in a Netlify Function; at signup via the `userSignup` event handler above.\n\nDeep guides for SSR/session hydration and authorization live in `references/advanced-patterns.md` and `references/authorization-and-sessions.md`.\n\n## CDN-edge RBAC (redirect rules)\n\nEnforced 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.**\n\n`_redirects`:\n```\n/admin/*  /admin/:splat  200!  Role=admin\n/admin/*  /login         401!\n# Multiple roles chained with commas:\n/private/* /private/:splat 200! Role=editor,admin\n```\n\n`netlify.toml`:\n```toml\n[[redirects]]\n  from = \"/admin/*\"\n  to = \"/admin/:splat\"\n  force = true\n  status = 200\n  conditions = {Role = [\"editor\", \"admin\"]}\n```\n\nUse redirect rules for path-based gating; use function-based `user.roles` checks for custom authorization logic.\n\n## Configuration (dashboard-only)\n\nBase: `https://app.netlify.com/projects/{site_name}/identity`. Enable with **Enable Identity**. Identity requires HTTPS — set up SSL before integrating on a custom domain.\n\n- **Registration** (`?tab=registration#registration-preferences`): **Open** (default, anyone can sign up) or **Invite only** (all users, including external-provider logins, must be invited first).\n- **Confirmation / autoconfirm** (`?tab=emails#confirmation-template`): check the box to skip email verification.\n- **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 client ID + secret, and enter them **in the Netlify settings UI** — not in code.\n- **Invitations** (`?tab=users`): enter addresses to send invites; link carries `invite_token`.\n- **Password recovery**: user page → **Send reset password email**; link carries `recovery_token`.\n\n### Emails (Pro plans or higher)\n\nDefault sender is `no-reply@netlify.com`. Custom SMTP sender and custom templates both require **Pro plans or higher**.\n\nTemplate variables (Go syntax): `{{ .Email }}`, `{{ .NewEmail }}` (email-change only), `{{ .SiteURL }}`, `{{ .ConfirmationURL }}`, `{{ .Token }}`.\n\nCustom-link hash fragments per action:\n```\n{{ .SiteURL }}/path/#invite_token={{ .Token }}\n{{ .SiteURL }}/path/#confirmation_token={{ .Token }}\n{{ .SiteURL }}/path/#recovery_token={{ .Token }}\n{{ .SiteURL }}/path/#email_change_token={{ .Token }}\n```\n\nCustom template constraints: inline CSS only; absolute image links; **no `<html>`/`<head>`/`<body>` tags**; ensure your build doesn't alter Go template variables.\n\n### Audit log (Pro plans or higher)\n\n`?tab=audit-log`. Search with a scoped term: `author:[string]` or `action:[string]`. Action names: `login`, `logout`, `user_signedup`, `user_deleted`, `user_modified`, `token_revoked`, `token_refreshed`, `user_recovery_requested`, `user_invited`.\n\n## External JWT providers (Enterprise)\n\nAvailable 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.\n\n- Roles path: Netlify Identity `app_metadata.roles`; external provider `app_metadata.authorization.roles`. Custom path → contact support.\n- JWT header must be `{\"alg\": \"HS256\", \"typ\": \"JWT\"}` (HS256 required). Payload `exp` is required and must be a future Unix Epoch time.\n- Set the JWT secret at `Project configuration > General > Visitor access > JWT secret`. Project-level overrides team-level defaults.\n\n## On failure — stop, don't guess\n\nIf callbacks 404, `/.netlify/identity/*` is unreachable, or an OAuth flow never returns: surface the error, the dashboard URL (`https://app.netlify.com/projects/{site_name}/identity`), and the setting to check (registration preference, external provider config, confirmation/autoconfirm). Then stop. Do not invent recovery commands. Remember: Identity does not work under `netlify dev` — confirm you are testing on a deploy.\n\nSite-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.\n\n<!-- gap: getSettings() is referenced by house rules for provider discovery but its signature/return shape is not documented in the intermediate. -->\n\n<!-- system: agent-context/identity/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (identity)\n\nThese are org conventions, not docs facts — merged into the rendered skill by\nctx-gen and never generated. Owned by the skills maintainer.\n\n1. Deep guides live in this skill: `references/advanced-patterns.md`\n   (SSR/session hydration) and `references/authorization-and-sessions.md`.\n2. Identity does not work under `netlify dev` — test auth flows on deploys\n   (Deploy Previews work).\n3. Identity configuration has no public API — it is dashboard-only. Never curl\n   `api.netlify.com` to flip or inspect Identity settings, never read auth\n   tokens from `~/Library/Preferences/netlify/config.json`, never probe for\n   undocumented endpoints.\n4. On failure (callback 404s, `/.netlify/identity/*` unreachable, OAuth flow\n   doesn't return), surface the error, the dashboard URL, and the setting to\n   check — then stop. Do not invent recovery commands.\n5. Never build a from-scratch third-party OAuth flow when Identity is in play —\n   no provider app registration, no `client_id`/`secret` in code, no custom\n   callback token exchange. Use `oauthLogin()` + `handleAuthCallback()`;\n   raw OAuth beside Identity is the single most common source of rework.\n6. Server-side `getUser()`/`login()`/`admin.*` require modern v2 functions\n   (`export default`) — v1 `export { handler }` is not supported. Typed\n   Identity event handlers (`UserSignupEvent`, `event.deny()`) require\n   `@netlify/functions` ≥ 5.2.0; older installs use the legacy filenames.\n7. Don't hard-code which auth providers exist — call `getSettings()` at\n   startup and render the signup form and OAuth buttons from what it returns.\n8. Site-gating requests (\"lock this site to my company\", employees-only)\n   route to the netlify-access-control skill first — Identity is the\n   app-level user layer only.\n9. Any answer that assigns or changes roles — at signup, via `admin.*`, or in\n   the dashboard — must say the change takes effect on the user's next login\n   or token refresh, not immediately. Keep that sentence next to the code that\n   sets the role, not only in a separate JWT section: an agent answering a\n   signup question reads the signup example and stops, and it has shipped\n   answers that omit the delay.\n"},"changes":[{"path":"/description","type":"changed","before":"Use when the task involves authentication, user signups, logins, password recovery, OAuth providers, role-based access control, or protecting routes and functions. Always use `@netlify/identity`. Never use `netlify-identity-widget` or `gotrue-js` — they are deprecated.","after":"Add user authentication to a Netlify site with @netlify/identity — 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, assigning roles at signup, or customizing Identity emails. For locking a whole site to your company or employees-only access, use netlify-access-control instead."},{"path":"/included_files","type":"changed","before":[{"relative_path":"LICENSE.txt","size_in_bytes":10776},{"relative_path":"agents/openai.yaml","size_in_bytes":335},{"relative_path":"assets/netlify-small.svg","size_in_bytes":1291},{"relative_path":"assets/netlify.png","size_in_bytes":2686},{"relative_path":"references/advanced-patterns.md","size_in_bytes":4095}],"after":[{"relative_path":"references/advanced-patterns.md","size_in_bytes":4428},{"relative_path":"references/authorization-and-sessions.md","size_in_bytes":3384}]},{"path":"/skill_md_contents","type":"changed","before":"---\nname: netlify-identity\ndescription: Use when the task involves authentication, user signups, logins, password recovery, OAuth providers, role-based access control, or protecting routes and functions. Always use `@netlify/identity`. Never use `netlify-identity-widget` or `gotrue-js` — they are deprecated.\n---\n\n# Netlify Identity\n\nNetlify Identity is a user management service for signups, logins, password recovery, user metadata, and role-based access control. It is built on [GoTrue](https://github.com/netlify/gotrue) and issues JSON Web Tokens (JWTs).\n\n**Always use `@netlify/identity`.** Never use `netlify-identity-widget` or `gotrue-js` — they are deprecated. `@netlify/identity` provides a unified, headless TypeScript API that works in both browser and server contexts (Netlify Functions, Edge Functions, SSR frameworks).\n\n## Setup\n\n```bash\nnpm install @netlify/identity\n```\n\nIdentity is automatically enabled when a deploy created by a Netlify Agent Runner session includes Identity code. Otherwise, it must be manually enabled in the UI. These are the default settings:\n\n- **Registration** — Open (anyone can sign up). Change to Invite only in **Project configuration > Identity** if needed.\n- **Autoconfirm** — Off (new signups require email confirmation). Enable in **Project configuration > Identity** to skip confirmation during development.\n\n### Local Development\n\nIdentity does **not** currently work with `netlify dev`. You must deploy to Netlify to test Identity features. Use `npx netlify deploy` for preview deploys during development. This limitation may be resolved in a future release.\n\n## Quick Start\n\nLog in from the browser:\n\n```typescript\nimport { login, getUser } from '@netlify/identity'\n\nconst user = await login('user@example.com', '<password>')\nconsole.log(`Hello, ${user.name}`)\n\n// Later, check auth state\nconst currentUser = await getUser()\n```\n\nProtect a Netlify Function:\n\n```typescript\n// netlify/functions/protected.mts\nimport { getUser } from '@netlify/identity'\nimport type { Context } from '@netlify/functions'\n\nexport default async (req: Request, context: Context) => {\n  const user = await getUser()\n  if (!user) return new Response('Unauthorized', { status: 401 })\n  return Response.json({ id: user.id, email: user.email })\n}\n```\n\n## Core API\n\nImport and use headless functions directly:\n\n```typescript\nimport {\n  getUser,\n  handleAuthCallback,\n  login,\n  logout,\n  signup,\n  oauthLogin,\n  onAuthChange,\n  getSettings,\n} from '@netlify/identity'\n```\n\n### Login\n\n```typescript\nimport { login, AuthError } from '@netlify/identity'\n\nasync function handleLogin(email: string, password: string) {\n  try {\n    const user = await login(email, password)\n    showSuccess(`Welcome back, ${user.name ?? user.email}`)\n  } catch (error) {\n    if (error instanceof AuthError) {\n      showError(error.status === 401 ? 'Invalid email or password.' : error.message)\n    }\n  }\n}\n```\n\n### Signup\n\nAfter signup, check `user.emailVerified` to determine if the user was auto-confirmed or needs to confirm their email.\n\n```typescript\nimport { signup, AuthError } from '@netlify/identity'\n\nasync function handleSignup(email: string, password: string, name: string) {\n  try {\n    const user = await signup(email, password, { full_name: name })\n    if (user.emailVerified) {\n      // Autoconfirm ON — user is logged in immediately\n      showSuccess('Account created. You are now logged in.')\n    } else {\n      // Autoconfirm OFF — confirmation email sent\n      showSuccess('Check your email to confirm your account.')\n    }\n  } catch (error) {\n    if (error instanceof AuthError) {\n      showError(error.status === 403 ? 'Signups are not allowed.' : error.message)\n    }\n  }\n}\n```\n\n### Logout\n\n```typescript\nimport { logout } from '@netlify/identity'\n\nawait logout()\n```\n\n### OAuth\n\nOAuth is a two-step flow: `oauthLogin(provider)` redirects away from the site, then `handleAuthCallback()` processes the redirect when the user returns.\n\n```typescript\nimport { oauthLogin } from '@netlify/identity'\n\n// Step 1: Redirect to provider (navigates away — never returns)\nfunction handleOAuthClick(provider: 'google' | 'github' | 'gitlab' | 'bitbucket') {\n  oauthLogin(provider)\n}\n```\n\nEnable providers in **Project configuration > Identity > External providers** before using OAuth.\n\n### Handling Callbacks\n\nAlways call `handleAuthCallback()` on page load in any app that uses OAuth, password recovery, invites, or email confirmation. It processes all callback types via the URL hash.\n\n```typescript\nimport { handleAuthCallback, AuthError } from '@netlify/identity'\n\nasync function processCallback() {\n  try {\n    const result = await handleAuthCallback()\n    if (!result) return // No callback hash — normal page load\n\n    switch (result.type) {\n      case 'oauth':\n        showSuccess(`Logged in as ${result.user?.email}`)\n        break\n      case 'confirmation':\n        showSuccess('Email confirmed. You are now logged in.')\n        break\n      case 'recovery':\n        // User is authenticated but must set a new password\n        showPasswordResetForm(result.user)\n        break\n      case 'invite':\n        // User must set a password to accept the invite\n        showInviteAcceptForm(result.token)\n        break\n      case 'email_change':\n        showSuccess('Email address updated.')\n        break\n    }\n  } catch (error) {\n    if (error instanceof AuthError) showError(error.message)\n  }\n}\n```\n\n### Auth State\n\n```typescript\nimport { getUser, onAuthChange, AUTH_EVENTS } from '@netlify/identity'\n\n// Check current user (never throws — returns null if not authenticated)\nconst user = await getUser()\n\n// Subscribe to auth state changes (returns unsubscribe function)\nconst unsubscribe = onAuthChange((event, user) => {\n  switch (event) {\n    case AUTH_EVENTS.LOGIN:\n      console.log('Logged in:', user?.email)\n      break\n    case AUTH_EVENTS.LOGOUT:\n      console.log('Logged out')\n      break\n    case AUTH_EVENTS.TOKEN_REFRESH:\n      break\n    case AUTH_EVENTS.USER_UPDATED:\n      console.log('Profile updated:', user?.email)\n      break\n    case AUTH_EVENTS.RECOVERY:\n      console.log('Password recovery initiated')\n      break\n  }\n})\n```\n\n### Settings-Driven UI\n\nFetch the project's Identity settings to conditionally render signup forms and OAuth buttons.\n\n```typescript\nimport { getSettings } from '@netlify/identity'\n\nconst settings = await getSettings()\n// settings.autoconfirm — boolean\n// settings.disableSignup — boolean\n// settings.providers — Record<AuthProvider, boolean>\n\nif (!settings.disableSignup) showSignupForm()\n\nfor (const [provider, enabled] of Object.entries(settings.providers)) {\n  if (enabled) showOAuthButton(provider)\n}\n```\n\n## Minimal React Example\n\n```tsx\nimport { useEffect, useState } from 'react'\nimport {\n  getUser,\n  handleAuthCallback,\n  login,\n  logout,\n  oauthLogin,\n  onAuthChange,\n} from '@netlify/identity'\n\nfunction App() {\n  const [user, setUser] = useState(null)\n  const [loading, setLoading] = useState(true)\n\n  useEffect(() => {\n    ;(async () => {\n      await handleAuthCallback()\n      setUser(await getUser())\n      setLoading(false)\n    })()\n    return onAuthChange((_event, currentUser) => setUser(currentUser))\n  }, [])\n\n  const handleLogin = async (email, password) => {\n    const currentUser = await login(email, password)\n    setUser(currentUser)\n  }\n\n  const handleGoogleLogin = () => oauthLogin('google')\n\n  const handleSignOut = async () => {\n    await logout()\n    setUser(null)\n  }\n\n  if (loading) return <p>Loading...</p>\n  // Render login form or user details based on `user` state\n}\n```\n\n## Error Handling\n\n`@netlify/identity` throws two error classes:\n\n- **`AuthError`** — Thrown by auth operations. Has `message`, optional `status` (HTTP status code), and optional `cause`.\n- **`MissingIdentityError`** — Thrown when Identity is not configured in the current environment.\n\n`getUser()` and `isAuthenticated()` never throw — they return `null` and `false` respectively on failure.\n\n| Status | Meaning |\n|--------|---------|\n| 401 | Invalid credentials or expired token |\n| 403 | Action not allowed (e.g., signups disabled) |\n| 422 | Validation error (e.g., weak password, malformed email) |\n| 404 | User or resource not found |\n\n## Identity Event Functions\n\nSpecial serverless functions that trigger on Identity lifecycle events. These use the **legacy named `handler` export** (not the modern default export).\n\n**Event names:** `identity-validate`, `identity-signup`, `identity-login`\n\n```typescript\n// netlify/functions/identity-signup.mts\nimport type { Handler, HandlerEvent, HandlerContext } from '@netlify/functions'\n\nconst handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {\n  const { user } = JSON.parse(event.body || '{}')\n\n  return {\n    statusCode: 200,\n    body: JSON.stringify({\n      app_metadata: {\n        ...user.app_metadata,\n        roles: ['member'],\n      },\n    }),\n  }\n}\n\nexport { handler }\n```\n\nThe response body replaces `app_metadata` and/or `user_metadata` on the user record — include all fields you want to keep.\n\n## Roles and Authorization\n\n- **`app_metadata.roles`** — Server-controlled. Only settable via the Netlify UI, admin API, or Identity event functions. Never let users set their own roles.\n- **`user_metadata`** — User-controlled. Users can update via `updateUser({ data: { ... } })`.\n\n### Role-Based Redirects\n\n```toml\n# netlify.toml\n[[redirects]]\n  from = \"/admin/*\"\n  to = \"/admin/:splat\"\n  status = 200\n  conditions = { Role = [\"admin\"] }\n\n[[redirects]]\n  from = \"/admin/*\"\n  to = \"/\"\n  status = 302\n```\n\nRules are evaluated top-to-bottom. The `nf_jwt` cookie is read by the CDN to evaluate role conditions.\n\n## Bundled References (Load As Needed)\n\n- [Advanced patterns](references/advanced-patterns.md) — password recovery, invite acceptance, email change, session hydration, SSR integration\n","after":"---\nname: netlify-identity\ndescription: Add user authentication to a Netlify site with @netlify/identity — 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, assigning roles at signup, or customizing Identity emails. For locking a whole site to your company or employees-only access, use netlify-access-control instead.\n---\n\n# Netlify Identity\n\nUse `@netlify/identity` (npm). For new projects it replaces the legacy `netlify-identity-widget` and `gotrue-js` — do not reach for those.\n\n```bash\nnpm install @netlify/identity\n```\n\nFramework 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).\n\n> **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.\n\n> **Identity config is dashboard-only — there is no public API.** Never curl `api.netlify.com` to flip or read Identity settings, never read tokens from local Netlify config, never probe undocumented endpoints. Enable and configure Identity at `https://app.netlify.com/projects/{site_name}/identity`.\n\n> **Never build a from-scratch OAuth flow alongside Identity.** No provider app registration in code, no `client_id`/`secret` in source, no custom callback token exchange. Use `oauthLogin()` + `handleAuthCallback()`. Raw OAuth beside Identity is the most common source of rework.\n\n## Client auth (browser)\n\n```ts\nimport { signup, login, logout, getUser, oauthLogin, handleAuthCallback } from '@netlify/identity'\n\n// Register — confirmation email sent by default (unless autoconfirm is on)\nconst user = await signup('jane@example.com', 'securepassword', { full_name: 'Jane Doe' })\n\n// Log in / out\nawait login('jane@example.com', 'securepassword')\nawait logout()\n\n// Current user or null\nconst current = await getUser()\nif (current) console.log(`Logged in as ${current.email}`)\n\n// External provider — redirects the browser; provider is one of\n// 'google' | 'github' | 'gitlab' | 'bitbucket'\noauthLogin('github')\n```\n\n> **`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:\n\n```ts\nimport { handleAuthCallback } from '@netlify/identity'\n\nconst result = await handleAuthCallback() // falsy if no token in URL hash\nif (result) console.log(result.type, result.user.email) // confirmation | invite | recovery | email change\n```\n\nAlternatives for a single token type: `recoverPassword()` (recovery), `acceptInvite()` (invite). Refresh a session with `refreshSession()`.\n\nDon't hard-code which providers exist. Call `getSettings()` at startup and render the signup form and OAuth buttons from what it returns.\n\n## Server-side auth (Netlify Functions & Edge Functions)\n\nServer-side `getUser()`/`login()`/`admin.*` require modern **v2 functions** (`export default`). The v1 `export { handler }` form is not supported.\n\n`getUser()` works in both runtimes. **`admin.*` runs ONLY in Netlify Functions — not the browser, not Edge Functions.**\n\n```ts\n// netlify/functions/me.ts — verify user\nimport { getUser } from '@netlify/identity'\nimport type { Context } from '@netlify/functions'\n\nexport default async (req: Request, context: Context) => {\n  const user = await getUser()\n  if (!user) return new Response('Unauthorized', { status: 401 })\n  return Response.json({ id: user.id, email: user.email })\n}\n```\n\nEdge Function form is identical but imports `Context` from `@netlify/edge-functions`.\n\n### Role checks\n\n```ts\n// netlify/functions/admin-users.ts\nimport { getUser, admin } from '@netlify/identity'\nimport type { Context } from '@netlify/functions'\n\nexport default async (req: Request, context: Context) => {\n  const user = await getUser()\n  if (!user) return new Response('Unauthorized', { status: 401 })\n  if (!user.roles.includes('admin')) return new Response('Forbidden', { status: 403 })\n  const users = await admin.listUsers()\n  return Response.json({ users })\n}\n```\n\n### CSRF: required for server-side auth endpoints\n\n> 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.\n\n```ts\n// netlify/functions/login.ts\nimport { login, verifyRequestOrigin } from '@netlify/identity'\nimport type { Context } from '@netlify/functions'\n\nexport default async (req: Request, context: Context) => {\n  verifyRequestOrigin(req)\n  const { email, password } = await req.json()\n  await login(email, password)\n  return new Response(null, { status: 302, headers: { Location: '/dashboard' } })\n}\n```\n\n## Identity event functions\n\nThe platform calls your handler when an Identity event occurs. Export a default object with a method per event. File: `netlify/functions/identity.mts`.\n\n> 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`.\n\n| Handler | Fires when |\n|---|---|\n| `userValidate` | Signup attempt, before account creation. Block bad signups here. |\n| `userSignup` | Signup completes (after email confirmation if enabled). Assign roles, sync, welcome. |\n| `userLogin` | User logs in. Track/last-seen/block. |\n| `userModified` | Profile updated. |\n| `userDeleted` | User deleted (notification only). |\n\nEvent `user` fields are camelCase (`appMetadata`, `userMetadata`, `confirmedAt`).\n\n```typescript\n// netlify/functions/identity.mts — deny a signup\nimport type { UserValidateEvent } from \"@netlify/functions\"\n\nexport default {\n  userValidate(event: UserValidateEvent) {\n    if (!event.user.email?.endsWith(\"@example.com\")) return event.deny()\n  },\n}\n```\n\n```typescript\n// netlify/functions/identity.mts — assign roles at signup\nimport type { UserSignupEvent } from \"@netlify/functions\"\n\nexport default {\n  userSignup(event: UserSignupEvent) {\n    return { user: { ...event.user, appMetadata: { ...event.user.appMetadata, roles: [\"member\"] } } }\n  },\n}\n```\n\n- `event.deny()` — rejects the action; end user gets `401`, no observability error. First handler to call it aborts the chain; later subscribers are not invoked. (Legacy filename functions signal denial with a non-2xx `Response` instead.)\n- Return `{ user: {...} }` to modify the record before persistence (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.\n- Background mode: `export const config: Config = { background: true }` — action completes immediately, handler runs async.\n\n## Roles & the JWT\n\n- `user.roles` is read from `app_metadata.roles`, carried in the JWT (cookie `nf_jwt`; refresh via `nf_refresh`).\n- `user_metadata` — user-editable profile (`full_name`, `email`). `app_metadata` — app data incl. `roles`, not user-editable.\n\n> **Role changes are NOT immediate.** They take effect on next login or token refresh. Changing roles does not invalidate the current JWT. Force it with `refreshSession()`.\n\nSet roles for existing users via `admin.updateUser()` in a Netlify Function; at signup via the `userSignup` event handler above.\n\nDeep guides for SSR/session hydration and authorization live in `references/advanced-patterns.md` and `references/authorization-and-sessions.md`.\n\n## CDN-edge RBAC (redirect rules)\n\nEnforced 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.**\n\n`_redirects`:\n```\n/admin/*  /admin/:splat  200!  Role=admin\n/admin/*  /login         401!\n# Multiple roles chained with commas:\n/private/* /private/:splat 200! Role=editor,admin\n```\n\n`netlify.toml`:\n```toml\n[[redirects]]\n  from = \"/admin/*\"\n  to = \"/admin/:splat\"\n  force = true\n  status = 200\n  conditions = {Role = [\"editor\", \"admin\"]}\n```\n\nUse redirect rules for path-based gating; use function-based `user.roles` checks for custom authorization logic.\n\n## Configuration (dashboard-only)\n\nBase: `https://app.netlify.com/projects/{site_name}/identity`. Enable with **Enable Identity**. Identity requires HTTPS — set up SSL before integrating on a custom domain.\n\n- **Registration** (`?tab=registration#registration-preferences`): **Open** (default, anyone can sign up) or **Invite only** (all users, including external-provider logins, must be invited first).\n- **Confirmation / autoconfirm** (`?tab=emails#confirmation-template`): check the box to skip email verification.\n- **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 client ID + secret, and enter them **in the Netlify settings UI** — not in code.\n- **Invitations** (`?tab=users`): enter addresses to send invites; link carries `invite_token`.\n- **Password recovery**: user page → **Send reset password email**; link carries `recovery_token`.\n\n### Emails (Pro plans or higher)\n\nDefault sender is `no-reply@netlify.com`. Custom SMTP sender and custom templates both require **Pro plans or higher**.\n\nTemplate variables (Go syntax): `{{ .Email }}`, `{{ .NewEmail }}` (email-change only), `{{ .SiteURL }}`, `{{ .ConfirmationURL }}`, `{{ .Token }}`.\n\nCustom-link hash fragments per action:\n```\n{{ .SiteURL }}/path/#invite_token={{ .Token }}\n{{ .SiteURL }}/path/#confirmation_token={{ .Token }}\n{{ .SiteURL }}/path/#recovery_token={{ .Token }}\n{{ .SiteURL }}/path/#email_change_token={{ .Token }}\n```\n\nCustom template constraints: inline CSS only; absolute image links; **no `<html>`/`<head>`/`<body>` tags**; ensure your build doesn't alter Go template variables.\n\n### Audit log (Pro plans or higher)\n\n`?tab=audit-log`. Search with a scoped term: `author:[string]` or `action:[string]`. Action names: `login`, `logout`, `user_signedup`, `user_deleted`, `user_modified`, `token_revoked`, `token_refreshed`, `user_recovery_requested`, `user_invited`.\n\n## External JWT providers (Enterprise)\n\nAvailable 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.\n\n- Roles path: Netlify Identity `app_metadata.roles`; external provider `app_metadata.authorization.roles`. Custom path → contact support.\n- JWT header must be `{\"alg\": \"HS256\", \"typ\": \"JWT\"}` (HS256 required). Payload `exp` is required and must be a future Unix Epoch time.\n- Set the JWT secret at `Project configuration > General > Visitor access > JWT secret`. Project-level overrides team-level defaults.\n\n## On failure — stop, don't guess\n\nIf callbacks 404, `/.netlify/identity/*` is unreachable, or an OAuth flow never returns: surface the error, the dashboard URL (`https://app.netlify.com/projects/{site_name}/identity`), and the setting to check (registration preference, external provider config, confirmation/autoconfirm). Then stop. Do not invent recovery commands. Remember: Identity does not work under `netlify dev` — confirm you are testing on a deploy.\n\nSite-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.\n\n<!-- gap: getSettings() is referenced by house rules for provider discovery but its signature/return shape is not documented in the intermediate. -->\n\n<!-- system: agent-context/identity/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (identity)\n\nThese are org conventions, not docs facts — merged into the rendered skill by\nctx-gen and never generated. Owned by the skills maintainer.\n\n1. Deep guides live in this skill: `references/advanced-patterns.md`\n   (SSR/session hydration) and `references/authorization-and-sessions.md`.\n2. Identity does not work under `netlify dev` — test auth flows on deploys\n   (Deploy Previews work).\n3. Identity configuration has no public API — it is dashboard-only. Never curl\n   `api.netlify.com` to flip or inspect Identity settings, never read auth\n   tokens from `~/Library/Preferences/netlify/config.json`, never probe for\n   undocumented endpoints.\n4. On failure (callback 404s, `/.netlify/identity/*` unreachable, OAuth flow\n   doesn't return), surface the error, the dashboard URL, and the setting to\n   check — then stop. Do not invent recovery commands.\n5. Never build a from-scratch third-party OAuth flow when Identity is in play —\n   no provider app registration, no `client_id`/`secret` in code, no custom\n   callback token exchange. Use `oauthLogin()` + `handleAuthCallback()`;\n   raw OAuth beside Identity is the single most common source of rework.\n6. Server-side `getUser()`/`login()`/`admin.*` require modern v2 functions\n   (`export default`) — v1 `export { handler }` is not supported. Typed\n   Identity event handlers (`UserSignupEvent`, `event.deny()`) require\n   `@netlify/functions` ≥ 5.2.0; older installs use the legacy filenames.\n7. Don't hard-code which auth providers exist — call `getSettings()` at\n   startup and render the signup form and OAuth buttons from what it returns.\n8. Site-gating requests (\"lock this site to my company\", employees-only)\n   route to the netlify-access-control skill first — Identity is the\n   app-level user layer only.\n9. Any answer that assigns or changes roles — at signup, via `admin.*`, or in\n   the dashboard — must say the change takes effect on the user's next login\n   or token refresh, not immediately. Keep that sentence next to the code that\n   sets the role, not only in a separate JWT section: an agent answering a\n   signup question reads the signup example and stops, and it has shipped\n   answers that omit the delay.\n"}],"summary":"Fields changed: 3. /description, /included_files, /skill_md_contents.","summary_kind":"deterministic","summary_metadata":{}}