home / skills / jeremylongshore / claude-code-plugins-plus-skills / clerk-enterprise-rbac

This skill helps implement enterprise SSO, RBAC, and organization management with Clerk across apps, enforcing permissions and secure access.

npx playbooks add skill jeremylongshore/claude-code-plugins-plus-skills --skill clerk-enterprise-rbac

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
9.3 KB
---
name: clerk-enterprise-rbac
description: |
  Configure enterprise SSO, role-based access control, and organization management.
  Use when implementing SSO integration, configuring role-based permissions,
  or setting up organization-level controls.
  Trigger with phrases like "clerk SSO", "clerk RBAC",
  "clerk enterprise", "clerk roles", "clerk permissions", "clerk SAML".
allowed-tools: Read, Write, Edit, Grep
version: 1.0.0
license: MIT
author: Jeremy Longshore <[email protected]>
---

# Clerk Enterprise RBAC

## Overview
Implement enterprise-grade SSO, role-based access control, and organization management.

## Prerequisites
- Clerk Enterprise tier subscription
- Identity Provider (IdP) with SAML/OIDC support
- Understanding of role-based access patterns
- Organization structure defined

## Instructions

### Step 1: Configure SAML SSO

#### In Clerk Dashboard
1. Go to Configure > SSO Connections
2. Add SAML Connection
3. Configure IdP settings:
   - ACS URL: `https://clerk.yourapp.com/v1/saml`
   - Entity ID: Provided by Clerk
   - Download SP metadata

#### IdP Configuration (Example: Okta)
```xml
<!-- SAML Attributes to map -->
<saml:Attribute Name="email">
  <saml:AttributeValue>user.email</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="firstName">
  <saml:AttributeValue>user.firstName</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="lastName">
  <saml:AttributeValue>user.lastName</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="role">
  <saml:AttributeValue>user.role</saml:AttributeValue>
</saml:Attribute>
```

### Step 2: Define Roles and Permissions
```typescript
// lib/permissions.ts

// Define all permissions in your system
export const PERMISSIONS = {
  // Resource: Action
  'users:read': 'View user list',
  'users:write': 'Create/update users',
  'users:delete': 'Delete users',
  'settings:read': 'View settings',
  'settings:write': 'Modify settings',
  'billing:read': 'View billing info',
  'billing:write': 'Manage billing',
  'reports:read': 'View reports',
  'reports:export': 'Export reports'
} as const

export type Permission = keyof typeof PERMISSIONS

// Define roles with their permissions
export const ROLES = {
  'org:admin': [
    'users:read', 'users:write', 'users:delete',
    'settings:read', 'settings:write',
    'billing:read', 'billing:write',
    'reports:read', 'reports:export'
  ],
  'org:manager': [
    'users:read', 'users:write',
    'settings:read',
    'reports:read', 'reports:export'
  ],
  'org:member': [
    'users:read',
    'reports:read'
  ],
  'org:viewer': [
    'reports:read'
  ]
} as const satisfies Record<string, Permission[]>

export type Role = keyof typeof ROLES
```

### Step 3: Permission Checking
```typescript
// lib/auth-permissions.ts
import { auth } from '@clerk/nextjs/server'
import { ROLES, Permission, Role } from './permissions'

export async function hasPermission(permission: Permission): Promise<boolean> {
  const { orgRole } = await auth()

  if (!orgRole) return false

  const role = orgRole as Role
  const rolePermissions = ROLES[role]

  if (!rolePermissions) return false

  return rolePermissions.includes(permission)
}

export async function requirePermission(permission: Permission): Promise<void> {
  const allowed = await hasPermission(permission)

  if (!allowed) {
    throw new Error(`Permission denied: ${permission}`)
  }
}

// Decorator pattern for API routes
export function withPermission(permission: Permission) {
  return async function(
    handler: (req: Request) => Promise<Response>
  ): Promise<(req: Request) => Promise<Response>> {
    return async (req: Request) => {
      const allowed = await hasPermission(permission)

      if (!allowed) {
        return Response.json(
          { error: 'Permission denied', required: permission },
          { status: 403 }
        )
      }

      return handler(req)
    }
  }
}
```

### Step 4: Protected Routes with RBAC
```typescript
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

const isPublicRoute = createRouteMatcher(['/', '/sign-in(.*)', '/sign-up(.*)'])
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
const isBillingRoute = createRouteMatcher(['/billing(.*)'])

export default clerkMiddleware(async (auth, request) => {
  const { userId, orgRole } = await auth()

  if (isPublicRoute(request)) {
    return NextResponse.next()
  }

  if (!userId) {
    return auth.redirectToSignIn()
  }

  // Admin routes require admin role
  if (isAdminRoute(request)) {
    if (orgRole !== 'org:admin') {
      return NextResponse.redirect(new URL('/unauthorized', request.url))
    }
  }

  // Billing routes require admin or manager
  if (isBillingRoute(request)) {
    if (!['org:admin', 'org:manager'].includes(orgRole || '')) {
      return NextResponse.redirect(new URL('/unauthorized', request.url))
    }
  }

  return NextResponse.next()
})
```

### Step 5: Organization Management
```typescript
// lib/organization.ts
import { clerkClient, auth } from '@clerk/nextjs/server'

export async function createOrganization(name: string, slug: string) {
  const { userId } = await auth()
  const client = await clerkClient()

  const org = await client.organizations.createOrganization({
    name,
    slug,
    createdBy: userId!
  })

  return org
}

export async function inviteToOrganization(
  orgId: string,
  email: string,
  role: string
) {
  const client = await clerkClient()

  const invitation = await client.organizations.createOrganizationInvitation({
    organizationId: orgId,
    emailAddress: email,
    role,
    inviterUserId: (await auth()).userId!
  })

  return invitation
}

export async function updateMemberRole(
  orgId: string,
  userId: string,
  role: string
) {
  const client = await clerkClient()

  await client.organizations.updateOrganizationMembership({
    organizationId: orgId,
    userId,
    role
  })
}

export async function getOrganizationMembers(orgId: string) {
  const client = await clerkClient()

  const { data: members } = await client.organizations.getOrganizationMembershipList({
    organizationId: orgId
  })

  return members
}
```

