Implementing Role-Based Access Control in Next.js App Router using Clerk Organizations
eugene musebe
Posted on July 31, 2023
Introduction
It is crucial to protect user data and control access to various application features in the dynamic world of web development. RBAC, or role-based access control, has become a well-liked and successful method of controlling permissions within web applications. RBAC enables developers to design a customised and secure user experience by assigning specific roles to users and defining their access rights in accordance with those roles. In this article, we'll examine how RBAC is implemented in the Next.js App Router using Clerk, a cutting-edge platform for user management and authentication.
We will look at how Clerk uses Organizations to simplify user authentication and role assignment, thereby streamlining RBAC integration into Next.js applications. Following this comprehensive guide will equip developers to build web applications that prioritise security and provide a tailored experience to each user based on their assigned role.
Prerequisites
Make sure you have the following prerequisites in place before implementing Role-Based Access Control in the app using Next.js and Clerk:
- Fundamentals of web development (HTML, CSS, and JavaScript).
- Basic understanding of React/Next.js.
- Node.js and npm must be installed.
- A code editor (Visual Studio Code).
- Git for version control - optional.
- An account with Clerk.com for authentication.
With these prerequisites in place, you can use Clerk to implement Role-Based Access Control in your Next.js application. If you haven't met any of these requirements, take the time to do so before proceeding with the tutorial.
Github Repository and Live Demo
You can access the source code of the application on GitHub at https://github.com/musebe/nextjs-clerk-organizations-rbac-authentication.
To see the completed application in action, visit https://nextjs-clerk-organizations-rbac-authentication.vercel.app.
Setting up Clerk and Clerk Organizations
The first step in using Clerk is to create a Clerk account. You can do this by going to clerk.com and creating a free account. You will be prompted to create a new application within your account after signing in. This application will serve as your starting point for exploring and utilising Clerk's various features and capabilities, which are highlighted below.
After signing in, click the 'Add application' button. Give your app a name and select all of the 'Social authentication' methods you want your users to use.
Creating Clerk Organizations
Now, it's time to create an organization. In Clerk, organizations serve as shared accounts, enabling collaboration among members across shared resources. Within an organization, there are members with elevated privileges, responsible for managing access to the organization's data and resources. These privileged members have the authority to grant or revoke permissions, ensuring seamless cooperation and secure control over the organization's assets.
To create an organization in Clerk after logging in and being on the home dashboard, follow these steps:
Click on the "Create Organization" button, and a new form or modal will appear, prompting you to provide the necessary information for the organization.
Fill in the required details for the organization, such as the organization name, logo, and any other relevant information.
Click on the
Create
button to finalize the creation of the organization.Clerk will then create the organization, and you will be redirected to the newly created organization's dashboard.
Adding members to an Organization
To add members to the Clerk organization, follow these steps:
Navigate to the
Members
section: After selecting the organization in Clerk's dashboard, navigate to the "Members" section, this is where user management is handled.Click on
Add Members
: Look for the "Add Members" button within the "Members" section and click on it to initiate the process.A pop-up form will appear: Once you click on "Add Members," a pop-up window will show up on your screen, providing a form to input the necessary details.
Search for the user: In the pop-up form, you can search for the specific user you want to add to the organization. Enter their email address or username in the search field.
Choose a role: For each user you add, you can assign them a role within the organization. Clerk typically offers two roles:
admin
andbasic_member
. Select the appropriate role for the user based on their level of access and permissions.
- admin - The
admin
role grants complete access to all organisational resources. Members with the admin role have administrator privileges and can fully manage organisations and memberships in organisations.- basic_member - The
basic_member
role is the default role for an organization's users. The availability of organisational resources is restricted. Basic members cannot manage organisations or memberships in organisations, but they can view information about the organisation and its members. - Click
Add
: Once you have verified the details, click the "Add" or "Confirm" button to finalize the addition of the user to the organization with the specified role.
- basic_member - The
Congratulations! You have successfully created an organization in Clerk. From the organization dashboard, you can manage users, roles, and other resources associated with the organization.
Next.js Setup
Now to the fun part! Let's spin up a new Next.js project to get started with implementing Role-Based Access Control (RBAC). To do this, simply navigate to the directory of your choice and run the following command in your terminal or command prompt:
npx create-next-app my-rbac-app
Replace my-rbac-app
with your desired project name. This command will set up a new Next.js project for you with all the necessary configurations to begin building your RBAC-enabled web application.
The best part is that starting from Next.js version 13, Tailwind CSS comes pre-configured by default, saving you the trouble of installing it separately. With Next.js and Tailwind CSS ready to go, you can now focus on integrating RBAC using Clerk into your freshly created Next.js project. So, let's get started and secure your web application with personalized user access and permissions!
Integrating Clerk with Next.js
The next step after creating the Next.js project and having Tailwind CSS set up is to integrate Clerk into your application for handling user authentication and role-based access control. Follow these steps to proceed:
- Install Clerk SDK: Use npm or yarn to install the Clerk SDK in your Next.js project. The Clerk SDK provides the necessary tools to authenticate users and manage roles. Run the following command in your project's root directory:
npm install @clerk/nextjs
- Initialize Clerk in your Next.js App: Import the Clerk SDK into your Next.js application and initialize it with your Clerk API credentials. This step enables your application to interact with Clerk's authentication services. In Next 13 using the app router, the initialization is done in the
layout.js
file as highlighted below :
// app/layout.js
import '@styles/globals.css';
import { ClerkProvider } from '@clerk/nextjs';
export const metadata = {
title: 'Clerk-Organizations',
description: 'Clerk Role-Based Authentication Using Organizations',
};
export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html lang='en'>
<body>
<div className='main'>
<div className='gradient' />
</div>
<main className='app'>
{children}
</main>
</body>
</html>
</ClerkProvider>
);
}
- Obtain Clerk API Credentials: To initialize the Clerk SDK, you'll need API credentials specific to your Clerk organization. Retrieve these credentials from the Clerk dashboard on the left sidebar under the
API Keys
tab. Copy the keys and paste them in the.env
file we are going to create .
- In your Next.js project's root directory, generate a new file named
.env.local
. Once created, paste theCLERK_PUBLISHABLE_KEY
andCLERK_SECRET_KEY
environment variables that you previously copied from the Clerk dashboard into this.env.local
file. Storing these keys in the.env.local
file ensures that your sensitive credentials are securely accessible by your Next.js application during runtime, safeguarding them from being exposed in your codebase. Remember to refrain from sharing or committing the.env.local
file to version control to maintain the confidentiality of your environment variables.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="<Your Publishable Key>"
CLERK_SECRET_KEY="<Your Clerk Secret Key>"
Having completed the setup and configurations as described above, it is now time to begin establishing the application layout in the project.
Application Layout
The next step is to create the application layout and pages. We will organize our pages using the folder-based approach provided by Next.js 13. Inside the app
directory, we shall create three separate folders named Profile
, Users
, and Admin
, each containing a page.jsx
file. These files will serve as the components for our respective pages. In the Profile
page, we will provide users with personalized information and options to manage their accounts. The Users
page will display general user-related data and interactions. Lastly, the Admin
page will serve as a dashboard for administrators, granting them access to oversee and control various aspects of the application. This organized structure will enhance code maintainability and make it easier to extend our Next.js app as it grows in functionality.
Inside the app
directory, create three separate folders for each page: Profile
, Users
, and Admin
. Within each folder, create a page.jsx
file with the content for each page.
Here's how you directory structure should look like :
- your-nextjs-app
- app
- profile
- page.jsx
- users
- page.jsx
- admin
- page.jsx
Now, let's update the content of each page.jsx
file accordingly:
-
Profile/page.jsx
:
const Profile = () => {
return (
<div>
<h1>Welcome to the Profile Page!</h1>
{/* Add any content or components specific to the Profile page here */}
</div>
);
};
export default Profile;
-
Users/page.jsx
:
const Users = () => {
return (
<div>
<h1>Welcome to the Users Page!</h1>
{/* Add any content or components specific to the Users page here */}
</div>
);
};
export default Users;
-
Admin/page.jsx
:
const Admin = () => {
return (
<div>
<h1>Welcome to the Admin Page!</h1>
{/* Add any content or components specific to the Admin page here */}
</div>
);
};
export default Admin;
Now, the pages will be accessible using the following URLs:
- Profile:
http://localhost:3000/profile
- Users:
http://localhost:3000/users
- Admin:
http://localhost:3000/admin
This organized approach will ensure a clear separation of concerns and make it easier to manage and scale your Next.js app.
Navbar
Onto the Navbar! Let's begin by creating a components
folder at the root of your Next.js app. Inside this new folder, we will place a file named Navbar.jsx
to contain our Navbar component. This step will help us organize and manage reusable components more efficiently.
Here's what the folder structure will look like after completing this step:
- your-nextjs-app
- components
- Navbar.jsx
- app
- Profile
- page.jsx
- Users
- page.jsx
- Admin
- page.jsx
- pages
... other pages if any ...
Once the components
folder is created, paste the content below into the new Navbar.jsx
file located inside the components
folder.
import Link from 'next/link';
const Navbar = () => {
const links = [
{ title: 'Profile', url: '/profile' },
{ title: 'Dashboard', url: '/user' },
{ title: 'Admin Dashboard', url: '/admin', role: 'admin' },
// Add more placeholder links as needed
];
return (
<header className='text-gray-600 body-font bg-white shadow'>
<div className='container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center justify-between'>
<div className='flex items-center'>
<a
href='/'
className='flex title-font font-medium items-center text-gray-900'
>
<svg
xmlns='http://www.w3.org/2000/svg'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
className='w-10 h-10 text-white p-2 bg-indigo-500 rounded-full'
viewBox='0 0 24 24'
>
<path d='M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5'></path>
</svg>
<span className='ml-3 text-xl'>SecureClerk</span>
</a>
</div>
<nav className='md:ml-auto md:mr-auto flex flex-wrap items-center text-base justify-center'>
<Link href='/profile'>
<div className='mr-5 cursor-pointer hover:text-gray-900'>
Profile
</div>
</Link>
<Link href='/user'>
<div className='mr-5 cursor-pointer hover:text-gray-900'>
Dashboard
</div>
</Link>
<Link href='/admin'>
<div className='mr-5 cursor-pointer hover:text-gray-900'>
Admin Dashboard
</div>
</Link>
{/* Add more links directly here as needed */}
</nav>
<div className='md:flex items-center'>
<a href='/sign-in'>
<button className='text-white bg-indigo-500 border-0 py-2 px-4 focus:outline-none hover:bg-indigo-600 rounded text-base mr-4'>
Login
</button>
</a>
<a href='/sign-up'>
<button className='text-white bg-indigo-500 border-0 py-2 px-4 focus:outline-none hover:bg-indigo-600 rounded text-base'>
Sign Up
</button>
</a>
</div>
</div>
</header>
);
};
export default Navbar;
The next step is to modify the Layout.js
file to include the Navbar
component so that it can be used by the entire app. We'll import the Navbar
component into the Layout.js
file and render it within the layout structure. This way, the Navbar
will be present across all pages wrapped within the RootLayout
.
Here's the updated Layout.js
file:
// app/Layout.js
import '@styles/globals.css';
import { ClerkProvider } from '@clerk/nextjs';
import Navbar from '../components/Navbar'; // Import the Navbar component
export const metadata = {
title: 'Clerk-Organizations',
description: 'Clerk Role-Based Authentication Using Organizations',
};
export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html lang='en'>
<body>
<div className='main'>
<div className='gradient' />
</div>
<main className='app'>
<Navbar /> {/* Include the Navbar component here */}
{children}
</main>
</body>
</html>
</ClerkProvider>
);
}
With this in place, the Navbar component provides a user-friendly navigation interface for our Next.js app. It includes links for "Profile," "Dashboard," and "Admin Dashboard," enabling seamless client-side navigation to the corresponding pages when clicked.
Sign in and sign up Components
Clerk offers a selection of pre-built components that seamlessly integrate sign-in, sign-up, and other user management features into your Next.js application. To leverage these functionalities, you can utilize the <SignIn />
and <SignUp />
components along with Next.js' optional catch-all route.
To implement this, within the "app" folder of your project, create two new directories named "sign-up" and "sign-in." Inside each directory, place the corresponding code for the sign-up and sign-in functionalities following this structure: sign-up/[[...sign-up]]/page.jsx
.
Below, find the code for the Sign In and Sign Up components using the Clerk components provided by @clerk/nextjs
.
Sign In Component:
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';
const SignInPage = () => {
return (
<div>
<h1>Sign In</h1>
<SignIn />
</div>
);
};
export default SignInPage;
Sign Up Component:
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs';
const SignUpPage = () => {
return (
<div>
<h1>Sign Up</h1>
<SignUp />
</div>
);
};
export default SignUpPage;
After adding the two pages, the updated project directory structure will look like this:
- your-nextjs-app
- components
- Navbar.jsx
- app
- Profile
- page.jsx
- Users
- page.jsx
- Admin
- page.jsx
- sign-up
- [[...sign-up]]
- page.tsx
- sign-in
- [[...sign-in]]
- page.tsx
- pages
... other pages if any ...
With the setup completed, when you visit the route http://localhost:3000/sign-in
, you will be able to load Clerk's login page, as demonstrated below:
Protecting your application routes
It's time to decide which pages are public and which require authentication now that Clerk has been installed and mounted in your application. We achieve this by declaring the public and private routes in a file called middleware.js
at the project's root. In our situation, we only want the home page to be viewable by the general public and the remaining portions of the page to be hidden until the user logs into the application. The code below makes it possible to achieve this:
import { authMiddleware } from '@clerk/nextjs';
export default authMiddleware({
publicRoutes: ["/"]
});
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
Hero Component
To finalize the layout, let's beautify the homepage by adding some content to it. We'll start by creating a Hero.jsx
page in the components
folder and then embed it into the page.jsx
file located inside the app
directory.
This component will serve as the hero section of the homepage.
// components/Hero.jsx
const Hero = () => {
return (
<div className="hero">
{/* Your hero content and layout here */}
<h1>Welcome to Our Website</h1>
<p>Discover amazing things with us!</p>
</div>
);
};
export default Hero;
Next, go to the page.jsx
file inside the app
directory and update it to include the Hero
component:
// app/page.jsx
import { Hero } from "@components";
export default function Home() {
return (
<main className='overflow-hidden'>
<Hero />
{/* Other content and components for the Home page can be added here */}
</main>
);
}
Now, with the Hero
component embedded in the Home
page, you have a beautiful hero section displayed at the top of the homepage. You can further enhance the layout by adding additional content and components to the Home
page as needed. This will create an engaging and visually appealing homepage for your Next.js app.
Your Final project structure should look like this upto this point :
- your-nextjs-app
- components
- Hero.jsx
- Navbar.jsx
- app
- profile
- page.jsx
- users
- page.jsx
- admin
- page.jsx
- sign-up
- [[...sign-up]]
- page.tsx
- sign-in
- [[...sign-in]]
- page.jsx
- layout.jsx
- page.jsx
- styles
- globals.css
- node_modules
- public
- favicon.ico
... other public files ...
- package.json
- next.config.js
Navbar links Conditional Rendering
For conditional link rendering, we will utilize Clerk's SignedOut
, UserButton
, and SignedIn
components. When a user is not signed in, we will display only the "Login" and "Sign Up" buttons. However, once the user is logged in, we will show the navigation links alongside Clerk's UserButton
component, which facilitates the sign-out functionality. This approach enables us to dynamically adjust the content displayed in the Navbar based on the user's authentication status.
To achieve this functionality, on the Navbar component, import the following components from Clerk's Next.js library:
import { SignedOut, UserButton, SignedIn } from '@clerk/nextjs';
These components will allow you to conditionally render content based on the user's authentication status, enabling you to show different elements for signed-in and signed-out users in the Navbar.
After logging in, it becomes necessary to verify the user's role, distinguishing between "admin" and "basic_member," and accordingly display the "Profile" and "Dashboard" links only for "basic_member," while showing all links for "admin." We can obtain this information using Clerk's useSession
hook, which allows access to the session status. To achieve this, simply include the useSession
hook within the Clerk object you previously imported. By doing so, you will be able to fetch the user's role from the session data and conditionally render the appropriate links in the Navbar.
import { SignedOut, UserButton, SignedIn, useSession } from '@clerk/nextjs';
To simplify the role-checking process, let's create a utility function that will iterate through the session data and return the user's role. To do this, create a new folder named utils
at the root of the project and add a file called userUtils.js
inside it. In userUtils.js
, implement the following function:
function checkUserRole(session) {
if (
!session ||
!session.user ||
!session.user.organizationMemberships ||
session.user.organizationMemberships.length === 0
) {
return null; // Return null if the user is not a basic member
}
const organizationMemberships = session.user.organizationMemberships;
// Loop through all organization memberships
for (const membership of organizationMemberships) {
if (membership.role) {
return membership.role.toLowerCase(); // Return the role in lowercase if it exists
}
}
return null; // Return null if no role is found in the memberships
}
export { checkUserRole };
This function will allow us to extract and determine the user's role from the session data, making it easier to handle role-based actions in the application.
The next step is to export the checkUserRole
function from the userUtils.js
file so that it can be used in the Navbar
component. To achieve this, use the following import statement in the Navbar
component:
import { checkUserRole } from '../utils/userUtils';
Inside the Navbar
component, we can determine the user's role from the session data as follows:
const { session } = useSession();
const userRole = checkUserRole(session);
By utilizing the useSession
hook from Clerk, we retrieve the user's session data. Then, we call the checkUserRole
function with the session
parameter, which enables us to obtain the user's role. This way, we can efficiently check the user's role within the Navbar
component and conditionally render the appropriate links based on the role information.
After the user has signed in, we can utilize the userRole
information to determine which links should be displayed for administrators and basic members.
<SignedIn>
{links.map((link) =>
(link.role === 'admin' && userRole === 'admin') || !link.role ? (
<Link key={link.title} href={link.url}>
{/* Use a div instead of an anchor tag */}
<div className='mr-5 cursor-pointer hover:text-gray-900'>
{link.title}
</div>
</Link>
) : null
)}
</SignedIn>
When the user is signed in, the SignedIn
component allows us to conditionally render the navigation links based on the user's role. The links.map
function iterates through the links
array, and for each link, it checks if the link is intended for administrators (link.role === 'admin'
) and if the user actually has an "admin" role (userRole === 'admin'
). If both conditions are met or if the link does not have a specific role assigned (!link.role
), the link is displayed using the Link
component from Next.js, enclosed within a div
element for proper styling. This way, we can selectively show the appropriate links based on the user's role after they have signed in.
Here's how you should arrange the SignedOut
and UserButton
components:
{/* SignedOut Component */}
<SignedOut>
<a href='/sign-in'>
<button className='text-white bg-indigo-500 border-0 py-2 px-4 focus:outline-none hover:bg-indigo-600 rounded text-base mr-4'>
Login
</button>
</a>
<a href='/sign-up'>
<button className='text-white bg-indigo-500 border-0 py-2 px-4 focus:outline-none hover:bg-indigo-600 rounded text-base'>
Sign Up
</button>
</a>
</SignedOut>
{/* UserButton Component */}
<SignedIn>
<div className='ml-4'>
<UserButton afterSignOutUrl='/' />
</div>
</SignedIn>
Having completed all the necessary steps, we are now able to conditionally render the application, displaying different links based on the various scenarios we have highlighted. This allows us to customize the Navbar content depending on the user's authentication status, role, and whether they are signed in or signed out. With this implementation, the application provides a seamless and user-specific navigation experience.
Ensuring Role-Based Access Control for the Admin Dashboard Page
Currently, we have successfully hidden the Admin Dashboard
link from basic members. However, there is still an issue to address - any user can access the /admin
route, even if they are not authorized to do so. This behavior is undesirable and needs to be rectified. Let's proceed to handle this situation to ensure that only users with the admin
role can access the Admin Dashboard
page.
To implement this functionality, we will utilize the useOrganizationList
hook on the admin page. By using the useOrganizationList
hook, we gain access to the list of available Organizations and the OrganizationMemberships to which the user is affiliated. This information will enable us to enforce role-based access control and ensure that only authorized users can access the Admin Dashboard page.
We must first import the useOrganizationList
hook into the admin page before we can use it. Here's how to go about it:
import { useOrganizationList } from '@clerk/nextjs';
After importing the useOrganizationList
hook, you can utilize it to access the organization data. By invoking the hook, you will receive three variables: organizationList
, isLoaded
, and setActive
.
const { organizationList, isLoaded, setActive } = useOrganizationList();
organizationList
will hold the list of available organizations, providing important information about each organization and its members. isLoaded
is a boolean value that indicates whether the organization data has been successfully loaded and is ready for use. Lastly, setActive
is a function that allows you to set the active organization.
The subsequent step involves waiting for the organization data to load and verifying whether the user's role is not admin. We achieve this using the useEffect
hook:
useEffect(() => {
if (isLoaded) {
// Find the admin organization from the loaded organization list
const adminOrganization = organizationList.find(
(org) => org.membership.role === 'admin'
);
// If the user is not an admin, redirect to the homepage
if (!adminOrganization || adminOrganization.membership.role !== 'admin') {
router.push('/'); // Replace '/' with the homepage URL
} else {
// If the user is an admin, no need to wait for the organization list; render the admin page directly
setShowLoader(false);
}
}
}, [isLoaded, organizationList]);
In this useEffect
block, we use the isLoaded
variable to ensure that we have successfully loaded the organization data. Once the data is available, we look for an organization with the role of 'admin' within the organization list. If the user does not have an admin role or there is no admin organization found, we redirect them to the homepage using the router.push
method. However, if the user is indeed an admin, we proceed to render the admin page directly, setting setShowLoader
to false
to indicate that the loading process is complete. This way, we enforce the role-based access control and appropriately handle navigation based on the user's role.
Conclusion
With these steps, we have successfully implemented Role-Based Access Control in the Next.js App Router using Clerk Organizations. It demonstrates how seamlessly Clerk's features can be integrated into the application, making the process straightforward and efficient. This approach allows us to control user access based on their roles, providing a secure and tailored user experience. Indeed, the integration with Clerk Organizations simplifies the implementation of role-based access, making it a smooth and straightforward process.
If you prefer not to create organizations through the Clerk dashboard, the Clerk team offers components that you can integrate directly into your codebase to handle organization management. For more details, you can explore the documentation at: https://clerk.com/docs/organizations/overview. These components provide a convenient way to manage organizations and memberships within your application, giving you flexibility and control over the organization setup process.
Refference
For further guidance and information, refer to the following:
Next.js Documentation: Access the official Next.js documentation at https://nextjs.org/docs for in-depth details about the framework.
Clerk Documentation: Explore Clerk's authentication capabilities and integration guides at https://clerk.com/docs to understand its usage better.
Simplifying Authentication in Next.js Applications with Clerk: Find detailed steps for setting up Clerk authentication in Next.js apps in the blog post: Dev.to Link.
Posted on July 31, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.