payload-auth-workos
v1.0.12
Published
Payload CMS authentication plugin for WorkOS
Maintainers
Readme
Payload Auth WorkOS
A Payload CMS authentication plugin that integrates WorkOS for OAuth-based user authentication.
Features
- 🔐 OAuth Authentication - Leverage WorkOS for secure, enterprise-grade authentication
- 🎨 Highly Configurable - Customize authentication flows, redirects, and user collections
- 👥 Multi-Collection Support - Configure different authentication for multiple user collections
- 🛡️ Admin Panel Integration - Optional integration with Payload's admin panel
- 🔄 Flexible User Management - Control sign-up permissions and user data synchronization
- 🏢 Enterprise SSO - Support for WorkOS organizations and connections
- 🛠️ Manual Configuration - Helper utilities for custom collection setup
Installation
# From npmjs.com (recommended)
pnpm add payload-auth-workos
# Or from GitHub Packages (scoped name, requires authentication)
pnpm add @markkropf/payload-auth-workos --registry=https://npm.pkg.github.comThe package is published to both:
- npmjs.com:
payload-auth-workos(unscoped) - View on npm - GitHub Packages:
@markkropf/payload-auth-workos(scoped) - View on GitHub
Quick Start
1. Set up WorkOS
First, create a WorkOS account and set up an application at workos.com. You'll need:
- Client ID - Your WorkOS application client ID
- API Key (Client Secret) - Your WorkOS API key
- Cookie Password - Secure password for session encryption (minimum 32 characters)
- Provider - OAuth provider (e.g.,
GoogleOAuth,GitHubOAuth,MicrosoftOAuth)
Note: The OAuth redirect URI is automatically generated from your plugin's name configuration. You'll need to add it to your WorkOS dashboard (see Configuration section below).
2. Configure Environment Variables
Create a .env file:
WORKOS_CLIENT_ID=your_client_id
WORKOS_API_KEY=your_api_key
WORKOS_COOKIE_PASSWORD=your_secure_32_character_minimum_password
WORKOS_PROVIDER=GoogleOAuthYou can generate a secure cookie password using:
openssl rand -base64 323. Add the Plugin to Your Payload Config
import { buildConfig } from 'payload'
import { authPlugin } from 'payload-auth-workos'
export default buildConfig({
// ... other config
plugins: [
authPlugin({
name: 'workos-auth',
usersCollectionSlug: 'users',
accountsCollectionSlug: 'accounts',
workosProvider: {
client_id: process.env.WORKOS_CLIENT_ID!,
client_secret: process.env.WORKOS_API_KEY!,
cookie_password: process.env.WORKOS_COOKIE_PASSWORD!,
provider: process.env.WORKOS_PROVIDER!, // or connection/organization
},
}),
],
})4. Add Sign-In Links to Your App
// In your Next.js app
export default function LoginPage() {
return (
<div>
<h1>Sign In</h1>
<a href="/api/{name}/auth/signin">Sign in with WorkOS</a>
</div>
)
}Note: Replace {name} with the name property you defined in your plugin configuration (e.g., workos-auth, app, admin).
5. Client-Side Session Management
To access the current user in your Next.js Client Components, use the provided AuthProvider and useAuth hook.
Layout (Server Component):
// app/layout.tsx
import { getPayload } from 'payload'
import config from '@payload-config'
import { headers } from 'next/headers'
import { AuthProvider } from 'payload-auth-workos/client'
export default async function RootLayout({ children }) {
const payload = await getPayload({ config })
// Verify auth using Payload's native API
const { user } = await payload.auth({ headers: await headers() })
return (
<html>
<body>
{/* Pass the server-verified user to the client provider */}
<AuthProvider user={user}>
{children}
</AuthProvider>
</body>
</html>
)
}Component (Client Component):
// components/UserProfile.tsx
'use client'
import { useAuth } from 'payload-auth-workos/client'
export function UserProfile() {
const { user } = useAuth()
// Assuming your plugin name is 'workos-auth'
if (!user) return <a href="/api/workos-auth/auth/signin">Sign in</a>
return <div>Hello, {user.email}</div>
}Configuration Options
AuthPluginConfig
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| name | string | ✅ | - | Unique identifier for this auth configuration |
| usersCollectionSlug | string | ✅ | - | Slug of the users collection |
| accountsCollectionSlug | string | ✅ | - | Slug of the accounts collection |
| workosProvider | WorkOSProviderConfig | ✅ | - | WorkOS configuration |
| useAdmin | boolean | ❌ | false | Use this config for admin panel auth |
| allowSignUp | boolean | ❌ | false | Allow new user registrations (secure by default) |
| successRedirectPath | string | ❌ | '/' | Redirect path after successful auth |
| errorRedirectPath | string | ❌ | '/auth/error' | Redirect path on auth error |
| endWorkOsSessionOnSignout | boolean | ❌ | false | End the WorkOS session on sign out (forces full re-auth) |
| replaceAdminLogoutButton | boolean | ❌ | false | Replace the Payload admin logout button with the plugin AdminLogoutButton |
| postSignoutRedirectPath | \/${string}` | (req) => `/${string}` | Promise<`/${string}`>| ❌ |'/admin/login'ifuseAdminelse'/'| Redirect path after sign out |
|onSuccess|function| ❌ | - | Custom callback after successful auth |
|onError|function` | ❌ | - | Custom error handler |
WorkOSProviderConfig
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| client_id | string | ✅ | WorkOS Client ID |
| client_secret | string | ✅ | WorkOS API Key |
| cookie_password | string | ✅ | Cookie encryption password (min 32 chars) |
| provider | string | ❌* | OAuth provider (e.g., GoogleOAuth, GitHubOAuth) |
| connection | string | ❌* | WorkOS connection ID |
| organization | string | ❌* | WorkOS organization ID |
Notes:
- You must provide either
provider,connection, ororganization. WorkOS requires one of these connection selectors. - The OAuth
redirect_uriis automatically generated as/api/{name}/auth/callbackand does not need to be configured.
Advanced Usage
Multiple User Collections
Configure different authentication for app users and admin users:
import { buildConfig } from 'payload'
import { authPlugin, createWorkOSProviderConfig } from 'payload-auth-workos'
// Create a shared WorkOS config to avoid repetition
const workosConfig = createWorkOSProviderConfig('GoogleOAuth', {
client_id: process.env.WORKOS_CLIENT_ID!,
client_secret: process.env.WORKOS_API_KEY!,
cookie_password: process.env.WORKOS_COOKIE_PASSWORD!,
})
export default buildConfig({
admin: {
user: 'adminUsers',
},
plugins: [
// Admin users
authPlugin({
name: 'admin',
useAdmin: true,
allowSignUp: false,
usersCollectionSlug: 'adminUsers',
accountsCollectionSlug: 'adminAccounts',
successRedirectPath: '/admin',
workosProvider: workosConfig,
}),
// App users
authPlugin({
name: 'app',
allowSignUp: true,
usersCollectionSlug: 'appUsers',
accountsCollectionSlug: 'appAccounts',
successRedirectPath: '/dashboard',
workosProvider: workosConfig,
}),
],
})Client-Side Authentication for Multiple Collections
When using multiple authentication scopes (e.g., admin and app), you can create isolated client-side auth providers to prevent state conflicts:
// lib/auth.ts
import { createAuthClient } from 'payload-auth-workos/client'
export const adminAuth = createAuthClient('admin')
export const appAuth = createAuthClient('app')Usage in Layouts:
// app/(app)/layout.tsx
import { appAuth } from '@/lib/auth'
// ... inside your layout
<appAuth.AuthProvider user={appUser}>
{children}
</appAuth.AuthProvider>Usage in Components:
// app/(app)/components/Header.tsx
'use client'
import { appAuth } from '@/lib/auth'
export function Header() {
const { user } = appAuth.useAuth()
// ...
}Cookie Management in Multi-Collection Setups
When using multiple auth collections, the plugin automatically manages cookies to prevent conflicts and ensure compatibility with Payload's admin panel.
How It Works
The plugin uses different cookie naming strategies based on whether a collection is used for admin authentication:
Admin collection (when
config.admin.usermatches your collection):payload-token- Standard Payload cookie for admin panel compatibility
Non-admin collections:
payload-token-{collectionSlug}- Collection-specific cookie
Example Cookie Names
With the configuration above, you'll get these cookies:
- Admin users:
payload-token - App users:
payload-token-appUsers
This allows users to be authenticated to multiple collections simultaneously without conflicts, while keeping the admin authentication simple and compatible with Payload's built-in admin panel.
Admin Panel Integration
The plugin automatically detects which collection is used for admin authentication (via config.admin.user) and uses the standard payload-token cookie. This ensures the Payload admin panel works seamlessly without any additional configuration.
No special configuration needed - just set config.admin.user to match your admin collection slug:
export default buildConfig({
admin: {
user: 'adminUsers', // Must match the admin collection slug
},
// ...
})Custom Prefix
If you've configured a custom cookiePrefix in your Payload config, the plugin respects it:
export default buildConfig({
cookiePrefix: 'myapp',
admin: {
user: 'adminUsers',
},
// ...
})This would create:
myapp-tokenfor admin usersmyapp-token-appUsersfor app users
Adding a Login Button to the Admin Panel
When using useAdmin: true, you can add a login button to the admin login page (/admin/login). Create a component file that imports the LoginButton from the /client subpath:
// src/components/WorkOSLoginButton.tsx
'use client'
import React from 'react'
import { LoginButton } from 'payload-auth-workos/client'
const WorkOSLoginButton = () => {
return (
<LoginButton
href="/api/{name}/auth/signin"
label="Sign in with WorkOS"
/>
)
}
export default WorkOSLoginButtonThen reference it in your Payload config:
// payload.config.ts
import { buildConfig } from 'payload'
import { authPlugin } from 'payload-auth-workos'
export default buildConfig({
admin: {
user: 'adminUsers',
components: {
afterLogin: ['@/components/WorkOSLoginButton'],
},
},
plugins: [
authPlugin({
name: 'admin',
useAdmin: true,
usersCollectionSlug: 'adminUsers',
accountsCollectionSlug: 'adminAccounts',
workosProvider: workosConfig,
}),
],
})LoginButton Props:
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| href | string | ✅ | - | The signin endpoint URL (e.g., /api/admin/auth/signin) |
| label | string | ❌ | 'Login' | Button text |
| className | string | ❌ | - | Additional CSS classes |
| style | React.CSSProperties | ❌ | - | Custom inline styles |
Note: The LoginButton uses Payload's native button classes by default to match the admin panel design.
Creating a Custom Login Button
If you need full control, you can create your own component using Payload's default button classes:
const CustomLoginButton = () => (
<div className="template-default__actions">
<a
href="/api/admin/auth/signin"
className="btn btn--style-primary btn--icon-style-without-border btn--size-medium btn--icon-position-right"
>
Sign in with Google Workspace
</a>
</div>
)
export default buildConfig({
admin: {
user: 'adminUsers',
components: {
afterLogin: [CustomLoginButton],
},
},
// ...
})Adding a Logout Button to the Admin Panel
To ensure the WorkOS session is ended (when configured) and cookies are properly cleared, sign out by hitting the plugin's signout endpoint: /api/{name}/auth/signout.
If you set replaceAdminLogoutButton: true, the plugin will automatically replace the admin logout button to point at /api/{name}/auth/signout:
authPlugin({
name: 'admin',
useAdmin: true,
replaceAdminLogoutButton: true,
usersCollectionSlug: 'adminUsers',
accountsCollectionSlug: 'adminAccounts',
workosProvider: workosConfig,
})Note: The plugin throws if admin.components.logout.Button is already configured. Remove the existing logout button or disable replaceAdminLogoutButton to avoid conflicts.
You can also replace the default Payload logout button manually using admin.components.logout.Button:
// payload.config.ts
import { buildConfig } from 'payload'
export default buildConfig({
admin: {
components: {
logout: {
Button: {
path: 'payload-auth-workos/client#AdminLogoutButton',
clientProps: {
href: '/api/{name}/auth/signout',
},
},
},
},
},
})AdminLogoutButton Props:
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| href | string | ✅ | - | The signout endpoint URL (e.g., /api/admin/auth/signout) |
| tabIndex | number | ❌ | 0 | Tab order for keyboard navigation |
Note: The AdminLogoutButton is intended for the Payload admin UI only, since it relies on @payloadcms/ui components and translations.
Creating a Custom Logout Button
If you need full control, you can create your own component and point it at the signout endpoint:
const CustomLogoutButton = () => (
<a href="/api/admin/auth/signout">Sign out</a>
)Manual Collection Configuration
If you need more control over your collections, you can configure them manually using the provided utilities:
import { buildConfig } from 'payload'
import { authPlugin } from 'payload-auth-workos'
import { withAccountCollection } from 'payload-auth-workos/collection'
import { deleteLinkedAccounts } from 'payload-auth-workos/collection/hooks'
// Define your users collection
const Users = {
slug: 'users',
auth: true,
fields: [
{
name: 'name',
type: 'text',
},
// Add custom fields here
],
hooks: {
beforeDelete: [deleteLinkedAccounts('accounts')], // Clean up accounts when user is deleted
},
}
// Create accounts collection with sensible defaults
const Accounts = withAccountCollection(
{
slug: 'accounts',
// Optional: override admin config, access control, etc.
},
Users.slug, // users collection slug
)
export default buildConfig({
collections: [Users, Accounts],
plugins: [
authPlugin({
name: 'workos-auth',
usersCollectionSlug: Users.slug,
accountsCollectionSlug: Accounts.slug,
workosProvider: {
client_id: process.env.WORKOS_CLIENT_ID!,
client_secret: process.env.WORKOS_API_KEY!,
cookie_password: process.env.WORKOS_COOKIE_PASSWORD!,
provider: 'GoogleOAuth',
},
}),
],
})Custom Success Handler
authPlugin({
name: 'app',
usersCollectionSlug: 'users',
accountsCollectionSlug: 'accounts',
workosProvider: { /* ... */ },
onSuccess: async ({ user, session, req }) => {
console.log('User authenticated:', user.email)
// Send welcome email, log analytics, etc.
},
})Custom Error Handler
authPlugin({
name: 'app',
usersCollectionSlug: 'users',
accountsCollectionSlug: 'accounts',
workosProvider: { /* ... */ },
onError: async ({ error, req }) => {
console.error('Auth error:', error)
// Log to error tracking service
},
})Enterprise SSO with Organizations
For enterprise customers using WorkOS organizations:
authPlugin({
name: 'enterprise',
usersCollectionSlug: 'users',
accountsCollectionSlug: 'accounts',
workosProvider: {
client_id: process.env.WORKOS_CLIENT_ID!,
client_secret: process.env.WORKOS_API_KEY!,
cookie_password: process.env.WORKOS_COOKIE_PASSWORD!,
organization: 'org_123456', // WorkOS organization ID
},
})Using Specific Connections
For specific WorkOS connections (e.g., a custom SAML connection):
authPlugin({
name: 'saml',
usersCollectionSlug: 'users',
accountsCollectionSlug: 'accounts',
workosProvider: {
client_id: process.env.WORKOS_CLIENT_ID!,
client_secret: process.env.WORKOS_API_KEY!,
cookie_password: process.env.WORKOS_COOKIE_PASSWORD!,
connection: 'conn_123456', // WorkOS connection ID
},
})Sign-Out Behavior and Redirects
By default, sign out only clears the local Payload session cookies. If you want to fully end the WorkOS session (so the user must re-authenticate with their IdP), set endWorkOsSessionOnSignout: true. postSignoutRedirectPath expects a path, but full URLs are accepted and normalized to a path. You can also control the post-signout redirect, including dynamic URLs:
authPlugin({
name: 'app',
usersCollectionSlug: 'users',
accountsCollectionSlug: 'accounts',
workosProvider: { /* ... */ },
endWorkOsSessionOnSignout: true,
postSignoutRedirectPath: ({ headers }) => {
const host = headers.get('host') || 'example.com'
return `https://${host}/goodbye`
},
})Authentication Endpoints
The plugin creates the following endpoints for each configuration:
GET /api/{name}/auth/signin- Initiates OAuth flowGET /api/{name}/auth/callback- Handles OAuth callbackGET /api/{name}/auth/signout- Signs out the userGET /api/{name}/auth/session- Returns current session status
(Assuming default /api route prefix. If you use a custom routes.api, adjust accordingly).
Note: For a full sign-out (including ending the WorkOS session when configured), direct users to the plugin's signout endpoint (/api/{name}/auth/signout) rather than the default Payload logout route.
Collections Schema
Users Collection
The plugin adds/requires these fields in your users collection:
{
email: string // User's email (unique)
firstName?: string // First name from WorkOS
lastName?: string // Last name from WorkOS
profilePictureUrl?: string // Profile picture URL
workosUserId: string // WorkOS user ID (unique)
}If you're using Payload's auth option on your collection, the plugin will extend it with these additional fields.
Accounts Collection
Stores OAuth account linkages:
{
user: relationship // Reference to user
provider: 'workos' // OAuth provider
providerAccountId: string // WorkOS account ID
organizationId?: string // WorkOS organization ID
accessToken?: string // OAuth access token (hidden)
refreshToken?: string // OAuth refresh token (hidden)
expiresAt?: Date // Token expiration
metadata?: object // Additional data
}Collection Utilities
withAccountCollection
A helper function that creates a complete accounts collection with sensible defaults:
import { withAccountCollection } from 'payload-auth-workos/collection'
const Accounts = withAccountCollection(
{
slug: 'accounts',
// Optional overrides:
access: {
// Custom access control
},
admin: {
// Custom admin config
},
fields: [
// Additional custom fields
],
},
'users', // users collection slug
)Features:
- Provides all required OAuth account fields
- Sets secure default access control
- Configures admin UI with sensible defaults
- Allows custom fields and overrides
- Enables automatic timestamps
deleteLinkedAccounts
A hook that automatically cleans up orphaned account records when a user is deleted:
import { deleteLinkedAccounts } from 'payload-auth-workos/collection/hooks'
const Users = {
slug: 'users',
hooks: {
beforeDelete: [deleteLinkedAccounts('accounts')],
},
// ... rest of config
}API Reference
Main Package Exports
import {
authPlugin,
createUsersCollection,
createAccountsCollection,
createWorkOSProviderConfig,
generateUserToken,
getPayloadCookies,
getExpiredPayloadCookies,
getAuthorizationUrl,
exchangeCodeForToken,
getUserInfo,
refreshAccessToken,
withAccountCollection,
deleteLinkedAccounts,
} from 'payload-auth-workos'authPlugin(config)- Main plugin functioncreateUsersCollection(slug)- Creates a users collectioncreateAccountsCollection(slug, usersSlug)- Creates an accounts collectioncreateWorkOSProviderConfig(provider, config)- Creates reusable WorkOS provider configgenerateUserToken(payload, collection, userId)- Generates a JWT tokengetPayloadCookies(payload, collection, token)- Generates auth cookie strings (uses standardpayload-tokenfor admin collections, collection-specific cookies for others)getExpiredPayloadCookies(payload, collection)- Generates expired cookie strings for sign-outgetAuthorizationUrl(config)- Generates WorkOS authorization URLexchangeCodeForToken(config, code)- Exchanges auth code for tokengetUserInfo(config, accessToken)- Gets user info from WorkOSrefreshAccessToken(config, refreshToken)- Refreshes access tokenwithAccountCollection(config, usersSlug)- Creates accounts collection with defaultsdeleteLinkedAccounts(accountsSlug)- Hook to delete linked accounts
Client Package Exports
For client-side components (use in files with 'use client' directive):
import { LoginButton, AdminLogoutButton, AuthProvider, useAuth, createAuthClient } from 'payload-auth-workos/client'
import type { LoginButtonProps, AdminLogoutButtonProps, AuthContextType, AuthProviderProps } from 'payload-auth-workos/client'LoginButton- Customizable login button component for admin panelAdminLogoutButton- Payload-style logout button component for admin panelAuthProvider- Context provider for user sessionsuseAuth- Hook to access the current user sessioncreateAuthClient(slug)- Factory to create isolated auth clients for multi-collection setupsLoginButtonProps- TypeScript type for LoginButton propsAdminLogoutButtonProps- TypeScript type for AdminLogoutButton propsAuthContextType- TypeScript type for auth contextAuthProviderProps- TypeScript type for auth provider props
Development
Setup
# Install dependencies
pnpm install
# Build the plugin
pnpm build
# Run in development mode
pnpm dev
# Run tests
pnpm test
# Lint
pnpm lintTesting Unreleased Builds
For local development, you can run the plugin inside the /dev app to test changes quickly.
If you want to validate the built package output, install a feature branch directly in another project. The prepare script builds dist during install so the distributed artifacts are available:
# Install from a feature branch
pnpm add github:MarkKropf/payload-auth-workos#your-branchProject Structure
payload-auth-workos/
├── src/
│ ├── collection/ # Collection utilities
│ │ ├── index.ts # withAccountCollection
│ │ └── hooks.ts # deleteLinkedAccounts
│ ├── collections/ # Collection creators
│ ├── endpoints/ # API endpoints
│ ├── lib/ # Core functionality
│ ├── plugin.ts # Main plugin
│ └── types.ts # TypeScript types
├── dev/ # Development environment
└── examples/ # Example configurationsTroubleshooting
Invalid Client Secret Error
Make sure you're using your WorkOS API Key (not Client ID) as the client_secret.
Invalid Connection Selector Error
WorkOS requires either provider, connection, or organization in your configuration. Make sure you've specified one of these.
Redirect URI Configuration
The plugin automatically generates OAuth redirect URIs based on your configuration. You must add these URIs to your WorkOS dashboard's allowed redirect URIs list.
Format:
- Standard endpoints:
{baseUrl}/api/{name}/auth/callback - Admin endpoints (
useAdmin: true):{baseUrl}/api/{name}/auth/callback
Where {name} is the value you specified in your plugin's name configuration.
Examples:
- Plugin with
name: 'workos-auth'→http://127.0.0.1:3000/api/workos-auth/auth/callback - Plugin with
name: 'admin'(even withuseAdmin: true) →http://127.0.0.1:3000/api/admin/auth/callback - Plugin with
name: 'app'→http://127.0.0.1:3000/api/app/auth/callback
Note: All endpoints use the /api prefix by default (or your configured routes.api).
Important: WorkOS requires 127.0.0.1 instead of localhost. Make sure the redirect URIs in your WorkOS dashboard match exactly, including the protocol (http/https) and port.
Collection Not Found Error
If you see errors about collections not existing, make sure:
- Your collection slugs match the ones specified in the plugin config
- Collections are defined before the plugin is loaded
- You've run type generation:
pnpm payload generate:types
Examples
Check the /examples directory for complete working examples:
- Basic configuration
- Multi-collection setup
- Custom collections with manual configuration
License
MIT
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
Development Workflow
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests:
pnpm test - Run linter:
pnpm lint - Submit a pull request
Support
For issues and questions:
Acknowledgments
- Built for Payload CMS
- Powered by WorkOS
