@hmp-global/payload-cas-auth
v0.2.2
Published
CAS SSO authentication plugin for Payload CMS v3 + Next.js 16.
Readme
@hmp-global/payload-cas-auth
CAS SSO authentication plugin for Payload CMS v3 + Next.js 16.
- Registers CAS login/callback/logout as Payload API endpoints (no per-app route files needed)
- JWT session management (signed with your own secret)
- Role-based access control derived from CAS attributes
- Seamless Payload admin panel bridging — users go straight to
/adminafter CAS login - Configurable CAS attribute → application role mapping
- Optional middleware role gates for path-level access control
- Dev bypass mode with injected role for local development
Installation
npm install @hmp-global/payload-cas-auth
# or
pnpm add @hmp-global/payload-cas-authPeer dependencies (install if not already present): next >= 16, payload >= 3, jose >= 5
Publishing a new version
- Bump the
versioninpackage.json - Add an
NPM_TOKENsecret to the GitHub repo (create one at npmjs.com → Access Tokens → Automation token) - Create a GitHub Release — the
publish.ymlworkflow builds and publishes to npm automatically
Setup — two files
1. payload.config.ts
import { buildConfig } from 'payload'
import { casAuthPlugin } from '@hmp-global/payload-cas-auth'
export default buildConfig({
// ...
plugins: [
casAuthPlugin({
casBaseUrl: process.env.CAS_BASE_URL!,
sessionSecret: process.env.CAS_SESSION_SECRET!,
publicBaseUrl: process.env.PUBLIC_BASE_URL, // e.g. https://app.example.com
postLoginRedirectPath: '/admin', // fallback when no returnTo is present
enabled: process.env.CAS_ENABLED !== 'false',
roleAttribute: 'department',
roleGroups: {
engineering: 'developer',
marketing: 'marketer',
admin: 'admin',
},
}),
],
})This registers four endpoints automatically:
| Method | Path | Purpose |
|--------|------|---------|
| GET | /api/auth/cas/login | Redirects to CAS server |
| GET | /api/auth/cas/callback | Validates ticket, sets session cookie |
| GET | /api/auth/logout | Clears session cookie |
| GET | /api/auth/admin-session | Bridges CAS session → Payload admin cookie |
2. src/middleware.ts
import { createCasMiddleware } from '@hmp-global/payload-cas-auth/middleware'
export const { middleware, config } = createCasMiddleware({
casBaseUrl: process.env.CAS_BASE_URL!,
sessionSecret: process.env.CAS_SESSION_SECRET!,
publicBaseUrl: process.env.PUBLIC_BASE_URL,
enabled: process.env.CAS_ENABLED !== 'false',
devRole: process.env.DEV_ROLE ?? 'admin',
// Paths that require CAS login (default: ['/dashboard', '/chat', '/admin'])
protectedPaths: ['/dashboard', '/chat', '/admin'],
// Optional path gates for roles resolved during CAS callback
roleGates: [
{ path: '/dashboard/cdn', allowedRoles: ['admin', 'developer'], redirectTo: '/dashboard' },
{ path: '/admin', allowedRoles: ['admin', 'developer'], redirectTo: '/dashboard' },
],
})Environment variables
| Variable | Required | Description |
|----------|----------|-------------|
| CAS_BASE_URL | ✅ | CAS server base URL, e.g. https://login.example.com/cas |
| CAS_SESSION_SECRET | ✅ | Secret for signing session JWTs (32+ random chars) |
| PUBLIC_BASE_URL | Recommended | Public hostname for CAS redirect URIs |
| CAS_ENABLED | — | Set to false to bypass CAS in dev. Default: true |
| DEV_ROLE | — | Role to inject when CAS_ENABLED=false. Default: admin |
Login redirects
The middleware preserves the originally requested protected path by passing a safe internal returnTo path through /api/auth/cas/login and /api/auth/cas/callback. For example, visiting /admin redirects through CAS and returns to /admin after the CAS session cookie is set.
If a login starts without a returnTo value, the callback redirects to postLoginRedirectPath. That option defaults to /dashboard for backward compatibility. Set it to /admin for Payload-admin-only apps.
External redirect targets are ignored; only internal paths that start with a single / are accepted.
Role configuration
The plugin reads a CAS attribute to determine each user's role:
casAuthPlugin({
// ...
roleAttribute: 'department', // CAS attribute name (default: 'role')
roleGroups: {
engineering: 'developer',
development: 'developer',
marketing: 'marketer',
content: 'marketer',
'it-admin': 'admin',
},
defaultRole: 'viewer',
})If the role-mappings collection is enabled (default), the CAS callback checks cas-role-mappings in Payload first, then falls back to roleGroups.
Role gates
Use middleware roleGates to restrict paths by resolved role:
roleGates: [
{ path: '/dashboard/cdn', allowedRoles: ['admin', 'developer'], redirectTo: '/dashboard' },
{ path: '/admin', allowedRoles: ['admin'], redirectTo: '/dashboard' },
]Using the role in your app
The middleware injects the resolved role as an x-user-role request header:
// In a server component or API route:
import { headers } from 'next/headers'
import { ROLE_HEADER } from '@hmp-global/payload-cas-auth'
const reqHeaders = await headers()
const role = reqHeaders.get(ROLE_HEADER) ?? 'unknown'
const canSeeCdn = role === 'admin' || role === 'developer'Admin panel auto-login
When a CAS-authenticated user visits /admin:
- Middleware sends unauthenticated
/adminvisits through CAS withreturnTo=/admin - CAS callback sets the CAS session cookie and redirects back to
/admin - Middleware sees the
_cas_adminmarker cookie is absent → routes to/api/auth/admin-session - The bridge finds/creates their Payload user account by email
- Syncs a deterministic server-side password (
HMAC(email, sessionSecret)) - Calls
payload.login()with that password → gets a verified Payload JWT - Sets
payload-token+_cas_admincookies → redirects to/admin - User lands directly in the admin panel — no separate login prompt
For apps served from multiple subdomains, set cookies.domain to the shared parent
domain, such as .example.com. This lets the CAS session, Payload admin token, and
admin marker cookie work across tenant subdomains.
Existing Payload accounts are matched by email. If you already created an admin account via the Payload email/password flow, the bridge will find it and update its password to the deterministic value (the original password is replaced — Payload admin login is intentionally replaced by CAS).
To limit admin access to specific CAS roles, add an /admin roleGate in middleware.
Finding your CAS attribute name
After first login with CAS_ENABLED=true, the server logs will print:
[cas-auth] login user=jsmith [email protected] role=unknown attrs={"department":"Engineering","uid":"jsmith"}If role=unknown, look at the attrs keys and set roleAttribute to the right key (e.g. department). Then add the values to devGroups/marketingGroups etc.
