Payload CMS authentication plugin for WorkOS
npm install payload-auth-workosA Payload CMS authentication plugin that integrates WorkOS for OAuth-based user authentication.
- š 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
``bashFrom npmjs.com (recommended)
pnpm add payload-auth-workos
The package is published to both:
- npmjs.com:
payload-auth-workos (unscoped) - View on npm
- GitHub Packages: @markkropf/payload-auth-workos (scoped) - View on GitHubQuick Start
$3
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).$3
Create a
.env file:`env
WORKOS_CLIENT_ID=your_client_id
WORKOS_API_KEY=your_api_key
WORKOS_COOKIE_PASSWORD=your_secure_32_character_minimum_password
WORKOS_PROVIDER=GoogleOAuth
`You can generate a secure cookie password using:
`bash
openssl rand -base64 32
`$3
`typescript
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
},
}),
],
})
`$3
`tsx
// In your Next.js app
export default function LoginPage() {
return (
)
}
`
Note: Replace {name} with the name property you defined in your plugin configuration (e.g., workos-auth, app, admin).$3
To access the current user in your Next.js Client Components, use the provided
AuthProvider and useAuth hook.Layout (Server Component):
`tsx
// 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 (
{/ Pass the server-verified user to the client provider /}
{children}
)
}
`Component (Client Component):
`tsx
// 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 Sign in
return
Hello, {user.email}
}
`Configuration Options
$3
| 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' if useAdmin else '/' | Redirect path after sign out |
| onSuccess | function | ā | - | Custom callback after successful auth |
| onError | function | ā | - | Custom error handler |$3
| 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, or organization. WorkOS requires one of these connection selectors.
- The OAuth redirect_uri is automatically generated as /api/{name}/auth/callback and does not need to be configured.Advanced Usage
$3
Configure different authentication for app users and admin users:
`typescript
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,
}),
],
})
`$3
When using multiple authentication scopes (e.g., admin and app), you can create isolated client-side auth providers to prevent state conflicts:
`tsx
// lib/auth.ts
import { createAuthClient } from 'payload-auth-workos/client'export const adminAuth = createAuthClient('admin')
export const appAuth = createAuthClient('app')
`Usage in Layouts:
`tsx
// app/(app)/layout.tsx
import { appAuth } from '@/lib/auth'// ... inside your layout
{children}
`Usage in Components:
`tsx
// app/(app)/components/Header.tsx
'use client'
import { appAuth } from '@/lib/auth'export function Header() {
const { user } = appAuth.useAuth()
// ...
}
`$3
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.user matches 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-appUsersThis 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:`typescript
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:`typescript
export default buildConfig({
cookiePrefix: 'myapp',
admin: {
user: 'adminUsers',
},
// ...
})
`This would create:
-
myapp-token for admin users
- myapp-token-appUsers for app users$3
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:`typescript
// src/components/WorkOSLoginButton.tsx
'use client'import React from 'react'
import { LoginButton } from 'payload-auth-workos/client'
const WorkOSLoginButton = () => {
return (
href="/api/{name}/auth/signin"
label="Sign in with WorkOS"
/>
)
}
export default WorkOSLoginButton
`Then reference it in your Payload config:
`typescript
// 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:
`typescript
const CustomLoginButton = () => (
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
)export default buildConfig({
admin: {
user: 'adminUsers',
components: {
afterLogin: [CustomLoginButton],
},
},
// ...
})
`$3
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:`typescript
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:`typescript
// 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:
`typescript
const CustomLogoutButton = () => (
Sign out
)
`
$3
If you need more control over your collections, you can configure them manually using the provided utilities:
`typescript
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',
},
}),
],
})
`$3
`typescript
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.
},
})
`$3
`typescript
authPlugin({
name: 'app',
usersCollectionSlug: 'users',
accountsCollectionSlug: 'accounts',
workosProvider: { / ... / },
onError: async ({ error, req }) => {
console.error('Auth error:', error)
// Log to error tracking service
},
})
`$3
For enterprise customers using WorkOS organizations:
`typescript
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
},
})
`$3
For specific WorkOS connections (e.g., a custom SAML connection):
`typescript
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
},
})
`$3
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:`typescript
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 flow
- GET /api/{name}/auth/callback - Handles OAuth callback
- GET /api/{name}/auth/signout - Signs out the user
- GET /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
$3
The plugin adds/requires these fields in your users collection:
`typescript
{
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.$3
Stores OAuth account linkages:
`typescript
{
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
$3
A helper function that creates a complete accounts collection with sensible defaults:
`typescript
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
$3
A hook that automatically cleans up orphaned account records when a user is deleted:
`typescript
import { deleteLinkedAccounts } from 'payload-auth-workos/collection/hooks'const Users = {
slug: 'users',
hooks: {
beforeDelete: [deleteLinkedAccounts('accounts')],
},
// ... rest of config
}
`API Reference
$3
`typescript
import {
authPlugin,
createUsersCollection,
createAccountsCollection,
createWorkOSProviderConfig,
generateUserToken,
getPayloadCookies,
getExpiredPayloadCookies,
getAuthorizationUrl,
exchangeCodeForToken,
getUserInfo,
refreshAccessToken,
withAccountCollection,
deleteLinkedAccounts,
} from 'payload-auth-workos'
`-
authPlugin(config) - Main plugin function
- createUsersCollection(slug) - Creates a users collection
- createAccountsCollection(slug, usersSlug) - Creates an accounts collection
- createWorkOSProviderConfig(provider, config) - Creates reusable WorkOS provider config
- generateUserToken(payload, collection, userId) - Generates a JWT token
- getPayloadCookies(payload, collection, token) - Generates auth cookie strings (uses standard payload-token for admin collections, collection-specific cookies for others)
- getExpiredPayloadCookies(payload, collection) - Generates expired cookie strings for sign-out
- getAuthorizationUrl(config) - Generates WorkOS authorization URL
- exchangeCodeForToken(config, code) - Exchanges auth code for token
- getUserInfo(config, accessToken) - Gets user info from WorkOS
- refreshAccessToken(config, refreshToken) - Refreshes access token
- withAccountCollection(config, usersSlug) - Creates accounts collection with defaults
- deleteLinkedAccounts(accountsSlug) - Hook to delete linked accounts$3
For client-side components (use in files with
'use client' directive):`typescript
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 panel
- AdminLogoutButton - Payload-style logout button component for admin panel
- AuthProvider - Context provider for user sessions
- useAuth - Hook to access the current user session
- createAuthClient(slug) - Factory to create isolated auth clients for multi-collection setups
- LoginButtonProps - TypeScript type for LoginButton props
- AdminLogoutButtonProps - TypeScript type for AdminLogoutButton props
- AuthContextType - TypeScript type for auth context
- AuthProviderProps - TypeScript type for auth provider propsDevelopment
$3
`bash
Install dependencies
pnpm installBuild the plugin
pnpm buildRun in development mode
pnpm devRun tests
pnpm testLint
pnpm lint
`$3
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:`bash
Install from a feature branch
pnpm add github:MarkKropf/payload-auth-workos#your-branch
`$3
`text
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 configurations
`Troubleshooting
$3
Make sure you're using your WorkOS API Key (not Client ID) as the
client_secret.$3
WorkOS requires either
provider, connection, or organization in your configuration. Make sure you've specified one of these.$3
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/callbackWhere
{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 with useAdmin: true) ā http://127.0.0.1:3000/api/admin/auth/callback
- Plugin with name: 'app' ā http://127.0.0.1:3000/api/app/auth/callbackNote: 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.$3
If you see errors about collections not existing, make sure:
1. Your collection slugs match the ones specified in the plugin config
2. Collections are defined before the plugin is loaded
3. You've run type generation:
pnpm payload generate:typesExamples
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.
$3
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Run tests:
pnpm test
5. Run linter: pnpm lint`For issues and questions:
- GitHub Issues
- WorkOS Documentation
- Payload CMS Documentation
- Built for Payload CMS
- Powered by WorkOS