āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā š shadcn/directory/clerk/clerk-docs/guides/secure/basic-rbac ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
To control which users can access certain parts of your app, you can use the roles feature. Although Clerk offers roles as part of the organizations feature set, not every app implements organizations. This guide covers a workaround to set up a basic Role Based Access Control (RBAC) system for products that don't use Clerk's organizations or roles.
This guide assumes that you're using Next.js App Router, but the concepts can be adapted to Next.js Pages Router and Remix.
<Steps> ## Configure the session tokenClerk provides user metadata, which can be used to store information, and in this case, it can be used to store a user's role. Since publicMetadata can only be read but not modified in the browser, it is the safest and most appropriate choice for storing information.
To build a basic RBAC system, you first need to make publicMetadata available to the application directly from the session token. By attaching publicMetadata to the user's session, you can access the data without needing to make a network request each time.
{
"metadata": "{{user.public_metadata}}"
}
types/ directory.globals.d.ts file with the following code. This file will provide auto-completion and prevent TypeScript errors when working with roles. For this guide, only the admin and moderator roles will be defined.export {}
// Create a type for the roles
export type Roles = 'admin' | 'moderator'
declare global {
interface CustomJwtSessionClaims {
metadata: {
role?: Roles
}
}
}
Later in the guide, you will add a basic admin tool to change a user's role. For now, manually add the admin role to your own user account.
{
"role": "admin"
}
Create a helper function to simplify checking roles.
utils/ folder.roles.ts file with the following code. The checkRole() helper uses the auth() helper to access the user's session claims. From the session claims, it accesses the metadata object to check the user's role. The checkRole() helper accepts a role of type Roles, which you created in the Create a global TypeScript definition step. It returns true if the user has that role or false if they do not.import { Roles } from '@/types/globals'
import { auth } from '@clerk/nextjs/server'
export const checkRole = async (role: Roles) => {
const { sessionClaims } = await auth()
return sessionClaims?.metadata.role === role
}
[!NOTE] You can customize the behavior of the
checkRole()helper function to suit your needs. For example, you could modify it to return the roles a user has or create aprotectByRole()function that handles role-based redirects.
Now, it's time to create an admin dashboard. The first step is to create the /admin route.
app/ directory, create an admin/ folder.admin/ folder, create a page.tsx file with the following placeholder code.export default function AdminDashboard() {
return <p>This is the protected admin dashboard restricted to users with the `admin` role.</p>
}
To protect the /admin route, choose one of the two following methods:
/admin before the request reaches the actual page./admin page component. This method protects this specific page. To protect other pages in the admin dashboard, apply this protection to each route.[!IMPORTANT] You only need to follow one of the following methods to secure your
/adminroute.
/admin route using middlewareproxy.ts file with the following code. The createRouteMatcher() function identifies routes starting with /admin. clerkMiddleware() intercepts requests to the /admin route, and checks the user's role in their metadata to verify that they have the admin role. If they don't, it redirects them to the home page.import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
export default clerkMiddleware(async (auth, req) => {
// Protect all routes starting with `/admin`
if (isAdminRoute(req) && (await auth()).sessionClaims?.metadata?.role !== 'admin') {
const url = new URL('/', req.url)
return NextResponse.redirect(url)
}
})
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
}
/admin route at the page-levelapp/admin/page.tsx file. The checkRole() function checks if the user has the admin role. If they don't, it redirects them to the home page.import { checkRole } from '@/utils/roles'
import { redirect } from 'next/navigation'
export default async function AdminDashboard() {
// Protect the page from users who are not admins
const isAdmin = await checkRole('admin')
if (!isAdmin) {
redirect('/')
}
return <p>This is the protected admin dashboard restricted to users with the `admin` role.</p>
}
app/admin/ directory, create an _actions.ts file with the following code. The setRole() action checks that the current user has the admin role before updating the specified user's role using Clerk's JS Backend SDK. The removeRole() action removes the role from the specified user.'use server'
import { checkRole } from '@/utils/roles'
import { clerkClient } from '@clerk/nextjs/server'
export async function setRole(formData: FormData) {
const client = await clerkClient()
// Check that the user trying to set the role is an admin
if (!checkRole('admin')) {
return { message: 'Not Authorized' }
}
try {
const res = await client.users.updateUserMetadata(formData.get('id') as string, {
publicMetadata: { role: formData.get('role') },
})
return { message: res.publicMetadata }
} catch (err) {
return { message: err }
}
}
export async function removeRole(formData: FormData) {
const client = await clerkClient()
try {
const res = await client.users.updateUserMetadata(formData.get('id') as string, {
publicMetadata: { role: null },
})
return { message: res.publicMetadata }
} catch (err) {
return { message: err }
}
}
app/admin/ directory, create a SearchUsers.tsx file with the following code. The <SearchUsers /> component includes a form for searching for users. When submitted, it appends the search term to the URL as a search parameter. Your /admin route will then perform a query based on the updated URL.'use client'
import { usePathname, useRouter } from 'next/navigation'
export const SearchUsers = () => {
const router = useRouter()
const pathname = usePathname()
return (
<div>
<form
onSubmit={(e) => {
e.preventDefault()
const form = e.currentTarget
const formData = new FormData(form)
const queryTerm = formData.get('search') as string
router.push(pathname + '?search=' + queryTerm)
}}
>
<label htmlFor="search">Search for users</label>
<input id="search" name="search" type="text" />
<button type="submit">Submit</button>
</form>
</div>
)
}
With the server action and the search form set up, it's time to refactor the app/admin/page.tsx.
app/admin/page.tsx file with the following code. It checks whether a search parameter has been appended to the URL by the search form. If a search parameter is present, it queries for users matching the entered term. If one or more users are found, the component displays a list of users, showing their first and last names, primary email address, and current role. Each user has Make Admin and Make Moderator buttons, which include hidden inputs for the user ID and role. These buttons use the setRole() server action to update the user's role.import { redirect } from 'next/navigation'
import { checkRole } from '@/utils/roles'
import { SearchUsers } from './SearchUsers'
import { clerkClient } from '@clerk/nextjs/server'
import { removeRole, setRole } from './_actions'
export default async function AdminDashboard(params: {
searchParams: Promise<{ search?: string }>
}) {
if (!checkRole('admin')) {
redirect('/')
}
const query = (await params.searchParams).search
const client = await clerkClient()
const users = query ? (await client.users.getUserList({ query })).data : []
return (
<>
<p>This is the protected admin dashboard restricted to users with the `admin` role.</p>
<SearchUsers />
{users.map((user) => {
return (
<div key={user.id}>
<div>
{user.firstName} {user.lastName}
</div>
<div>
{
user.emailAddresses.find((email) => email.id === user.primaryEmailAddressId)
?.emailAddress
}
</div>
<div>{user.publicMetadata.role as string}</div>
<form action={setRole}>
<input type="hidden" value={user.id} name="id" />
<input type="hidden" value="admin" name="role" />
<button type="submit">Make Admin</button>
</form>
<form action={setRole}>
<input type="hidden" value={user.id} name="id" />
<input type="hidden" value="moderator" name="role" />
<button type="submit">Make Moderator</button>
</form>
<form action={removeRole}>
<input type="hidden" value={user.id} name="id" />
<button type="submit">Remove Role</button>
</form>
</div>
)
})}
</>
)
}
The foundation of a custom RBAC (Role-Based Access Control) system is now set up. Roles are attached directly to the user's session, allowing your application to access them without the need for additional network requests. The checkRole() helper function simplifies role checks and reduces code complexity. The final component is the admin dashboard, which enables admins to efficiently search for users and manage roles.
</Steps>
ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā