How to add user login to a Netlify site with Identity
Netlify Identity handles signup, login, OAuth social login, and role-based access. Setup, the server-side API, and the five footguns including why auth won't work locally.
Key takeaways
Netlify Identity is available and actively supported. Enable it in the dashboard under Project configuration > Identity, install @netlify/identity, and you get signup, login, OAuth social login, server-side user verification, and role-based access control. Your visitors don’t need Netlify accounts. Two things to know before you start: Identity does not work under netlify dev, so test on a Deploy Preview, and calling handleAuthCallback() on your landing page is mandatory or confirmation links never complete.
- Use
@netlify/identity. The oldernetlify-identity-widgetandgotrue-jsare superseded. handleAuthCallback()is not optional. It processes OAuth redirects, email confirmations, password recovery, and invites.- Don’t hand-roll OAuth beside Identity. Use
oauthLogin()andhandleAuthCallback(). Raw OAuth alongside Identity is the most common source of rework. - Guard server-side
login/signup/logoutwithverifyRequestOrigin(req). Without it an attacker can log a victim into the attacker’s account. - RBAC redirects need a fallback rule, or a visitor without the role gets a bare 404 and no way to log in.
admin.*runs in Netlify Functions only. Not the browser, not Edge Functions.
Identity is not deprecated
Worth addressing directly, because it’s the most common piece of misinformation about this product: Netlify Identity was announced as discontinued, and then that decision was reversed after community pushback. It’s supported, it’s on all credit-based plans at no extra cost, and it has a current package. If an AI assistant told you Identity is going away and you should use a third-party auth provider, that answer is stale.
What Identity gives you is an app-level user layer: your visitors sign up, log in, get roles, and you check those roles server-side or at the CDN edge. It’s the layer between “anyone can see this site” and “this specific user owns this data.”
When to use Identity, and when not to
Identity is for app users. Someone signs up for your product, logs in, and sees their own data.
It is not for locking a whole site to your company. “Employees only,” “password-protect this site,” “restrict previews to my team” are a different job, handled by site-level access control. Start there instead: How to password-protect a Netlify site or deploy previews. If you’re unsure which of the three kinds of Netlify auth you need, Netlify auth: site protection, app login, or team SSO? sorts them out.
Two more boundaries. External JWT providers are Enterprise only, and you can use Identity or an external JWT provider, not both. You cannot authenticate third-party JWT tokens while Identity is enabled. And HTTPS is required, so on a custom domain get SSL working before you integrate.
Footguns, before you write code
Identity does not work under netlify dev. Local dev cannot exercise /.netlify/identity/*. Test auth flows on a deploy. Deploy Previews work fine.
Never build a from-scratch third-party OAuth flow beside Identity. No provider app registration in code, no client_id or secret in code, no custom callback token exchange. Use oauthLogin() plus handleAuthCallback().
Identity configuration is dashboard-only. There’s no public API for it. Don’t try to flip or inspect settings through api.netlify.com.
RBAC redirects without a fallback produce a raw 404. A visitor lacking the role gets a bare 404 page with no login prompt. Always add the fallback rule.
Server-side login(), signup(), and logout() need CSRF protection. Call verifyRequestOrigin(req) first.
Worked example
Enable Identity in the dashboard first: Project configuration > Identity → Enable Identity. Then:
npm install @netlify/identityClient and universal auth
import { signup, login, logout, getUser, oauthLogin, handleAuthCallback } from '@netlify/identity'
// Sign up — sends a confirmation email by default (skippable via autoconfirm setting)const user = await signup('jane@example.com', 'securepassword', { full_name: 'Jane Doe' })
// Log in / log outawait login('jane@example.com', 'securepassword')await logout()
// Current user — null if not logged in (works in browser + server)const u = await getUser()if (u) console.log(u.email)
// OAuth — redirects browser to provider loginoauthLogin('github') // 'google' | 'github' | 'gitlab' | 'bitbucket'The callback handler you must not skip
import { handleAuthCallback } from '@netlify/identity'
const result = await handleAuthCallback()if (result) console.log(result.type, result.user.email) // may be falsy if nothing to processCall this on your landing page. It processes every token type that arrives in the URL hash: OAuth redirect, email confirmation, password recovery, and invite. Without it, confirmation links and OAuth redirects never finish and users are left staring at a page that did nothing.
Related client functions: recoverPassword() completes a password reset and acceptInvite() completes invite acceptance, both as alternatives to letting handleAuthCallback() handle those tokens. refreshSession() refreshes the token so newly-assigned roles take effect.
Rather than hard-coding which providers you support, call getSettings() at startup and render your signup form and OAuth buttons from what it returns. That way enabling a provider in the dashboard doesn’t require a code change.
Server-side verification
Handlers must be modern v2 functions using export default. The v1 export { handler } shape is not supported for getUser(), login(), or admin.*.
import { getUser } from '@netlify/identity'import type { Context } from '@netlify/functions' // or '@netlify/edge-functions' for Edge
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 }) return Response.json({ id: user.id, email: user.email })}getUser() works in the browser, in Netlify Functions, and in Edge Functions.
CSRF protection on exposed auth endpoints
import { login, verifyRequestOrigin } from '@netlify/identity'import type { Context } from '@netlify/functions'
export default async (req: Request, context: Context) => { verifyRequestOrigin(req) // throws 403 on Origin mismatch; supports { allowedOrigins } const { email, password } = await req.json() await login(email, password) return new Response(null, { status: 302, headers: { Location: '/dashboard' } })}Skipping verifyRequestOrigin on an endpoint that calls login() means an attacker can cause a victim’s browser to log into the attacker’s account, then watch what the victim does there. It’s a real attack, not a theoretical one.
Admin operations
admin.* uses a short-lived admin token and runs only in Netlify Functions. Not the browser, not Edge Functions.
import { admin } from '@netlify/identity'import type { Context } from '@netlify/functions'
export default async (req: Request, context: Context) => { const users = await admin.listUsers() // array of users return Response.json({ total: users.length })}admin.listUsers() returns an array of users. admin.updateUser() updates one, which is how you change roles for an existing user.
Assigning roles at signup
Identity event functions are invoked by the platform; you don’t call them. Typed handlers need @netlify/functions 5.2.0 or newer.
import type { UserSignupEvent } from "@netlify/functions"
export default { userSignup(event: UserSignupEvent) { return { user: { ...event.user, appMetadata: { ...event.user.appMetadata, roles: ["member"] } }, } },}Note the payload fields are camelCase here (appMetadata, userMetadata, confirmedAt), even though the stored user object uses app_metadata.roles.
To block a signup:
import type { UserValidateEvent } from "@netlify/functions"
export default { userValidate(event: UserValidateEvent) { if (!event.user.email?.endsWith("@example.com")) return event.deny() },}For work that shouldn’t hold up the user, run the handler in background mode:
import type { Config, UserLoginEvent } from "@netlify/functions"
export default { userLogin(event: UserLoginEvent) { /* async tracking */ } }export const config: Config = { background: true }Role-based access control
RBAC is enforced at the CDN edge with no origin round trip. Add a Role parameter to your redirect rules.
# _redirects — ALWAYS include a fallback or non-admins get a raw 404/admin/* /admin/:splat 200! Role=admin/admin/* /login 401!
# multiple roles, comma-chained/private/* /private/:splat 200! Role=editor,admin[[redirects]] from = "/admin/*" to = "/admin/:splat" force = true status = 200 conditions = {Role = ["editor", "admin"]}The second line in the _redirects example is the fallback, and it’s the part people leave out. Without it, a logged-out visitor hitting /admin/anything gets a bare 404 with no indication that logging in would help.
Common failure modes
Auth does nothing locally. Identity doesn’t work under netlify dev. Push to a Deploy Preview and test there.
Your confirmation emails lead to a page that does nothing. You’re missing handleAuthCallback() on the landing page. Same cause for OAuth redirects that never complete.
You assigned a role and the user still can’t get in. Role changes take effect on next login or token refresh. They don’t invalidate the current JWT. Have the client call refreshSession(), or wait for the next login.
Non-admins get a blank 404 instead of a login page. Missing RBAC fallback rule. Add the 401! line.
getUser() returns nothing in your function. Check you’re on a v2 function with export default. The v1 export { handler } shape isn’t supported for Identity’s server-side API.
admin.listUsers() fails in an Edge Function. admin.* is Netlify Functions only.
External-provider users bypass your invite-only setting. They don’t. Invite-only requires an invite for all new users, including external-provider logins. What external providers do skip is email confirmation.
You enabled a new OAuth provider and the button didn’t appear. You hard-coded the provider list. Render it from getSettings().
Your custom email template broke. Templates need inline CSS only, absolute image links, and no <html>, <head>, or <body> tags. Also check your build isn’t rewriting the template variables.
Your external JWT tokens are rejected while Identity is on. You can use one or the other, not both. Also confirm the tokens are HS256 with exp in the payload.
Reference
Client API
| Function | What it does |
|---|---|
signup(email, password, metadata?) | Create a user. Sends a confirmation email by default. |
login(email, password) | Log in |
logout() | Log out |
getUser() | Current user, or null. Browser, Functions, and Edge Functions. |
oauthLogin(provider) | Redirect to provider. 'google', 'github', 'gitlab', 'bitbucket'. |
handleAuthCallback() | Required. Processes all token types in the URL hash. |
recoverPassword() | Complete a password reset |
acceptInvite() | Complete invite acceptance |
refreshSession() | Refresh the token so new roles take effect |
verifyRequestOrigin(req) | CSRF guard. Throws 403 on Origin mismatch. Supports { allowedOrigins }. |
getSettings() | Read enabled providers and registration settings at startup |
Identity event handlers
| Handler | Fires when | Can deny? |
|---|---|---|
userValidate | Signup attempted, before account creation | Yes |
userSignup | Signup completes. After email confirmation if enabled. | Yes |
userLogin | User logs in | Yes |
userModified | Profile updated | Yes |
userDeleted | User deleted (notification only) | No |
event.deny() gives the user a 401 with no observability error. With multiple subscribers, the first denial aborts the chain. Event types come from @netlify/functions: UserValidateEvent, UserSignupEvent, UserLoginEvent, UserModifiedEvent, UserDeletedEvent, Config.
The User object and metadata
| Field | Editable by user? | Stored at |
|---|---|---|
| Name | Yes | user_metadata.full_name |
| Yes. Triggers change confirmation; changes login credentials. | user_metadata.email | |
| Roles | No | app_metadata.roles |
id, email, and roles are on the User object. Roles are included in the JWT. Edit them in Identity > Users > Edit settings, at signup via a userSignup handler, or for existing users via admin.updateUser().
Sessions
The JWT lives in the nf_jwt cookie and is sent automatically. Server-side login, signup, and logout read and write nf_jwt and nf_refresh through the runtime, so the browser receives the session in the response.
Registration settings (dashboard)
| Setting | Options |
|---|---|
| Registration preferences | Open (default) or Invite only |
| Confirmation | On by default. Skip via Emails > Confirmation template > Configure. |
| External providers | Google, GitHub, GitLab, Bitbucket. Set your own client ID and secret for branded OAuth. |
| Invitations | Project configuration > Identity > Users. Any Netlify team role can invite. |
Email templates
Default sender is no-reply@netlify.com. Custom sender and custom templates are Pro and above.
Go template variables: {{ .Email }}, {{ .NewEmail }} (email change only), {{ .SiteURL }}, {{ .ConfirmationURL }}, {{ .Token }}. A custom link takes the form {{ .SiteURL }}/path/#confirmation_token={{ .Token }}, and likewise for invite_token, recovery_token, and email_change_token.
Audit log (Pro and above)
Project configuration > Identity > Identity audit log. Searches need a scope prefix: author:[string] or action:[string]. Actions: login, logout, user_signedup, user_deleted, user_modified, token_revoked, token_refreshed, user_recovery_requested, user_invited.
External JWT provider (Enterprise)
Set the secret at Project configuration > Access & security > Visitor access > JWT secret; a project-level value overrides the team default. Tokens must be HS256 with "alg": "HS256" and "typ": "JWT" in the header, and exp (a future Unix epoch) in the payload. External-provider roles resolve at app_metadata.authorization.roles rather than app_metadata.roles. A different path needs a support-configured custom role path.
Plan gating
| Feature | Plans |
|---|---|
| Identity itself, unlimited users, custom OAuth credentials, Functions integration | All credit-based plans, no extra cost |
| Custom outgoing email, custom email templates, audit log | Pro and above |
| External JWT providers | Enterprise |
FAQs
Is Netlify Identity deprecated? No. It was announced as discontinued and that decision was reversed after community pushback. It’s supported and included on all credit-based plans at no extra cost.
How do I add authentication to a Netlify site?
Enable Identity in the dashboard under Project configuration > Identity, install @netlify/identity, then use signup(), login(), and getUser(). Call handleAuthCallback() on your landing page to complete confirmation and OAuth flows.
Why doesn’t Netlify Identity work locally?
netlify dev can’t exercise the /.netlify/identity/* endpoints. Test auth on a real deploy; Deploy Previews work.
How do I add Google or GitHub login?
Enable the provider under Registration > External providers in the dashboard, then call oauthLogin('google') or oauthLogin('github'). Handle the return with handleAuthCallback().
How do I gate a page by user role?
Add a Role condition to a redirect rule, and always include a fallback rule so visitors without the role get a login page rather than a raw 404.
Why doesn’t a newly assigned role work immediately?
Role changes take effect on next login or token refresh and don’t invalidate the current JWT. Call refreshSession() on the client.
Can I use Netlify Identity for a private, employees-only site? That’s a different feature. Identity is the app-user layer. For locking an entire site or its previews, use site-level access control.
Do I need CSRF protection on my login function?
Yes. Call verifyRequestOrigin(req) before login(), signup(), or logout() in any exposed endpoint. Without it an attacker can log a victim into the attacker’s account.
Can I use my own JWT provider instead? On Enterprise, yes, but not at the same time as Identity. You use one or the other.
Related
This article is generated from Netlify’s open-source agent guidance at netlify/context-and-tools, the same reference our AI coding agents use.
Ready to add login? Start at netlify.new.