### Step 6: React Components with RBAC
```typescript
// components/permission-gate.tsx
'use client'
import { useAuth, useOrganization } from '@clerk/nextjs'
import { ROLES, Permission, Role } from '@/lib/permissions'

interface PermissionGateProps {
  permission: Permission
  children: React.ReactNode
  fallback?: React.ReactNode
}

export function PermissionGate({
  permission,
  children,
  fallback = null
}: PermissionGateProps) {
  const { orgRole } = useAuth()

  if (!orgRole) return fallback

  const role = orgRole as Role
  const permissions = ROLES[role] || []

  if (!permissions.includes(permission)) {
    return fallback
  }

  return <>{children}</>
}

// Usage
function AdminPanel() {
  return (
    <div>
      <h1>Dashboard</h1>

      <PermissionGate permission="users:write">
        <button>Add User</button>
      </PermissionGate>

      <PermissionGate permission="billing:read">
        <BillingSection />
      </PermissionGate>

      <PermissionGate
        permission="settings:write"
        fallback={<p>Contact admin for settings access</p>}
      >
        <SettingsForm />
      </PermissionGate>
    </div>
  )
}
```

### Step 7: API Route Protection
```typescript
// app/api/admin/users/route.ts
import { auth } from '@clerk/nextjs/server'
import { hasPermission } from '@/lib/auth-permissions'

export async function GET() {
  const { userId, orgId } = await auth()

  if (!userId || !orgId) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  if (!await hasPermission('users:read')) {
    return Response.json({ error: 'Forbidden' }, { status: 403 })
  }

  // Fetch users scoped to organization
  const users = await db.user.findMany({
    where: { organizationId: orgId }
  })

  return Response.json(users)
}

export async function POST(request: Request) {
  const { userId, orgId } = await auth()

  if (!userId || !orgId) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  if (!await hasPermission('users:write')) {
    return Response.json({ error: 'Forbidden' }, { status: 403 })
  }

  const data = await request.json()

  const user = await db.user.create({
    data: {
      ...data,
      organizationId: orgId,
      createdBy: userId
    }
  })

  return Response.json(user)
}
```

## SSO Configuration Matrix

| IdP | Protocol | Setup Guide |
|-----|----------|-------------|
| Okta | SAML 2.0 | Clerk Dashboard > SSO |
| Azure AD | OIDC/SAML | Clerk Dashboard > SSO |
| Google Workspace | OIDC | Clerk Dashboard > SSO |
| OneLogin | SAML 2.0 | Clerk Dashboard > SSO |

## Output
- SAML SSO configured
- Roles and permissions defined
- RBAC enforcement in middleware
- Organization management

## Error Handling
| Error | Cause | Solution |
|-------|-------|----------|
| SSO login fails | Misconfigured IdP | Check attribute mapping |
| Permission denied | Missing role | Review role assignments |
| Org not found | User not in org | Prompt org selection |

## Resources
- [Clerk SSO Guide](https://clerk.com/docs/authentication/saml)
- [Organizations](https://clerk.com/docs/organizations/overview)
- [Roles & Permissions](https://clerk.com/docs/organizations/roles-permissions)

## Next Steps
Proceed to `clerk-migration-deep-dive` for auth provider migration.

Overview

This skill configures enterprise SSO, role-based access control (RBAC), and organization management for Clerk-powered applications. It provides patterns for SAML/OIDC integration, role and permission definitions, middleware enforcement, and organization lifecycle operations. Use it to centralize authentication, scope resources by organization, and enforce least-privilege access.

How this skill works

The skill guides you through registering a SAML or OIDC connection in the Clerk dashboard and mapping IdP attributes (email, name, role). It defines a canonical permission set and role-to-permission mappings, exposes server-side checks and decorators, and plugs RBAC into middleware and API routes. Organization helpers cover creation, invitations, role updates, and member listing for scoped operations.

When to use it

  • Integrating enterprise SSO (SAML or OIDC) with your application
  • Enforcing role-based permissions across server and client code
  • Protecting admin, billing, or sensitive routes by org role
  • Managing organizations, invitations, and membership programmatically
  • Scoping database queries and API responses to an organization

Best practices

  • Map IdP attributes (email, firstName, lastName, role) consistently and validate on sign-in
  • Define a minimal, descriptive permission vocabulary and reuse it across services
  • Keep authorization logic centralized (hasPermission, requirePermission, middleware) to avoid duplication
  • Use PermissionGate components on the client to hide UI elements when access is denied
  • Audit role assignments and provide clear fallback/unauthorized UX to users

Example use cases

  • Add Okta SAML SSO to a Next.js app with ACS URL from Clerk and attribute mappings
  • Define roles like org:admin, org:manager, org:member and use them to gate API endpoints
  • Protect /admin and /billing routes with middleware checks redirecting unauthorized users
  • Create organizations and send invitation emails programmatically using clerkClient helpers
  • Render admin actions conditionally in React using PermissionGate around buttons or forms

FAQ

What IdP attributes are required?

At minimum map email and a role attribute. First and last name help user profiles. Ensure your IdP sends a stable role claim your app expects.

How do I test permissions locally?

Mock auth responses with orgRole and orgId, or use Clerk's dev/test tooling. Unit-test hasPermission with representative role values to validate mappings.