Implement basic Role Based Access Control (RBAC) with metadata
To control which users can access certain parts of your application, you can leverage Clerk's roles feature. Although Clerk offers a roles feature as part of the feature set for organizations, not every app implements organizations. This guide will cover a workaround for setting 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 are using Next.js App Router. The concepts here can be adapted to Next.js Pages Router and Remix.
Configure the session token
Clerk provides user metadata, which is a tool that can be leveraged to build flexible custom logic into your application. Metadata can be used to store information, and in this case, it can be used to store a user's role.
unsafeMetadata
can be read and updated in the browser, and because the user could modify this metadata it should be treated as unsafe and validated by your application before trusting it. privateMetadata
can not be read or modified in the browser. publicMetadata
can be read by the browser and can only be updated server-side or in the Clerk Dashboard, making it the safest and best choice for this use case.
To build a basic RBAC system, first, you need to make publicMetadata
available to the application directly from the session token. With publicMetadata
attached directly to the user's session, a fetch and network request isn't required every time you need to access the data.
In the Clerk Dashboard, navigate to Sessions. In the Customize session token section, select Edit. In the modal that opens, enter the following JSON. If you have already customized your session token, you may need to merge this with what you currently have.
Provide a global TypeScript definition
In your application's root folder, add a types
directory. Inside of the types
directory, add a globals.d.ts
file. This file will provide auto-complete, prevent TypeScript errors when working with the role, and control the roles that are allowed in the application. For this guide, only an admin
and moderator
role will be defined.
Set the admin role for your user
Later, you will add a basic admin tool to change the user's role, but for now, let's manually add a role to your own user account. In the Clerk Dashboard, navigate to Users and select your own user account. Scroll down to the Metadata section and next to the Public option, select Edit. Add the following JSON and select Save.
Create an admin dashboard and protect it
Now that your user has the admin
role, let's build an admin dashboard to help you easily change users' roles. In your app/
directory, create an admin/
folder. Within the admin/
folder, create a dashboard/
folder. In the dashboard/
folder, create a file named page.tsx
. Copy the following code and paste it into the file.
You want the dashboard to only be available to users with the admin
role. As you configured earlier, a user's role can be found in the metadata stored in the session token. You can access the session token's claims using Clerk's auth()
hook.
The /admin/dashboard
route now requires the user to sign into the application. It also requires that the user have a publicMetadata
of {"role": "admin" }
.
Create a reusable function to check roles
Let's create a helper function to make checking roles easier. The first step is modifying globals.d.ts
. Create a type
for Roles
so that the union type for the roles can be used in other places in the application. Then, modify the interface you previously added to use the new Roles
type.
The next step is creating the helper function. Create the utils/
directory and inside, add the file roles.ts
. In the roles.ts
file, add the following code.
This checkRole()
helper will accept a role using the Roles
type and will return true
if the user has that role, or false
if the user does not.
Next, the admin dashboard can be refactored to use the checkRole()
helper. Navigate back to the admin dashboard file. In the if()
statement, remove the check that was used previously and replace it with the new checkRole()
helper with "admin"
as the argument.
Add admin tools to find users and add roles
You can leverage the checkRole()
function you added along with server actions to build basic tools for finding users and managing roles.
Start with the server action. The setRole()
action below will check the role of the current user using the checkRoles()
helper to confirm that the user is an admin
. It will then set the role for the selected user to the specified role.
With the server action in place, you can build the <SearchUsers />
component. This will have a form that can be used to search for users. On form submission, the search term is added to the URL as a search parameter. The page component, which is a server component that you will refactor next, will perform a query based on this change.
With the server action and the search form in place, you'll refactor the server component for the app/admin/dashboard
route. It will now check if a search parameter has been added to the URL by the search form, and if there is a search parameter present, it will search for users that match the entered term. If an array of one or more users is returned, then the component will render a list of users using their first and last name, primary email address, current role, and 'Make Admin' and 'Make Moderator' buttons. The buttons include hidden inputs for the user ID and the role, and they use the setRole()
server action to update the role for the user.
Finished 🎉
The building blocks needed for a custom RBAC system are in place. Roles are attached directly to the user's session, providing them to your application without needing a separate fetch and network request. The helper function is in place to check the user's role, reducing the code and simplifying the process. The final piece is an admin dashboard that allows admin's to find users and set their roles.
Feedback
Last updated on