Adds WorkOS-based authentication to Payload through generated signin, callback, signout and session endpoints. It creates or extends users and accounts collections, and can be used for the admin panel, for app users, or for several collections at once. Requires a WorkOS client ID, API key and a cookie password of at least 32 characters.
Payload CMS authentication plugin for WorkOS
Installation
npm i payload-auth-workos5
Stars
2
Forks
1
Open Issues
5 months ago
Last Updated
0
Weekly Downloads
v1.0.12
npm Version
219.1 KB
Unpacked Size
2
Dependencies
Trends appear after 7 days of data. Daily snapshots have not started yet.
Trends appear after 7 days of data. Daily snapshots have not started yet.
Each point is npm's rolling 7-day download count.
Health Score
Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.
Badge for your README
[](https://payloaddirectory.dev/plugins/1097851106-root)<a href="https://payloaddirectory.dev/plugins/1097851106-root"><img src="https://payloaddirectory.dev/badge/1097851106-root.svg" alt="Payload Directory"></a>README
A Payload CMS authentication plugin that integrates WorkOS for OAuth-based user authentication.
# 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.com
The package is published to both:
payload-auth-workos (unscoped) - View on npm@markkropf/payload-auth-workos (scoped) - View on GitHubFirst, create a WorkOS account and set up an application at workos.com. You'll need:
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).
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=GoogleOAuth
You can generate a secure cookie password using:
openssl rand -base64 32
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
},
}),
],
})
// 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).
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>
}
| 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 |
| 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:
provider, connection, or organization. WorkOS requires one of these connection selectors.redirect_uri is automatically generated as /api/{name}/auth/callback and does not need to be configured.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,
}),
],
})
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()
// ...
}
When using multiple auth collections, the plugin automatically manages cookies to prevent conflicts and ensure compatibility with Payload's admin panel.
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 compatibilityNon-admin collections:
payload-token-{collectionSlug} - Collection-specific cookieWith the configuration above, you'll get these cookies:
payload-tokenpayload-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.
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
},
// ...
})
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-token for admin usersmyapp-token-appUsers for app usersWhen 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 WorkOSLoginButton
Then 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.
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],
},
},
// ...
})
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.
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>
)
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',
},
}),
],
})
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.
},
})
authPlugin({
name: 'app',
usersCollectionSlug: 'users',
accountsCollectionSlug: 'accounts',
workosProvider: { /* ... */ },
onError: async ({ error, req }) => {
console.error('Auth error:', error)
// Log to error tracking service
},
})
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
},
})
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
},
})
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`
},
})
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.
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.
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
}
withAccountCollectionA 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:
deleteLinkedAccountsA 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
}
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 standard payload-token for 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 accountsFor 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# Install dependencies
pnpm install
# Build the plugin
pnpm build
# Run in development mode
pnpm dev
# Run tests
pnpm test
# Lint
pnpm lint
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-branch
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
Make sure you're using your WorkOS API Key (not Client ID) as the client_secret.
WorkOS requires either provider, connection, or organization in your configuration. Make sure you've specified one of these.
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:
{baseUrl}/api/{name}/auth/callbackuseAdmin: true): {baseUrl}/api/{name}/auth/callbackWhere {name} is the value you specified in your plugin's name configuration.
Examples:
name: 'workos-auth' ā http://127.0.0.1:3000/api/workos-auth/auth/callbackname: 'admin' (even with useAdmin: true) ā http://127.0.0.1:3000/api/admin/auth/callbackname: '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.
If you see errors about collections not existing, make sure:
pnpm payload generate:typesCheck the /examples directory for complete working examples:
MIT
Contributions are welcome! Please open an issue or submit a pull request.
pnpm testpnpm lintFor issues and questions:
payload better auth
Better Auth database adapter and plugins that replace Payload's local auth strategy
700/wk74v3
payload auth
Replaces Payload's built-in auth with Better Auth, sharing one session between the admin panel and your app
590/wk344v3
payload authjs
Use Auth.js 5 providers to sign in and authenticate users in Payload CMS 3
560/wk207v3
payload auth plugin
Authentication plugin adding OAuth, OpenID Connect, SSO and passkey sign-in to Payload CMS
500/wk310v3
payload auth cookie
Authenticate Payload admin and frontend users from cookies set by an external SSO system
4019/wk3v3
payload better auth
Integrates the Better Auth library into Payload CMS 3 for social login, two-factor auth and role-based access
3474v3