This tutorial was written by Kevin Kimani, a developer and technical writer. Connect with him on his website or X to see more of his work!


Making sure your application is secure through robust authentication and authorization mechanisms is crucial in the app development process. This helps to ensure that only authenticated users can access the protected resources of your application and that each user can access only the resources they are allowed to access.

In this guide, you will learn how to leverage Descope to integrate authentication and role-based access control (RBAC) into a Next.js application with the new App Router. Whether you’re building a new application or enhancing an existing one, this guide will equip you with the skills to create secure login and access controls.

Authorization and authentication

Both authentication and authorization are integral to developing a secure application, but they serve distinct purposes. Authentication is the process of verifying the user’s identity and is typically done through a set of login credentials, such as email and password, magic links, passkeys, and more. In contrast, authorization determines if the authenticated user is allowed to access specific resources or perform certain actions.

Next.js supports both client-side rendering (CSR) and server-side rendering (SSR), and each of these approaches has its own implications for authentication and authorization. In CSR, the client handles rendering after the initial page load, which can lead to a momentary flash of unauthorized content before the auth state is verified. This issue can be easily mitigated with loading indicators or skeleton screens. In SSR, the server handles rendering before sending the content to the client. This approach prevents unauthorized content flashes since the server blocks the request until it verifies the auth state.

Also read: Next.js vs React.js vs SvelteKit

Descope simplifies the process of adding authentication and authorization to your Next.js application by offering a highly intuitive SDK and the use of flows to build authentication screens. Descope Flows is a visual no-code interface to build screens and authentication flows for common user interactions with your application, such as login, sign-up, user invites, and multi-factor authentication (MFA). This feature abstracts away the implementation details of authentication methods, session management, and error handling, allowing you to focus on building the core features of your application rather than handling these complexities.

Additionally, using Descope eliminates the need to write authentication and authorization logic from scratch, which helps to save valuable development time and reduces the risk of potential security vulnerabilities. The robust infrastructure of Descope helps to ensure that your application’s authentication and authorization mechanisms are secure, scalable, and easy to maintain.

Implementing authn and authz with Descope

The following sections explain how you can implement these features in a Next.js application using Descope. To follow along, you need the following:

To demonstrate how to implement RBAC, this guide uses a simple blogging web application that allows users to create blog posts and then publish them. With the Descope RBAC, you create two roles: editor and admin. Editors can write blog posts and then submit them for approval. Admins can view the submitted posts and then publish them.

Creating a Descope project

On the Descope Console, create a new project with the name descope-nextjs-auth-rbac:

Fig: Creating a new project
Fig: Creating a new project

Navigate to the project setup. Select Consumers under Who uses your application? and then click Next:

Fig: Selecting the target audience.png
Fig: Selecting the target audience.png

Select Magic Link for Which authentication methods do you want to use? and then click Next:

Fig: Selecting the authentication method
Fig: Selecting the authentication method

Skip the MFA method step and click Go ahead without MFA. You can always set this up later:

Fig: Skipping the MFA method step
Fig: Skipping the MFA method step

On the next page, you can view the flows generated for your project. Click Next to generate these flows:

Fig: The generated flows
Fig: The generated flows

Once the flows are generated, select Project from the sidebar and take note of your project ID, which you’ll use in the next step:

Fig: Obtaining the project ID
Fig: Obtaining the project ID

Next, you need to obtain a management key. Select Company from the sidebar, select the Management Keys tab on the Company page, and click the + Management Key button to create a new management key. Provide the key name and, under Project Assignment, select Use this management key for all the projects in the company. Click the Generate Key button and copy the value of your key:

Fig: Generating a management key
Fig: Generating a management key

Once you have the key, you can implement authentication for the Next.js application. You will come back to the console to set up the RBAC.

Exploring the starter template

A starter template for the blogging application has already been prepared to keep this guide’s focus on authentication and authorization. To clone it to your local machine, execute the following command in the terminal:

git clone --single-branch -b starter-template https://github.com/kimanikevin254/descope-nextjs-auth-rbac.git

Navigate into the project directory and install all the dependencies:

cd descope-nextjs-auth-rbac && npm i

Create a .env project in the root folder and add the following content, which defines the location of the SQLite database:

DATABASE_URL="file:./dev.db"

Run a Prisma migration for the models defined in the prisma/schema.prisma file:

npx prisma migrate dev --name init

Run the app with the command npm run dev and navigate to http://localhost:3000/ on your web browser. You should see a dashboard where the posts are displayed. At the moment, no posts are available:

Fig: Dashboard
Fig: Dashboard

Users are able to write posts by clicking the Start Writing button, which directs them to the Write a Post page that has a rich text editor:

Fig: Write a post
Fig: Write a post

However, don’t create any posts until you have implemented authentication.

Implementing authentication

Execute the following command in the terminal to install the Descope Next.js SDK, which you’ll use to implement authentication:

npm i @descope/nextjs-sdk

Open the app/layout.js file and replace the existing code with the following to wrap the whole application with the Descope Auth Provider:

import { Inter } from "next/font/google";
import "./globals.css";
import { AuthProvider } from "@descope/nextjs-sdk";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
   title: "Descope - Next.js Auth",
   description:
       "Demonstrating how to add auth & RBAC to Next.js 14 with Descope",
};

export default function RootLayout({ children }) {
   return (
       <AuthProvider projectId={process.env.DESCOPE_PROJECT_ID}>
           <html lang="en">
               <body
                   className={`${inter.className} max-w-screen-lg mx-auto py-4`}
               >
                   {children}
               </body>
           </html>
       </AuthProvider>
   );
}

You will set the value of DESCOPE_PROJECT_ID in the .env file later on.

You can use the sign-up-or-in flow that you generated when configuring the project to allow the user to sign in to the application. The sign-up-or-in flow presents the user with a Welcome screen where they are prompted to provide their email address. Once the user provides the email address and clicks the Continue button, a magic link is sent to the provided email address. Once the user clicks the magic link, Descope verifies the link, and the user is authenticated. The flow then checks if the user is new or returning. If the user is new, they are prompted to provide additional information(their name), and their details are updated:

Fig: Descope sign-up-or-in flow
Fig: Descope sign-up-or-in flow

Open the app/sign-in/page.js file and replace the existing code with the following:

"use client";

import { Descope } from "@descope/nextjs-sdk";
import axios from "axios";
import { useRouter } from "next/navigation";

export default function Page() {
   const router = useRouter();

   // Register user or redirect to home
   const handleEvent = async (event) => {
       try {
           if (event.detail.firstSeen !== true) {
               return router.replace("/");
           }

           // Register the user
           const { data } = await axios.post("/api/register", {
               descopeUserId: event.detail.user.userId,
               email: event.detail.user.email,
               name: event.detail.user.name,
           });

           if (data.error) {
               alert("Something went wrong");
           } else {
               return router.replace("/");
           }
       } catch (error) {
           console.log(error);
       }
   };
   return (
       <Descope
           flowId="sign-up-or-in"
           onSuccess={(e) => handleEvent(e)}
           onError={(e) => alert("Something went wrong. Please try again.")}
       />
   );
}

In the preceding code, once the user signs up or in successfully, the event data returned from this component is passed to the handleEvent function. This function checks if the user is new by examining event.details.firstSeen. If the user is not new, they are redirected to the home screen. Otherwise, it sends a POST request to the /api/register endpoint with the user’s details to register the user. If the registration is successful, the user is redirected to the home page; otherwise, an error message is displayed.

The /api/register endpoint is defined in the app/api/register/route.js files, and it adds the user to the local database.

You also need to set up a middleware to enforce authentication for all the pages in this application. Create a file named middleware.js in the project root folder and add the following code:

import { authMiddleware } from "@descope/nextjs-sdk/server";

export default authMiddleware({
   projectId: process.env.DESCOPE_PROJECT_ID,
   redirectUrl: process.env.SIGN_IN_ROUTE,
});

export const config = {
   matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};

This code uses the authMiddleware function provided by the Descope Next.js SDK to protect all routes and redirect unauthenticated users to the sign-in page.

Update the .env file with the following:

DESCOPE_PROJECT_ID=<YOUR-PROJECT-ID>
DESCOPE_MANAGEMENT_KEY=<YOUR-MANAGEMENT-KEY>


SIGN_IN_ROUTE="/sign-in"

Make sure to replace the placeholder values with the values you obtained earlier.

The authentication for your Next.js application is now complete.

Before you test it, open the app/write/page.js file. In this file, notice that the savePost function requires the Descope user ID. You can retrieve this using the hooks provided by the Next.js SDK.

Add the following import statement to the file:

import { useUser } from "@descope/nextjs-sdk/client";

Add the following statement that retrieves the user just before the savePost function:

const { user } = useUser();

You can now test the authentication flow. On your browser, navigate to http://localhost:3000. You are redirected to http://localhost:3000/sign-in since you have not signed in:

Fig: Sign-in page
Fig: Sign-in page

Provide your email address, click the link sent to your inbox, and provide your name since this is the first time you’re signing in. Then you are redirected to the home page:

Fig: Home page
Fig: Home page

This confirms that the authentication is working as expected.

Implementing authorization

Currently, anyone can log in to the application, create posts, submit them for approval, and approve the posts. For this example, you want to specify that editors can write posts and then submit them for approval, and admins can publish the posts.

Before you implement this functionality, click the Start Writing button and create a few posts to test the application with:

Fig: Home page with some posts
Fig: Home page with some posts

Go to the Descope Console, select Authorization from the sidebar, and click the + Role button. In the Add Role modal, provide “editor” as the name and “Can write posts and submit them for approval” as the description. Then click the Add button:

Fig: Adding a role
Fig: Adding a role

Repeat the process to create a role for the admin. Provide “admin” as the role and “Can toggle a post’s published status” as the description.

Assign the “editor” role to the user who is logged in to the application. In the Descope Console, select Users from the sidebar to edit the user details:

Fig: Editing user details
Fig: Editing user details

On the user details modal, select + Add Tenant / Role, assign the editor role, and click Save:

Fig: Assigning editor role
Fig: Assigning editor role

Open the app/page.js file and add the following import statement:

import { useUser } from "@descope/nextjs-sdk/client";

Add the following code before the fetchPosts function.

const { user } = useUser();

This hook allows you to retrieve the user’s details.

Locate the following lines of code in the same file:

<Link
   href={"/write"}
   className="px-4 py-1 border rounded-lg bg-black text-white"
>
   Start Writing
</Link>

Replace it with the following to display only the Start Writing button to editors:

{
   user?.roleNames?.includes("editor") && (
       <Link
           href={"/write"}
           className="px-4 py-1 border rounded-lg bg-black text-white"
       >
           Start Writing
       </Link>
   )
}

Open the app/posts/[postId]/page.js file and add the following lines of code in their respective locations (indicated by the comments):

import { useUser } from "@descope/nextjs-sdk/client"; // After the import statement

const { user } = useUser(); // Before the return statement

Locate the following lines of code:

<Button
   variant="default"
   className="px-6 mt-6"
   onClick={() => togglePublishedStatus()}
>
   {post.published ? "Unpublish" : "Publish"}
</Button>

Replace it with the following to specify that only admins can publish/unpublish a post:

{
   user?.roleNames?.includes("admin") && (
       <Button
           variant="default"
           className="px-6 mt-6"
           onClick={() => togglePublishedStatus()}
       >
           {post.published ? "Unpublish" : "Publish"}
       </Button>
   )
}

For additional security, you also validate these roles in the API router handlers. In the lib folder, create a new file named descope.js and add the following code:

import { createSdk } from "@descope/nextjs-sdk/server";

export const descopeSdk = createSdk({
   projectId: process.env.DESCOPE_PROJECT_ID,
   managementKey: process.env.DESCOPE_MANAGEMENT_KEY,
});

This code initializes a Descope client instance and exports it for use in other parts of the application.

Open the app/api/posts/create/route.js file and add the following code just after const data = await request.json():

// Make sure the user has the editor role
const userRoles = descopeSdk.getJwtRoles(data.sessionToken);

if (!userRoles?.includes("editor")) {
   throw new Error("User is not an editor");
}

This code ensures that the user has the editor role. If not, it throws an error.

Remember to add the following import statement:

import { descopeSdk } from "@/lib/descope";

Open the app/api/posts/toggleStatus/route.js file and add the following code just after const data = await request.json() to throw an error if the user making the request does not have the admin role:

// Make sure the user has the admin role
const userRoles = descopeSdk.getJwtRoles(data.sessionToken);

if (!userRoles?.includes("admin")) {
   throw new Error("User is not an admin");
}

Remember to add the following import statement:

import { descopeSdk } from "@/lib/descope";

With the route handlers verifying the user roles, you need to make sure that the session token is passed in the request body. Start by opening the app/write/page.js file and retrieving the session token using the useSession hook:

const { sessionToken } = useSession();

Make sure the useSession hook is imported into the file:

import { useSession, useUser } from "@descope/nextjs-sdk/client";

Add the retrieved session token to the body of the POST request in the savePost function:

const { data } = await axios.post("/api/posts/create", {
   title,
   content,
   descopeUserId: user?.userId,
   sessionToken,
});

Open the app/posts/[postId]/page.js file and retrieve the session token using the useSession hook:

const { sessionToken } = useSession();

Make sure the hook is imported into the file:

import { useSession, useUser } from "@descope/nextjs-sdk/client";

Replace the togglePublishedStatus function with the following code that ensures that the session token is passed to the route handlers:

const togglePublishedStatus = async () => {
   try {
       const { data } = await axios.put("/api/posts/toggleStatus", {
           postId: params.postId,
           sessionToken,
       });

       setPost(data.data);
   } catch (error) {
       alert("Something went wrong");
       console.log(error);
   }
};

Now, all the authorization checks are complete. You can test to see if everything is working as expected.

Navigate to http://localhost:3000/sign-in and log in to the application. Since you assigned the editor role to the user, you can see the Start Writing button:

Fig: Logged in as an editor
Fig: Logged in as an editor

Click on a post to open the details page. However, you cannot see the Publish/Unpublish button since only admins are allowed to see it:

Fig: Post details page as an editor
Fig: Post details page as an editor

Now, go back to the Descope Console and assign the user the admin role.

Go back to the application and refresh the page. Since the user now has the admin role, they can see the button to Publish/Unpublish a post:

Fig: Post details page as an admin
Fig: Post details page as an admin

This confirms that the authorization flow is working as expected.

You can access the full project code on GitHub.

Conclusion

In this guide, you learned the difference between authorization and authentication, as well as the reason these two concepts are important in web application security. You have also learned how to use Descope to implement authentication and authorization in your Next.js application.

Descope is a leading customer authentication and identity platform that helps developers easily design their application’s authentication processes using no-code workflows. This helps developers save a lot of time and effort that would otherwise be spent on implementing complex authentication and authorization features, allowing them to focus on implementing core features of their applications. Sign up for a free account today!