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:
Node.js v18 installed on your local machine
Git CLI installed on your local machine
A free Descope account
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
:
Navigate to the project setup. Select Consumers
under Who uses your application?
and then click Next
:
Select Magic Link
for Which authentication methods do you want to use?
and then click Next
:
Skip the MFA method step and click Go ahead without MFA
. You can always set this up later:
On the next page, you can view the flows generated for your project. Click Next
to generate these 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:
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:
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:
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:
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:
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 theapp/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:
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:
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:
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:
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:
On the user details modal, select + Add Tenant / Role
, assign the editor
role, and click Save
:
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:
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:
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:
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!