Skip to content

plutotcool/payload-plugin-two-factor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Payload Two-Factor Authentication

npm license

TOTP-based two-factor authentication (2FA) for the Payload CMS v3 admin panel.

  • πŸ” Time-based one-time passwords (TOTP) β€” works with Google Authenticator, Authy, 1Password, etc.
  • 🧩 Self-service enable / disable from the user's account page
  • πŸ›‘οΈ Secrets encrypted at rest with AES-256-GCM
  • πŸͺ Forgery-proof verification session via an HMAC-signed cookie keyed by your Payload secret
  • 🚦 Edge middleware that gates /admin navigation until 2FA is verified
  • 🌍 English & French translations out of the box
  • βš™οΈ No native dependencies β€” runs on serverless / edge runtimes (e.g. Cloudflare Workers)

Requires Payload ^3, React ^19, and Next.js ^15 || ^16.

Installation

pnpm add @plutotcool/payload-plugin-two-factor
# or: npm i / yarn add / bun add

Generate an encryption key (32 bytes / 64 hex chars) and store it as an env var:

openssl rand -hex 32
TWO_FACTOR_ENCRYPTION_KEY=your-64-char-hex-key

Usage

1. Register the plugin

// payload.config.ts
import { buildConfig } from 'payload'
import { twoFactorPlugin } from '@plutotcool/payload-plugin-two-factor'

export default buildConfig({
  // ...
  plugins: [
    twoFactorPlugin({
      // The auth collection to protect (default: 'users')
      collection: 'users',
      // Shown in the authenticator app
      issuer: 'Acme Admin',
      // 64-char hex string β€” keep it secret, keep it stable
      encryptionKey: process.env.TWO_FACTOR_ENCRYPTION_KEY!,
    }),
  ],
})

The plugin injects three hidden fields (twoFactorSecret, twoFactorEnabled, twoFactorPending) plus a UI field into the target collection, and registers the endpoints under /api/two-factor/*. Run your usual migration / schema sync afterwards.

2. Add the verification page

Create the route the middleware redirects to. Re-export the bundled page:

// src/app/(payload)/admin/verify/page.tsx
export { TwoFactorVerifyPage as default } from '@plutotcool/payload-plugin-two-factor/client'

Pass your own logo if you like:

import { TwoFactorVerifyPage } from '@plutotcool/payload-plugin-two-factor/client'
import Logo from '@/components/Logo'

export default function VerifyPage() {
  return <TwoFactorVerifyPage logo={<Logo />} />
}

3. Enforce verification with middleware

// src/middleware.ts
import { withTwoFactorMiddleware } from '@plutotcool/payload-plugin-two-factor/middleware'

export const middleware = withTwoFactorMiddleware()

export const config = {
  matcher: ['/admin/:path*'],
}

The middleware reads the Payload JWT, and for accounts with 2FA enabled but not yet verified this session, redirects /admin navigation to the verify page. It relies on process.env.PAYLOAD_SECRET being set.

Options

Option Type Default Description
issuer string required Label displayed in the authenticator app.
encryptionKey string required 64-char hex string (32 bytes) used to encrypt TOTP secrets at rest.
collection string 'users' Slug of the auth collection to protect.
totp.window number 1 Tolerance steps before/after the current 30s step, to absorb clock drift.
disabled boolean false No-op the plugin while keeping its fields (so the DB schema stays stable). Skips endpoint registration.

withTwoFactorMiddleware({ verifyPath }) accepts an optional verifyPath (default '/admin/verify') if you mount the verify page elsewhere.

Subpath exports

Import path Contents
@plutotcool/payload-plugin-two-factor twoFactorPlugin (server entry) + types
@plutotcool/payload-plugin-two-factor/client 'use client' React components for the admin panel
@plutotcool/payload-plugin-two-factor/middleware withTwoFactorMiddleware (Next.js / edge)
@plutotcool/payload-plugin-two-factor/types TypeScript types only

How it works

  • Setup β€” POST /api/two-factor/setup generates a TOTP secret + QR code and stores it encrypted as pending on the user.
  • Enable / disable β€” POST /api/two-factor/verify validates a code and flips twoFactorEnabled, promoting the pending secret to the active one.
  • Login verification β€” POST /api/two-factor/verify-login validates a code and sets an HMAC-signed payload-two-factor cookie marking the session verified.
  • Gatekeeping β€” the middleware checks that cookie against the JWT on every /admin navigation.

Secrets are encrypted with AES-256-GCM before hitting the database, and TOTP codes are compared in constant time.

Local development

A throwaway Payload app lives in dev/ (SQLite, a single users collection wired to the plugin). It imports the plugin straight from src, so changes are picked up without rebuilding.

pnpm install
cp dev/.env.example dev/.env   # then edit the secrets
pnpm dev                       # β†’ http://localhost:3000/admin

Useful scripts:

pnpm dev:generate-importmap   # regenerate the admin import map
pnpm dev:generate-types       # regenerate dev/payload-types.ts
pnpm build                    # compile to dist/ (SWC + tsc types, no bundling)
pnpm typecheck                # type-check the plugin source

Build philosophy

This package is not bundled. Following the Payload v3 convention, sources are transpiled file-by-file so 'use client' / RSC boundaries survive intact:

  • SWC transpiles TS β†’ JS (.swcrc)
  • tsc emits declarations only (--emitDeclarationOnly)
  • copyfiles copies .scss assets

dist/ mirrors src/, and the exports map keeps client, server, and middleware entry points separate so server-only code never leaks to the client.

License

MIT Β© plutot.cool

About

πŸ” - A plugin for Payload to easily setup 2FA within your admin panel

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors