clerk-enterprise-rbac

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Clerk Enterprise RBAC

Clerk 企业级RBAC

Overview

概述

Implement enterprise-grade SSO, role-based access control, and organization management.
实现企业级SSO、基于角色的访问控制(RBAC)以及组织管理。

Prerequisites

前提条件

  • Clerk Enterprise tier subscription
  • Identity Provider (IdP) with SAML/OIDC support
  • Understanding of role-based access patterns
  • Organization structure defined
  • 订阅Clerk企业版
  • 支持SAML/OIDC协议的身份提供商(IdP)
  • 了解基于角色的访问模式
  • 已定义组织架构

Instructions

操作步骤

Step 1: Configure SAML SSO

步骤1:配置SAML SSO

In Clerk Dashboard

在Clerk控制台中

  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
  1. 进入 配置 > SSO连接
  2. 添加SAML连接
  3. 配置IdP设置:
    • ACS URL:
      https://clerk.yourapp.com/v1/saml
    • Entity ID: 由Clerk提供
    • 下载SP元数据

IdP Configuration (Example: Okta)

IdP配置(示例: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>
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

步骤2:定义角色与权限

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
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

步骤3:权限校验

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)
    }
  }
}
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

步骤4:基于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()
})
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

步骤5:组织管理

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
}
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

步骤6:集成RBAC的React组件

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>
  )
}
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

步骤7:API路由保护

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)
}
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

SSO配置矩阵

IdPProtocolSetup Guide
OktaSAML 2.0Clerk Dashboard > SSO
Azure ADOIDC/SAMLClerk Dashboard > SSO
Google WorkspaceOIDCClerk Dashboard > SSO
OneLoginSAML 2.0Clerk Dashboard > SSO
IdP协议设置指南
OktaSAML 2.0Clerk控制台 > SSO
Azure ADOIDC/SAMLClerk控制台 > SSO
Google WorkspaceOIDCClerk控制台 > SSO
OneLoginSAML 2.0Clerk控制台 > SSO

Output

输出结果

  • SAML SSO configured
  • Roles and permissions defined
  • RBAC enforcement in middleware
  • Organization management
  • 已配置SAML SSO
  • 已定义角色与权限
  • 中间件中已启用RBAC强制校验
  • 已实现组织管理

Error Handling

错误处理

ErrorCauseSolution
SSO login failsMisconfigured IdPCheck attribute mapping
Permission deniedMissing roleReview role assignments
Org not foundUser not in orgPrompt org selection
错误原因解决方案
SSO登录失败IdP配置错误检查属性映射
权限被拒绝缺少对应角色检查角色分配
组织未找到用户未加入组织提示用户选择组织

Resources

参考资源

Next Steps

后续步骤

Proceed to
clerk-migration-deep-dive
for auth provider migration.
继续学习
clerk-migration-deep-dive
进行身份认证提供商迁移。