@netlify/identity
v0.4.1
Published
Headless auth functions for Netlify Identity (not the widget). Import { login, getUser } and call them. No init, no class, no UI.
Readme
@netlify/identity
A lightweight, no-config headless authentication library for projects using Netlify Identity. Works in both browser and server contexts. This is NOT the Netlify Identity Widget. This library exports standalone async functions (e.g., import { login, getUser } from '@netlify/identity'). There is no class to instantiate and no .init() call. Just import the functions you need and call them.
Status: Beta. The API may change before 1.0.
Prerequisites:
- Netlify Identity must be enabled on your Netlify project
- Server-side functions (
getUser,login,admin.*, etc.) require Netlify Functions (modern/v2, withexport default) or Edge Functions. Lambda-compatible functions (v1, withexport { handler }) are not supported - For local development, use
netlify devso the Identity endpoint is available
How this library relates to other Netlify auth packages
| Package | What it is | When to use it |
| ------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------ |
| @netlify/identity (this library) | Headless TypeScript API for browser and server | You want full control over your auth UI and need server-side auth (SSR, Netlify Functions) |
| netlify-identity-widget | Pre-built login/signup modal (HTML + CSS) | You want a drop-in UI component with no custom design |
| gotrue-js | Low-level GoTrue HTTP client (browser only) | You're building your own auth wrapper and need direct API access |
This library provides a unified API that works in both browser and server contexts, handles cookie management, and normalizes the user object. You do not need to install gotrue-js or the widget separately.
Table of contents
- Installation
- Quick start
- API
- Functions --
getUser,login,signup,logout,oauthLogin,handleAuthCallback,onAuthChange,hydrateSession,refreshSession, and more - Admin Operations --
admin.listUsers,admin.getUser,admin.createUser,admin.updateUser,admin.deleteUser - Types --
User,AuthEvent,CallbackResult,Settings,Admin,ListUsersOptions,CreateUserParams, etc. - Errors --
AuthError,MissingIdentityError
- Functions --
- Framework integration -- Next.js, Remix, TanStack Start, Astro, SvelteKit
- Guides
Installation
npm install @netlify/identityQuick start
Log in (browser)
import { login, getUser } from '@netlify/identity'
// Log in
const user = await login('[email protected]', 'password123')
console.log(`Hello, ${user.name}`)
// Later, check auth state
const currentUser = await getUser()Protect a Netlify Function
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 })
}Protect an Edge Function
import { getUser } from '@netlify/identity'
import type { Context } from '@netlify/edge-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 })
}API
Functions
getUser
getUser(): Promise<User | null>Returns the current authenticated user, or null if not logged in. Returns the best available normalized User from the current context. In the browser or when the server can reach the Identity API, all fields are populated. When falling back to JWT claims (e.g., Identity API unreachable), fields like createdAt, updatedAt, emailVerified, and rawGoTrueData may be missing. Never throws.
Next.js note: Calling
getUser()in a Server Component opts the page into dynamic rendering because it reads cookies. This is expected and correct for authenticated pages. Next.js handles the internal dynamic rendering signal automatically.
isAuthenticated
isAuthenticated(): Promise<boolean>Returns true if a user is currently authenticated. Equivalent to (await getUser()) !== null. Never throws.
getIdentityConfig
getIdentityConfig(): IdentityConfig | nullReturns the Identity endpoint URL (and operator token on the server), or null if Identity is not available. Never throws.
getSettings
getSettings(): Promise<Settings>Fetches your project's Identity settings (enabled providers, autoconfirm, signup disabled).
Throws: MissingIdentityError if Identity is not configured. AuthError if the endpoint is unreachable.
login
login(email: string, password: string): Promise<User>Logs in with email and password. Works in both browser and server contexts.
In the browser, emits a 'login' event. On the server (Netlify Functions, Edge Functions), calls the Identity API directly and sets the nf_jwt cookie via the Netlify runtime.
Throws: AuthError on invalid credentials or network failure. In the browser, MissingIdentityError if Identity is not configured. On the server, AuthError if the Netlify Functions runtime is not available.
signup
signup(email: string, password: string, data?: SignupData): Promise<User>Creates a new account. Works in both browser and server contexts.
If autoconfirm is enabled in your Identity settings, the user is logged in immediately: cookies are set and a 'login' event is emitted. If autoconfirm is disabled (the default), the user receives a confirmation email and must click the link before they can log in. In that case, no cookies are set and no auth event is emitted.
The optional data parameter sets user metadata (e.g., { full_name: 'Jane Doe' }), stored in the user's user_metadata field.
Throws: AuthError on failure (e.g., email already registered, signup disabled). In the browser, MissingIdentityError if Identity is not configured. On the server, AuthError if the Netlify Functions runtime is not available.
logout
logout(): Promise<void>Logs out the current user and clears the session. Works in both browser and server contexts.
In the browser, emits a 'logout' event. On the server, calls the Identity /logout endpoint with the JWT from the nf_jwt cookie, then deletes the cookie. Auth cookies are always cleared, even if the server call fails.
Throws: In the browser, MissingIdentityError if Identity is not configured. On the server, AuthError if the Netlify Functions runtime is not available.
oauthLogin
oauthLogin(provider: string): neverRedirects to an OAuth provider. The page navigates away, so this function never returns normally. Browser only.
The provider argument should be one of the AuthProvider values: 'google', 'github', 'gitlab', 'bitbucket', 'facebook', or 'saml'.
Throws: MissingIdentityError if Identity is not configured. AuthError if called on the server.
handleAuthCallback
handleAuthCallback(): Promise<CallbackResult | null>Processes the URL hash after an OAuth redirect, email confirmation, password recovery, invite acceptance, or email change. Call on page load. Returns null if the hash contains no auth parameters. Browser only.
Throws: MissingIdentityError if Identity is not configured. AuthError if token exchange fails.
onAuthChange
onAuthChange(callback: AuthCallback): () => voidSubscribes to auth state changes (login, logout, token refresh, user updates, and recovery). Returns an unsubscribe function. Also fires on cross-tab session changes. No-op on the server. The 'recovery' event fires when handleAuthCallback() processes a password recovery token; listen for it to redirect users to a password reset form.
hydrateSession
hydrateSession(): Promise<User | null>Bootstraps the browser-side session from server-set auth cookies (nf_jwt, nf_refresh). Returns the hydrated User, or null if no auth cookies are present. No-op on the server.
When to use: After a server-side login (e.g., via a Netlify Function or Server Action), the nf_jwt cookie is set but no browser session exists yet. getUser() calls hydrateSession() automatically, but account operations like updateUser() or verifyEmailChange() require a live browser session. Call hydrateSession() explicitly if you need the session ready before calling those operations.
If a browser session already exists (e.g., from a browser-side login), this is a no-op and returns the existing user.
import { hydrateSession, updateUser } from '@netlify/identity'
// On page load, hydrate the session from server-set cookies
await hydrateSession()
// Now browser account operations work
await updateUser({ data: { full_name: 'Jane Doe' } })refreshSession
refreshSession(): Promise<string | null>Refreshes an expired or near-expired session. Returns the new access token on success, or null if no refresh is needed or the refresh token is invalid/missing.
Browser: Checks if the current access token is near expiry and refreshes it if needed, syncing the new token to the nf_jwt cookie. Note: the library automatically refreshes tokens in the background after any browser flow that establishes a session (login(), signup(), hydrateSession(), handleAuthCallback(), confirmEmail(), recoverPassword(), acceptInvite()), so you typically don't need to call this manually. getUser() also restarts the refresh timer when it finds an existing session. Browser-side errors return null, not an AuthError.
Server: Reads the nf_jwt and nf_refresh cookies. If the access token is expired or within 60 seconds of expiry, exchanges the refresh token for a new access token via the Identity /token endpoint and updates both cookies on the response. Call this in framework middleware or at the start of server-side request handlers to ensure the JWT is valid for downstream processing.
Throws: AuthError on network failure or if the Identity endpoint URL cannot be determined. Does not throw for invalid/expired refresh tokens (returns null instead).
// Example: Astro middleware
import { refreshSession } from '@netlify/identity'
export async function onRequest(context, next) {
await refreshSession()
return next()
}requestPasswordRecovery
requestPasswordRecovery(email: string): Promise<void>Sends a password recovery email to the given address.
Throws: MissingIdentityError if Identity is not configured. AuthError on network failure.
confirmEmail
confirmEmail(token: string): Promise<User>Confirms an email address using the token from a confirmation email. Logs the user in on success.
Throws: MissingIdentityError if Identity is not configured. AuthError if the token is invalid or expired.
acceptInvite
acceptInvite(token: string, password: string): Promise<User>Accepts an invite token and sets a password for the new account. Logs the user in on success.
Throws: MissingIdentityError if Identity is not configured. AuthError if the token is invalid or expired.
verifyEmailChange
verifyEmailChange(token: string): Promise<User>Verifies an email change using the token from a verification email.
Throws: MissingIdentityError if Identity is not configured. AuthError if the token is invalid.
recoverPassword
recoverPassword(token: string, newPassword: string): Promise<User>Redeems a recovery token and sets a new password. Logs the user in on success.
Throws: MissingIdentityError if Identity is not configured. AuthError if the token is invalid or expired.
updateUser
updateUser(updates: UserUpdates): Promise<User>Updates the current user's metadata or credentials. Requires an active session. Pass email or password to change credentials, or data to update user metadata (e.g., { data: { full_name: 'New Name' } }).
Throws: MissingIdentityError if Identity is not configured. AuthError if no user is logged in, or the update fails.
Admin Operations
The admin namespace provides user management functions for administrators. These work in two contexts:
- Server: Uses the operator token from the Netlify runtime for full admin access. No logged-in user required.
- Browser: Uses the logged-in user's JWT. The user must have an admin role.
import { admin } from '@netlify/identity'Example: managing users in a Netlify Function
import { admin } from '@netlify/identity'
import type { Context } from '@netlify/functions'
export default async (req: Request, context: Context) => {
// List all users
const users = await admin.listUsers()
// Create a new user (auto-confirmed, no email sent)
const newUser = await admin.createUser({
email: '[email protected]',
password: 'securepassword',
data: { user_metadata: { full_name: 'Jane Doe' } },
})
// Update a user's role
await admin.updateUser(newUser.id, { role: 'editor' })
return Response.json({ created: newUser.id, total: users.length })
}admin.listUsers
admin.listUsers(options?: ListUsersOptions): Promise<User[]>Lists all users. Pagination options are supported on the server; they are ignored in the browser.
Throws: AuthError if the operator token is missing (server) or no user is logged in (browser).
admin.getUser
admin.getUser(userId: string): Promise<User>Gets a single user by ID.
Throws: AuthError if the user is not found, the operator token is missing (server), or no user is logged in (browser).
admin.createUser
admin.createUser(params: CreateUserParams): Promise<User>Creates a new user. The user is auto-confirmed. Optional data is spread into the request body as additional attributes.
Throws: AuthError on failure (e.g., email already exists).
admin.updateUser
admin.updateUser(userId: string, attributes: AdminUserUpdates): Promise<User>Updates an existing user by ID. Pass any attributes to change (e.g., { email: '[email protected]' }). See AdminUserUpdates for typed fields.
Throws: AuthError if the user is not found or the update fails.
admin.deleteUser
admin.deleteUser(userId: string): Promise<void>Deletes a user by ID.
Throws: AuthError if the user is not found or the deletion fails.
Types
User
interface User {
id: string
email?: string
emailVerified?: boolean
createdAt?: string
updatedAt?: string
provider?: AuthProvider
name?: string
pictureUrl?: string
roles?: string[]
metadata?: Record<string, unknown>
appMetadata?: Record<string, unknown>
rawGoTrueData?: Record<string, unknown>
}Settings
interface Settings {
autoconfirm: boolean
disableSignup: boolean
providers: Record<AuthProvider, boolean>
}IdentityConfig
interface IdentityConfig {
url: string
token?: string
}AuthProvider
type AuthProvider = 'google' | 'github' | 'gitlab' | 'bitbucket' | 'facebook' | 'saml' | 'email'UserUpdates
interface UserUpdates {
email?: string
password?: string
data?: Record<string, unknown>
[key: string]: unknown
}Fields accepted by updateUser(). All fields are optional.
AdminUserUpdates
interface AdminUserUpdates {
email?: string
password?: string
role?: string
confirm?: boolean
app_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>
[key: string]: unknown
}Fields accepted by admin.updateUser(). Unlike UserUpdates, admin updates can set role, force-confirm a user, and write to app_metadata.
SignupData
type SignupData = Record<string, unknown>User metadata passed as the third argument to signup(). Stored in the user's user_metadata field.
AppMetadata
interface AppMetadata {
provider: AuthProvider
roles?: string[]
[key: string]: unknown
}ListUsersOptions
interface ListUsersOptions {
page?: number
perPage?: number
}Pagination options for admin.listUsers(). Only used on the server; pagination is ignored in the browser.
CreateUserParams
interface CreateUserParams {
email: string
password: string
data?: Record<string, unknown>
}Parameters for admin.createUser(). Optional data is spread into the request body as top-level attributes (use it to set app_metadata, user_metadata, role, etc.).
Admin
interface Admin {
listUsers: (options?: ListUsersOptions) => Promise<User[]>
getUser: (userId: string) => Promise<User>
createUser: (params: CreateUserParams) => Promise<User>
updateUser: (userId: string, attributes: AdminUserUpdates) => Promise<User>
deleteUser: (userId: string) => Promise<void>
}The type of the admin export. Useful for passing the admin namespace as a dependency.
AUTH_EVENTS
const AUTH_EVENTS: {
LOGIN: 'login'
LOGOUT: 'logout'
TOKEN_REFRESH: 'token_refresh'
USER_UPDATED: 'user_updated'
RECOVERY: 'recovery'
}Constants for auth event names. Use these instead of string literals for type safety and autocomplete.
| Event | When it fires |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| LOGIN | login(), signup() (with autoconfirm), recoverPassword(), confirmEmail(), acceptInvite(), handleAuthCallback() (OAuth/confirmation), hydrateSession() |
| LOGOUT | logout() |
| TOKEN_REFRESH | The library's auto-refresh timer refreshes an expiring access token and syncs the new token to the nf_jwt cookie. Fires automatically after any session-establishing flow: login(), signup(), hydrateSession(), handleAuthCallback(), confirmEmail(), recoverPassword(), acceptInvite(). getUser() also restarts the timer when it finds an existing session. |
| USER_UPDATED | updateUser(), verifyEmailChange(), handleAuthCallback() (email change) |
| RECOVERY | handleAuthCallback() (recovery token only). The user is authenticated but has not set a new password yet. Listen for this to redirect to a password reset form. recoverPassword() emits LOGIN instead because it completes both steps (token redemption + password change). |
AuthEvent
type AuthEvent = 'login' | 'logout' | 'token_refresh' | 'user_updated' | 'recovery'AuthCallback
type AuthCallback = (event: AuthEvent, user: User | null) => voidCallbackResult
interface CallbackResult {
type: 'oauth' | 'confirmation' | 'recovery' | 'invite' | 'email_change'
user: User | null
token?: string
}The token field is only present for invite callbacks, where the user hasn't set a password yet. Pass token to acceptInvite(token, password) to finish.
For all other types (oauth, confirmation, recovery, email_change), the user is logged in directly and token is not set.
Errors
AuthError
class AuthError extends Error {
status?: number
cause?: unknown
}MissingIdentityError
class MissingIdentityError extends Error {}Thrown when Identity is not configured in the current environment.
Framework integration
Recommended pattern for SSR frameworks
For SSR frameworks (Next.js, Remix, Astro, TanStack Start), the recommended pattern is:
- Browser-side for auth mutations:
login(),signup(),logout(),oauthLogin() - Server-side for reading auth state:
getUser(),getSettings(),getIdentityConfig()
Browser-side auth mutations call the Identity API directly from the browser, set the nf_jwt cookie, and emit onAuthChange events. This keeps the client UI in sync immediately. Server-side reads work because the cookie is sent with every request.
The library also supports server-side mutations (login(), signup(), logout() inside Netlify Functions), but these require the Netlify Functions runtime to set cookies. After a server-side mutation, you need a full page navigation so the browser sends the new cookie.
Next.js (App Router)
Server Actions return results; the client handles navigation:
// app/actions.ts
'use server'
import { login, logout } from '@netlify/identity'
export async function loginAction(formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
await login(email, password)
return { success: true }
}
export async function logoutAction() {
await logout()
return { success: true }
}// app/login/page.tsx
'use client'
import { loginAction } from '../actions'
export default function LoginPage() {
async function handleSubmit(formData: FormData) {
const result = await loginAction(formData)
if (result.success) {
window.location.href = '/dashboard' // full page load
}
}
return <form action={handleSubmit}>...</form>
}// app/dashboard/page.tsx
import { getUser } from '@netlify/identity'
import { redirect } from 'next/navigation'
export default async function Dashboard() {
const user = await getUser()
if (!user) redirect('/login')
return <h1>Hello, {user.email}</h1>
}Use window.location.href instead of Next.js redirect() after server-side auth mutations. Next.js redirect() triggers a soft navigation via the Router, which may not include the newly-set auth cookie. A full page load ensures the cookie is sent and the server sees the updated auth state. Reading auth state with getUser() in Server Components works normally, and redirect() is fine for auth gates (where no cookie was just set).
Remix
Login with Action (server-side pattern):
// app/routes/login.tsx
import { login } from '@netlify/identity'
import { redirect, json } from '@remix-run/node'
import type { ActionFunctionArgs } from '@remix-run/node'
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
try {
await login(email, password)
return redirect('/dashboard')
} catch (error) {
return json({ error: (error as Error).message }, { status: 400 })
}
}// app/routes/dashboard.tsx
import { getUser } from '@netlify/identity'
import { redirect } from '@remix-run/node'
export async function loader() {
const user = await getUser()
if (!user) return redirect('/login')
return { user }
}Remix redirect() works after server-side login() because Remix actions return real HTTP responses. The browser receives a 302 with the Set-Cookie header already applied, so the next request includes the auth cookie. This is different from Next.js, where redirect() in a Server Action triggers a client-side (soft) navigation that may not include newly-set cookies.
TanStack Start
Login from the browser (recommended):
// app/server/auth.ts - server functions for reads only
import { createServerFn } from '@tanstack/react-start'
import { getUser } from '@netlify/identity'
export const getServerUser = createServerFn({ method: 'GET' }).handler(async () => {
const user = await getUser()
return user ?? null
})// app/routes/login.tsx - browser-side auth for mutations
import { login, signup, onAuthChange } from '@netlify/identity'
import { getServerUser } from '~/server/auth'
export const Route = createFileRoute('/login')({
beforeLoad: async () => {
const user = await getServerUser()
if (user) throw redirect({ to: '/dashboard' })
},
component: Login,
})
function Login() {
const handleLogin = async (email: string, password: string) => {
await login(email, password) // browser-side: sets cookie + localStorage
window.location.href = '/dashboard'
}
// ...
}// app/routes/dashboard.tsx
import { logout } from '@netlify/identity'
import { getServerUser } from '~/server/auth'
export const Route = createFileRoute('/dashboard')({
beforeLoad: async () => {
const user = await getServerUser()
if (!user) throw redirect({ to: '/login' })
},
loader: async () => {
const user = await getServerUser()
return { user: user! }
},
component: Dashboard,
})
function Dashboard() {
const { user } = Route.useLoaderData()
const handleLogout = async () => {
await logout() // browser-side: clears cookie + localStorage
window.location.href = '/'
}
// ...
}Use window.location.href instead of TanStack Router's navigate() after auth changes. This ensures the browser sends the updated cookie on the next request.
Astro (SSR)
Login via API endpoint (server-side pattern):
// src/pages/api/login.ts
import type { APIRoute } from 'astro'
import { login } from '@netlify/identity'
export const POST: APIRoute = async ({ request }) => {
const { email, password } = await request.json()
try {
await login(email, password)
return new Response(null, {
status: 302,
headers: { Location: '/dashboard' },
})
} catch (error) {
return Response.json({ error: (error as Error).message }, { status: 400 })
}
}---
// src/pages/dashboard.astro
import { getUser } from '@netlify/identity'
const user = await getUser()
if (!user) return Astro.redirect('/login')
---
<h1>Hello, {user.email}</h1>SvelteKit
Login from the browser (recommended):
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import { login } from '@netlify/identity'
let email = ''
let password = ''
let error = ''
async function handleLogin() {
try {
await login(email, password)
window.location.href = '/dashboard'
} catch (e) {
error = (e as Error).message
}
}
</script>
<form on:submit|preventDefault={handleLogin}>
<input bind:value={email} type="email" />
<input bind:value={password} type="password" />
<button type="submit">Log in</button>
{#if error}<p>{error}</p>{/if}
</form>// src/routes/dashboard/+page.server.ts
import { getUser } from '@netlify/identity'
import { redirect } from '@sveltejs/kit'
export async function load() {
const user = await getUser()
if (!user) redirect(302, '/login')
return { user }
}Handling OAuth callbacks in SPAs
All SPA frameworks need a callback handler that runs on page load to process OAuth redirects, email confirmations, and password recovery tokens. Use a wrapper component that blocks page content while processing tokens. This prevents a flash of unauthenticated content that occurs when the page renders before the callback completes.
// React component (works with Next.js, Remix, TanStack Start)
import { useEffect, useState } from 'react'
import { handleAuthCallback } from '@netlify/identity'
const AUTH_HASH_PATTERN = /^#(confirmation_token|recovery_token|invite_token|email_change_token|access_token)=/
export function CallbackHandler({ children }: { children: React.ReactNode }) {
const [processing, setProcessing] = useState(
() => typeof window !== 'undefined' && AUTH_HASH_PATTERN.test(window.location.hash),
)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!window.location.hash || !AUTH_HASH_PATTERN.test(window.location.hash)) return
handleAuthCallback()
.then((result) => {
if (!result) {
setProcessing(false)
return
}
if (result.type === 'invite') {
window.location.href = `/accept-invite?token=${result.token}`
} else if (result.type === 'recovery') {
window.location.href = '/reset-password'
} else {
window.location.href = '/dashboard'
}
})
.catch((err) => {
setError(err instanceof Error ? err.message : 'Callback failed')
setProcessing(false)
})
}, [])
if (error) return <div>Auth error: {error}</div>
if (processing) return <div>Confirming your account...</div>
return <>{children}</>
}Wrap your page content with this component in your root layout so it runs on every page:
// Root layout
<CallbackHandler>
<Outlet /> {/* or {children} in Next.js */}
</CallbackHandler>If you only mount it on a /callback route, OAuth redirects and email confirmation links that land on other pages will not be processed.
Guides
React useAuth hook
The library is framework-agnostic, but here's a simple React hook for keeping components in sync with auth state:
import { useState, useEffect } from 'react'
import { getUser, onAuthChange } from '@netlify/identity'
import type { User } from '@netlify/identity'
export function useAuth() {
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
getUser().then(setUser)
return onAuthChange((_event, user) => setUser(user))
}, [])
return user
}function NavBar() {
const user = useAuth()
return user ? <p>Hello, {user.name}</p> : <a href="/login">Log in</a>
}Listening for auth changes
Use onAuthChange to keep your UI in sync with auth state. It fires on login, logout, token refresh, user updates, and recovery. It also detects session changes in other browser tabs (via localStorage).
import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
const unsubscribe = onAuthChange((event, user) => {
switch (event) {
case AUTH_EVENTS.LOGIN:
console.log('Logged in:', user?.email)
break
case AUTH_EVENTS.LOGOUT:
console.log('Logged out')
break
case AUTH_EVENTS.TOKEN_REFRESH:
console.log('Token refreshed for:', user?.email)
break
case AUTH_EVENTS.USER_UPDATED:
console.log('User updated:', user?.email)
break
case AUTH_EVENTS.RECOVERY:
console.log('Recovery login:', user?.email)
// Redirect to password reset form, then call updateUser({ password })
break
}
})
// Later, to stop listening:
unsubscribe()On the server, onAuthChange is a no-op and the returned unsubscribe function does nothing.
OAuth login
OAuth login is a two-step flow: redirect the user to the provider, then process the callback when they return.
Step by step:
import { oauthLogin, handleAuthCallback } from '@netlify/identity'
// 1. Kick off the OAuth flow (e.g., from a "Sign in with GitHub" button).
// This navigates away from the page and does not return.
oauthLogin('github')// 2. On page load, handle the redirect back from the provider.
const result = await handleAuthCallback()
if (result?.type === 'oauth') {
console.log('Logged in via OAuth:', result.user?.email)
}handleAuthCallback() exchanges the token in the URL hash, logs the user in, clears the hash, and emits an auth event via onAuthChange ('login' for OAuth/confirmation, 'recovery' for password recovery).
Password recovery
Password recovery is a two-step flow. The library handles the token exchange automatically via handleAuthCallback(), which logs the user in and returns {type: 'recovery', user}. A 'recovery' event (not 'login') is emitted via onAuthChange, so event-based listeners can also detect this flow. You then show a "set new password" form and call updateUser() to save it.
Step by step:
import { requestPasswordRecovery, handleAuthCallback, updateUser } from '@netlify/identity'
// 1. Send recovery email (e.g., from a "forgot password" form)
await requestPasswordRecovery('[email protected]')
// 2-3. On page load, handle the callback
const result = await handleAuthCallback()
if (result?.type === 'recovery') {
// 4. User is now logged in. Show your "set new password" form.
// When they submit:
const newPassword = document.getElementById('new-password').value
await updateUser({ password: newPassword })
}If you use the event-based pattern instead of checking result.type, listen for the 'recovery' event:
import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
onAuthChange((event, user) => {
if (event === AUTH_EVENTS.RECOVERY) {
// Redirect to password reset form.
// The user is authenticated, so call updateUser({ password }) to set the new password.
}
})Invite acceptance
When an admin invites a user, they receive an email with an invite link. Clicking it redirects to your site with an invite_token in the URL hash. Unlike other callback types, the user is not logged in automatically because they need to set a password first.
Step by step:
import { handleAuthCallback, acceptInvite } from '@netlify/identity'
// 1. On page load, handle the callback.
const result = await handleAuthCallback()
if (result?.type === 'invite' && result.token) {
// 2. The user is NOT logged in yet. Show a "set your password" form.
// When they submit:
const password = document.getElementById('password').value
const user = await acceptInvite(result.token, password)
console.log('Account created:', user.email)
}Session lifetime
Sessions are managed by Netlify Identity on the server side. The library stores two cookies:
nf_jwt: A short-lived JWT access token (default: 1 hour).nf_refresh: A long-lived refresh token used to obtain new access tokens without re-authenticating.
Browser auto-refresh: After any session-establishing flow (login(), signup(), hydrateSession(), handleAuthCallback(), confirmEmail(), recoverPassword(), acceptInvite()), the library automatically schedules a background refresh 60 seconds before the access token expires. getUser() also restarts the refresh timer when it finds an existing session (e.g., after a page reload). When the refresh fires, it obtains a new access token, syncs it to the nf_jwt cookie, and emits a TOKEN_REFRESH event. This keeps the cookie fresh as long as the user has the tab open. If the refresh fails (e.g., the refresh token was revoked), the timer stops and the user will need to log in again.
Server-side refresh: On the server, the access token in the nf_jwt cookie is validated as-is. If it has expired and no refresh happens, getUser() returns null. To handle this, call refreshSession() in your framework middleware or request handler. This checks if the token is near expiry, exchanges the refresh token for a new one, and updates the cookies on the response.
Session lifetime is configured in your Netlify Identity settings, not in this library.
License
MIT
