---
title: "How to add user login to a Netlify site with Identity | Netlify Knowledge Base"
description: "Guides and articles to help you get the most out of the Netlify platform."
source: "https://www.netlify.com/knowledge-base/how-to-add-user-login-to-a-netlify-site-with-identity/"
last_updated: "2026-09-23T19:52:53.000Z"
---
## 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 older `netlify-identity-widget` and `gotrue-js` are superseded.
-   **`handleAuthCallback()` is not optional.** It processes OAuth redirects, email confirmations, password recovery, and invites.
-   **Don’t hand-roll OAuth beside Identity.** Use `oauthLogin()` and `handleAuthCallback()`. Raw OAuth alongside Identity is the most common source of rework.
-   **Guard server-side `login`/`signup`/`logout` with `verifyRequestOrigin(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/identity
```

### Client 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 process
```

Call 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

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

netlify/functions/identity.mts

```
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:

netlify/functions/identity.mts

```
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:

netlify/functions/identity.mts

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

netlify.toml

```
[[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`

Email

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

-   [Team SAML SSO](https://docs.netlify.com/manage/security/secure-netlify-access/configure-team-saml-sso)
-   [Organization SAML SSO](https://docs.netlify.com/manage/security/secure-netlify-access/configure-organization-saml-sso)
-   [`@netlify/identity` on npm](https://www.npmjs.com/package/@netlify/identity)

This article is generated from Netlify’s open-source agent guidance at [netlify/context-and-tools](https://github.com/netlify/context-and-tools), the same reference our AI coding agents use.

* * *

Ready to add login? Start at [netlify.new](https://netlify.new).