Skip to main content

authentication

This skill should be used when the user asks to "add login to Payload", "set up OAuth/SSO", "write a custom auth strategy", "use API keys", "configure auth cookies", "customize verification emails", "refresh a JWT", or "handle forgot-password" — anything about Payload v3 authentication strategies, operations, or auth emails beyond the basics covered in collections.

Source facts

Repository
Agents-Store/codex-plugins
Last source activity
November 1, 2026 at 02:46
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
authentication
description
This skill should be used when the user asks to "add login to Payload", "set up OAuth/SSO", "write a custom auth strategy", "use API keys", "configure auth cookies", "customize verification emails", "refresh a JWT", or "handle forgot-password" — anything about Payload v3 authentication strategies, operations, or auth emails beyond the basics covered in collections.
# PayloadCMS — Authentication Auth in Payload is a property of a collection, not a separate subsystem. Set `auth: true` (or an `AuthConfig` object) on any collection and it gains a login source, auto-generated `email`/`password`/`resetPasswordToken`/`loginAttempts`/`lockUntil` fields, a full set of auth REST endpoints, Local API methods, and `req.user` population on every request. The `collections` skill covers turning it on; this skill goes deeper — the four strategies, every operation, auth emails, token data, and locking down cookies. ## The Four Strategies Payload ships four built-in ways to identify a user. They are not mutually exclusive — a collection can accept cookies, bearer JWTs, and API keys at once. | Strategy | How the client authenticates | Enable with | Use for | | --- | --- | --- | --- | | HTTP-only Cookie | Browser sends the `payload-token` cookie automatically after login | On by default with `auth: true` | Admin panel, same-site frontends | | JWT (bearer) | `Authorization: JWT <token>` header | On by default; token returned by login/refresh/me | Mobile apps, server-to-server, cross-origin | | API Key | `Authorization: <slug> API-Key <key>` header | `useAPIKey: true` (add `{ reveal: true }` to let admins re-read keys, 3.90+) | Service accounts, third-party integrations | | Custom | Your `authenticate` function resolves a user | `strategies: [...]` | OAuth/SSO, SAML, header-forwarded identity | Cookies are immune to XSS (JavaScript cannot read them); JWTs are convenient for non-browser clients. Both carry the same signed payload. ## AuthConfig Reference ```ts // src/collections/Users.ts import type { CollectionConfig } from 'payload' export const Users: CollectionConfig = { slug: 'users', admin: { useAsTitle: 'email' }, auth: { tokenExpiration: 7200, // Seconds the session/JWT stays valid (2h) verify: true, // Require email verification before login maxLoginAttempts: 5, // Failed logins before lockout (0 disables) lockTime: 600 * 1000, // Lockout duration in ms (10 min) useAPIKey: false, // Per-user API keys; since 3.90 shown once at generation — `{ reveal: true }` lets admins re-read them useSessions: true, // Default true; false => stateless JWT depth: 0, // Relationship depth when building the JWT removeTokenFromResponses: false, // Strip token from auth-op responses cookies: { secure: true, sameSite: 'Lax' }, // disableLocalStrategy: true, // Advanced: turn off email/password entirely // loginWithUsername: { ... }, // Username login (see below) // strategies: [ ... ], // Custom strategies (see below) // forgotPassword: { ... }, // Reset-email customization (see below) // verify: { ... }, // Verify-email customization (see below) }, fields: [ { name: 'roles', type: 'select', hasMany: true, saveToJWT: true, // Read roles from req.user without a DB hit options: ['admin', 'editor', 'user'], defaultValue: ['user'], required: true, }, ], } ``` Run `pnpm generate:types` after editing the collection so the generated `User` type stays in sync. ## Auth Operations Every auth collection exposes these. REST paths are relative to `/api/<slug>`; Local API methods take `{ collection: '<slug>', ... }`. | Operation | REST | Local API | | --- | --- | --- | | Login | `POST /<slug>/login` | `payload.login({ collection, data: { email, password } })` | | Logout | `POST /<slug>/logout?allSessions=false` | `logout` server function (from `@payloadcms/next/auth`) | | Me (current user) | `GET /<slug>/me` | read `req.user` | | Refresh token | `POST /<slug>/refresh-token` | `refresh` server function | | Forgot password | `POST /<slug>/forgot-password` | `payload.forgotPassword({ collection, data: { email }, disableEmail })` | | Reset password | `POST /<slug>/reset-password` | `payload.resetPassword({ collection, data: { token, password }, overrideAccess })` | | Verify email | `POST /<slug>/verify/<token>` | `payload.verifyEmail({ collection, token })` | | Unlock | `POST /<slug>/unlock` | `payload.unlock({ collection, data: { email } })` | Login (Local API): ```ts // src/app/(auth)/actions.ts const { user, token, exp } = await payload.login({ collection: 'users', data: { email, password }, }) ``` Login (REST) returns the user and sets the `payload-token` cookie; the JSON body also carries `token` for non-browser clients: ```ts const res = await fetch('/api/users/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) const { user, token } = await res.json() ``` Kick off a reset and let Payload email the link, or suppress the email to handle delivery yourself: ```ts const token = await payload.forgotPassword({ collection: 'users', data: { email: 'jane@example.com' }, disableEmail: false, // true => you get the token back, no email sent }) // Later, with the token from the email link: await payload.resetPassword({ collection: 'users', data: { token, password: 'new-secret' }, overrideAccess: true, }) ``` ## API Keys Since 3.90 From 3.90.0 an API key is shown **once**, when it is generated. Payload then omits `apiKey` from every read (admin UI, REST, GraphQL, Local API) and exposes only the boolean `hasAPIKey`. Copy the key at generation time and keep it in a secret manager; to replace a lost key, regenerate it. Only users with admin access can generate, regenerate or revoke keys, and an API key is independent of the user's password — changing the password does not disable it. To keep the pre-3.90 behaviour — admins can reveal stored keys from the Admin Panel — switch the boolean to an object: ```ts // src/collections/ServiceAccounts.ts auth: { useAPIKey: { reveal: true, // also registers POST /api/<slug>/:id/api-key/reveal (admin-only) }, } ``` `reveal: true` also requires the revealing admin to have `read` and `update` access to the document and `update` access to the generated `apiKey` field. Leave it off unless a person genuinely needs to re-read a key — a key that cannot be read cannot leak through an over-broad `read` rule. Restrict who may generate keys by overriding the generated `apiKey` field's `create`/`update` access. `useAPIKey: true` still compiles on 3.90; `{ reveal?: boolean }` is the object form. ## Upgrade to 3.90 (security release) 3.90.0 (2026-09-18) is a security release — upgrade promptly, then regenerate types and, on a relational database, create and run a migration: ```bash pnpm payload generate:types pnpm payload migrate:create upgrade-3-90 # SQL adapters only — adds resetPasswordRequestedAt and _objectKey columns pnpm payload migrate ``` What changes for authentication: - **Password change revokes other sessions** — no action needed; other devices must sign in again. - **Password reset clears lockouts; forgot-password is throttled** — every auth collection gains a `resetPasswordRequestedAt` field, which is why the migration is required on Postgres/SQLite/D1. MongoDB needs no migration. - **API keys are no longer readable** after generation — see above; opt back in with `useAPIKey: { reveal: true }`. - **`schedulePublish` jobs now record the scheduling user's auth collection** — re-create any publish/unpublish events that were pending at upgrade time, and pass `user: { relationTo, value }` (value is the user ID) from custom queue code. - **Form Builder, uploads, jobs** also changed defaults — see the `official-plugins`, `adapters` and `jobs-queue` skills. ## Auth Emails: Verify & Forgot-Password Customize the verification and reset emails inside `auth`. Both objects accept `generateEmailHTML` and `generateEmailSubject`. `generateEmailHTML` receives `{ req, token, user }`; `generateEmailSubject` receives `{ req, user }`. Point the link at your own frontend route — not Payload's API. ```ts // src/collections/Users.ts auth: { verify: { generateEmailHTML: ({ token, user }) => `<p>Hi ${user.email}, confirm your account:</p> <a href="${process.env.NEXT_PUBLIC_SITE_URL}/verify?token=${token}">Verify email</a>`, generateEmailSubject: ({ user }) => `Verify your account, ${user.email}`, }, forgotPassword: { expiration: 3600 * 1000, // Reset-token validity in ms (1h) generateEmailHTML: ({ token, user }) => `<p>Reset your password:</p> <a href="${process.env.NEXT_PUBLIC_SITE_URL}/reset-password?token=${token}">Choose a new password</a>`, generateEmailSubject: ({ user }) => `Hey ${user.email}, reset your password`, }, } ``` These emails only send if an email transport is configured at the root `email:` key. SMTP, Resend, Nodemailer, etc. live in the `adapters` skill — without one, Payload logs the email to the console in dev and silently no-ops in prod. ## Token Data & `saveToJWT` The JWT/cookie carries a small signed payload, read back as `req.user` on every request with no extra DB query. By default it holds `id`, `email`, and `collection`. Opt fields in per-field: ```ts { name: 'roles', type: 'select', hasMany: true, saveToJWT: true } { name: 'tenant', type: 'relationship', relationTo: 'tenants', saveToJWT: true } { name: 'internalNote', type: 'text', saveToJWT: false } // explicit exclude { name: 'plan', type: 'text', saveToJWT: 'subscriptionPlan' } // alternate JWT key ``` `group` and `tabs` fields store the whole object when `saveToJWT: true`, and child fields can override individually. Anything you need inside access functions or hooks (`req.user.roles`, `req.user.tenant`) must be marked `saveToJWT: true` — otherwise it's absent from `req.user` until you re-fetch the document. Read it anywhere a `req` is in scope: ```ts // In an access function, hook, or custom endpoint if (req.user?.roles?.includes('admin')) return true ``` Tune `tokenExpiration` for session length, `maxLoginAttempts` + `lockTime` for brute-force protection (Payload increments `loginAttempts` and stamps `lockUntil` automatically), and `removeTokenFromResponses: true` when you only want cookie-based auth and never want the raw token in JSON. ## Cookies, CSRF & Cross-Domain ```ts auth: { cookies: { secure: true, // HTTPS only — keep false in local dev over http sameSite: 'Lax', // 'Strict' | 'Lax' | 'None' domain: '.example.com', // Share the cookie across subdomains }, } ``` Cookies are read from the responses of `login`, `logout`, `refresh`, and `me`. For a cross-origin frontend you must set `sameSite: 'None'` **and** `secure: true`, then whitelist origins for CSRF in the root config (`csrf`) and CORS (`cors`) — wildcards are not allowed once `sameSite: 'None'` is in play: ```ts // src/payload.config.ts export default buildConfig({ serverURL: process.env.PAYLOAD_URL, cors: ['https://app.example.com'], csrf: ['https://app.example.com'], // serverURL is added automatically collections: [Users], }) ``` ## `loginWithUsername` & `disableLocalStrategy` Swap email-as-identifier for a username (with optional email fallback): ```ts auth: { loginWithUsername: { allowEmailLogin: true, // Accept username OR email at the login form requireEmail: false, // Make email optional at signup }, } ``` Turn off email/password entirely when a collection authenticates only via API key or a custom strategy: ```ts auth: { useAPIKey: true, // Since 3.90 the key is shown once; use `{ reveal: true }` to let admins re-read it disableLocalStrategy: true, // No password fields, no login op } ``` ## Custom Strategies (OAuth / SSO) A custom strategy is the integration point for OAuth, SAML, or a reverse-proxy that forwards a trusted identity header. Add objects to `auth.strategies`. Each has a `name` and an `authenticate` function receiving `{ payload, headers, canSetHeaders, isGraphQL }`. It must resolve to `{ user }` where `user` is `{ collection: '<slug>', ...userDoc }` (or `null` to fall through to the next strategy), optionally with `responseHeaders`. ```ts // src/auth/headerStrategy.ts import type { AuthStrategy } from 'payload' export const headerStrategy: AuthStrategy = { name: 'forwarded-identity', authenticate: async ({ payload, headers }) => { const email = headers.get('x-forwarded-user') // set by a trusted proxy if (!email) return { user: null } const { docs } = await payload.find({ collection: 'users', where: { email: { equals: email } }, limit: 1, }) const found = docs[0] return { user: found ? { collection: 'users', ...found } : null, } }, } ``` Wire it in, usually alongside `disableLocalStrategy`: ```ts // src/collections/Users.ts auth: { disableLocalStrategy: true, strategies: [headerStrategy], } ``` ## Protecting Next.js Routes In the App Router, resolve the user from headers via the Local API and gate server components or route handlers: ```ts // src/app/(app)/dashboard/page.tsx import { headers as nextHeaders } from 'next/headers' import { getPayload } from 'payload' import config from '@payload-config' import { redirect } from 'next/navigation' export default async function Dashboard() { const payload = await getPayload({ config }) const { user } = await payload.auth({ headers: await nextHeaders() }) if (!user) redirect('/login') return <h1>Welcome {user.email}</h1> } ``` See the `nextjs-integration` skill for the full server-component, middleware, and route-handler patterns.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub