WorkOS provides enterprise-grade authentication — SSO, Directory Sync, and user management — for B2B SaaS applications. workos.userManagement.getAuthorizationUrl({ provider, redirectUri, state }) starts SSO. workos.userManagement.authenticateWithCode({ code, clientId }) exchanges the OAuth code for a user session. workos.sso.getAuthorizationUrl({ connection, redirectUri }) initiates connection-specific SAML/OIDC. workos.directorySync.listUsers({ directory, limit }) reads users from SCIM directory sync. Directory Sync webhooks notify of dsync.user.created/updated/deleted events for automated provisioning. workos.userManagement.impersonateUser(userId) enables support impersonation. withAuth() middleware from @workos-inc/authkit-nextjs protects App Router pages. AuthKit provides a hosted sign-in UI at your domain with email/password, Magic Auth, and Google OAuth. Claude Code generates WorkOS SSO flows, Directory Sync webhook handlers, impersonation setup, and Next.js AuthKit integration.
CLAUDE.md for WorkOS
## WorkOS Stack
- Version: @workos-inc/node >= 7.17, @workos-inc/authkit-nextjs >= 0.13
- AuthKit: withAuth() middleware — const { user } = await withAuth({ ensureSignedIn: true })
- SSO: workos.userManagement.getAuthorizationUrl({ provider: "authkit", redirectUri, state })
- Exchange: workos.userManagement.authenticateWithCode({ code, clientId, session: true })
- DirSync: workos.directorySync.listUsers({ directory, limit, after }) — SCIM users
- Webhook: workos.webhooks.constructEvent(payload, sig, secret) → dsync.user.created/updated
- Impersonate: workos.userManagement.impersonateUser(userId, { adminId, reason })
- Portal: workos.portal.generateLink({ organization, intent: "sso" }) — admin self-service
AuthKit Setup
// middleware.ts — protect routes with WorkOS AuthKit
import { authkitMiddleware } from "@workos-inc/authkit-nextjs"
export default authkitMiddleware({
middlewareAuth: {
enabled: true,
// Routes requiring authentication
unauthenticatedPaths: [
"/",
"/sign-in",
"/sign-up",
"/api/webhooks/(.*)",
"/pricing",
"/blog/(.*)",
],
},
})
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
}
// lib/workos.ts — WorkOS client
import WorkOS from "@workos-inc/node"
export const workos = new WorkOS(process.env.WORKOS_API_KEY!)
export const WORKOS_CLIENT_ID = process.env.WORKOS_CLIENT_ID!
Server Components
// app/dashboard/page.tsx — Server Component with WorkOS session
import { withAuth } from "@workos-inc/authkit-nextjs"
import { workos } from "@/lib/workos"
import { db } from "@/lib/db"
export default async function DashboardPage() {
// withAuth returns the authenticated user or redirects
const { user } = await withAuth({ ensureSignedIn: true })
// Fetch org membership
const memberships = await workos.userManagement.listOrganizationMemberships({
userId: user.id,
})
const firstOrg = memberships.data[0]
const organization = firstOrg
? await workos.organizations.getOrganization(firstOrg.organizationId)
: null
return (
<div>
<h1>Welcome, {user.firstName}</h1>
<p className="text-muted-foreground">{user.email}</p>
{organization && (
<p className="text-sm">Organization: {organization.name}</p>
)}
</div>
)
}
SSO Callback Handler
// app/api/auth/callback/route.ts — WorkOS SSO callback
import { NextRequest, NextResponse } from "next/server"
import { workos, WORKOS_CLIENT_ID } from "@/lib/workos"
import { db } from "@/lib/db"
import { cookies } from "next/headers"
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const code = searchParams.get("code")
const error = searchParams.get("error")
if (error) {
return NextResponse.redirect(new URL(`/sign-in?error=${error}`, request.url))
}
if (!code) {
return NextResponse.redirect(new URL("/sign-in?error=missing_code", request.url))
}
try {
const { user, accessToken, refreshToken, impersonator } =
await workos.userManagement.authenticateWithCode({
code,
clientId: WORKOS_CLIENT_ID,
session: true,
})
// Upsert user in your database
const dbUser = await db.user.upsert({
where: { workosId: user.id },
create: {
workosId: user.id,
email: user.email,
name: [user.firstName, user.lastName].filter(Boolean).join(" "),
avatarUrl: user.profilePictureUrl,
},
update: {
email: user.email,
name: [user.firstName, user.lastName].filter(Boolean).join(" "),
avatarUrl: user.profilePictureUrl,
lastSignedInAt: new Date(),
},
})
// Store session token
const cookieStore = await cookies()
cookieStore.set("workos_session", accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, // 1 week
})
// Log if impersonation session
if (impersonator) {
console.log(`User ${user.email} impersonated by admin ${impersonator.email}`)
}
return NextResponse.redirect(new URL("/dashboard", request.url))
} catch (err) {
console.error("[WorkOS Callback]", err)
return NextResponse.redirect(new URL("/sign-in?error=auth_failed", request.url))
}
}
Directory Sync Webhook
// app/api/webhooks/workos/route.ts — SCIM provisioning webhook
import { NextRequest, NextResponse } from "next/server"
import { workos } from "@/lib/workos"
import { db } from "@/lib/db"
export async function POST(request: NextRequest) {
const payload = await request.text()
const sigHeader = request.headers.get("workos-signature")!
let event: ReturnType<typeof workos.webhooks.constructEvent>
try {
event = workos.webhooks.constructEvent(
payload,
sigHeader,
process.env.WORKOS_WEBHOOK_SECRET!,
)
} catch {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 })
}
switch (event.event) {
case "dsync.user.created": {
const dirUser = event.data
await db.user.upsert({
where: { directoryUserId: dirUser.id },
create: {
directoryUserId: dirUser.id,
email: dirUser.emails[0]?.value ?? "",
name: `${dirUser.firstName} ${dirUser.lastName}`.trim(),
role: mapGroupsToRole(dirUser.groups?.map(g => g.name) ?? []),
active: dirUser.state === "active",
departmentId: await getDepartmentId(dirUser.groups),
},
update: {
email: dirUser.emails[0]?.value ?? "",
name: `${dirUser.firstName} ${dirUser.lastName}`.trim(),
active: dirUser.state === "active",
},
})
break
}
case "dsync.user.updated": {
const dirUser = event.data
await db.user.update({
where: { directoryUserId: dirUser.id },
data: {
email: dirUser.emails[0]?.value ?? "",
name: `${dirUser.firstName} ${dirUser.lastName}`.trim(),
role: mapGroupsToRole(dirUser.groups?.map(g => g.name) ?? []),
active: dirUser.state === "active",
},
})
break
}
case "dsync.user.deleted": {
await db.user.update({
where: { directoryUserId: event.data.id },
data: { active: false, deletedAt: new Date() },
})
break
}
case "dsync.group.user_added": {
const { user: { id: userId }, group: { name: groupName } } = event.data
const newRole = mapGroupsToRole([groupName])
if (newRole) {
await db.user.update({
where: { directoryUserId: userId },
data: { role: newRole },
})
}
break
}
}
return NextResponse.json({ ok: true })
}
function mapGroupsToRole(groups: string[]): string {
if (groups.some(g => g.toLowerCase().includes("admin"))) return "admin"
if (groups.some(g => g.toLowerCase().includes("manager"))) return "manager"
return "member"
}
async function getDepartmentId(groups?: { id: string; name: string }[]): Promise<string | null> {
if (!groups?.length) return null
const dept = groups.find(g => !g.name.toLowerCase().includes("admin"))
if (!dept) return null
return db.department.findFirst({ where: { name: dept.name } })
.then(d => d?.id ?? null)
}
For the Clerk alternative when all-in-one user management with a hosted UI, per-seat pricing, and simpler setup for consumer or SMB B2B apps is preferred over WorkOS’s enterprise focus — Clerk covers the same auth primitives (email/password, OAuth, MFA) with a more developer-friendly API for apps without enterprise SSO requirements, see the Clerk guide. For the Auth0 alternative when a mature identity platform with a large marketplace of enterprise integrations, fine-grained authorization (Auth0 FGA), and attack protection rules is needed — Auth0 has a broader ecosystem for complex enterprise identity requirements with more granular security controls than WorkOS. The Claude Skills 360 bundle includes WorkOS skill sets covering SSO, Directory Sync, and AuthKit integration. Start with the free tier to try enterprise auth generation.