netlify/context-and-tools3 files

Netlify Identity

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.

Specification
Skill ID
netlify/context-and-tools/netlify-identity
Publisher
netlify
Repository
context-and-tools
Installs
0
Files
3
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

netlify/context-and-tools/netlify-identityInstalls these files
  • SKILL.md
  • references/advanced-patterns.md
  • references/authorization-and-sessions.md

What this skill tells the agent

Netlify Identity

Use @netlify/identity (npm). For new projects it replaces the legacy netlify-identity-widget and gotrue-js — do not reach for those.

npm install @netlify/identity

Framework examples (Next.js/Astro/Remix/SvelteKit) and the full API reference are in the `@netlify/identity` README on npm.

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.
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.
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.

Client auth (browser)

import { signup, login, logout, getUser, oauthLogin, handleAuthCallback } from '@netlify/identity'

// Register — confirmation email sent by default (unless autoconfirm is on)
const user = await signup('jane@example.com', 'securepassword', { full_name: 'Jane Doe' })

// Log in / out
await login('jane@example.com', 'securepassword')
await logout()

// Current user or null
const current = await getUser()
if (current) console.log(`Logged in as ${current.email}`)

// External provider — redirects the browser; provider is one of
// 'google' | 'github' | 'gitlab' | 'bitbucket'
oauthLogin('github')
`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:
import { handleAuthCallback } from '@netlify/identity'

const result = await handleAuthCallback() // falsy if no token in URL hash
if (result) console.log(result.type, result.user.email) // confirmation | invite | recovery | email change

Alternatives for a single token type: recoverPassword() (recovery), acceptInvite() (invite). Refresh a session with refreshSession().

Don't hard-code which providers exist. Call getSettings() at startup and render the signup form and OAuth buttons from what it returns.

Server-side auth (Netlify Functions & Edge Functions)

Server-side getUser()/login()/admin.* require modern v2 functions (export default). The v1 export { handler } form is not supported.

getUser() works in both runtimes. *`admin.` runs ONLY in Netlify Functions — not the browser, not Edge Functions.**

// netlify/functions/me.ts — verify user
import { getUser } from '@netlify/identity'
import type { Context } from '@netlify/functions'

export default async (req: Request, context: Context) => {
  const user = await getUser()
  if (!user) return new Response('Unauthorized', { status: 401 })
  return Response.json({ id: user.id, email: user.email })
}

Edge Function form is identical but imports Context from @netlify/edge-functions.

Role checks

// netlify/functions/admin-users.ts
import { getUser, admin } from '@netlify/identity'
import type { Context } from '@netlify/functions'

export default async (req: Request, context: Context) => {
  const user = await getUser()
  if (!user) return new Response('Unauthorized', { status: 401 })
  if (!user.roles.includes('admin')) return new Response('Forbidden', { status: 403 })
  const users = await admin.listUsers()
  return Response.json({ users })
}

CSRF: required for server-side auth endpoints

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.
// netlify/functions/login.ts
import { login, verifyRequestOrigin } from '@netlify/identity'
import type { Context } from '@netlify/functions'

export default async (req: Request, context: Context) => {
  verifyRequestOrigin(req)
  const { email, password } = await req.json()
  await login(email, password)
  return new Response(null, { status: 302, headers: { Location: '/dashboard' } })
}

Identity event functions

The platform calls your handler when an Identity event occurs. Export a default object with a method per event. File: netlify/functions/identity.mts.

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.
HandlerFires when
userValidateSignup attempt, before account creation. Block bad signups here.
userSignupSignup completes (after email confirmation if enabled). Assign roles, sync, welcome.
userLoginUser logs in. Track/last-seen/block.
userModifiedProfile updated.
userDeletedUser deleted (notification only).

Event user fields are camelCase (appMetadata, userMetadata, confirmedAt).

// netlify/functions/identity.mts — deny a signup
import type { UserValidateEvent } from "@netlify/functions"

export default {
  userValidate(event: UserValidateEvent) {
    if (!event.user.email?.endsWith("@example.com")) return event.deny()
  },
}
// netlify/functions/identity.mts — assign roles at signup
import type { UserSignupEvent } from "@netlify/functions"

export default {
  userSignup(event: UserSignupEvent) {
    return { user: { ...event.user, appMetadata: { ...event.user.appMetadata, roles: ["member"] } } }
  },
}
  • 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.)
  • 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.
  • Background mode: export const config: Config = { background: true } — action completes immediately, handler runs async.

Roles & the JWT

  • user.roles is read from app_metadata.roles, carried in the JWT (cookie nf_jwt; refresh via nf_refresh).
  • user_metadata — user-editable profile (full_name, email). app_metadata — app data incl. roles, not user-editable.
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().

Set roles for existing users via admin.updateUser() in a Netlify Function; at signup via the userSignup event handler above.

Deep guides for SSR/session hydration and authorization live in references/advanced-patterns.md and references/authorization-and-sessions.md.

CDN-edge RBAC (redirect rules)

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.

_redirects:

/admin/*  /admin/:splat  200!  Role=admin
/admin/*  /login         401!
# Multiple roles chained with commas:
/private/* /private/:splat 200! Role=editor,admin

netlify.toml:

[[redirects]]
  from = "/admin/*"
  to = "/admin/:splat"
  force = true
  status = 200
  conditions = {Role = ["editor", "admin"]}

Use redirect rules for path-based gating; use function-based user.roles checks for custom authorization logic.

Configuration (dashboard-only)

Base: https://app.netlify.com/projects/{site_name}/identity. Enable with Enable Identity. Identity requires HTTPS — set up SSL before integrating on a custom domain.

More from netlify

Use this skill in a project

Open any RiverX project, search the Skills panel for netlify/context-and-tools/netlify-identity, and install it in one click.

Use this skill