Since it became possible to create full-stack applications with React, the question of authorization has been a recurring topic. In this extensive guide, I want to explore how authorization can be implemented in Next.js when using React Server Components and Server Actions in Nextâs App Router.
While this guide follows the event of the recent security incident in Next.js, it is not a direct response to it. I have followed the Next.js teamâs recommendations for implementing authorization close to the data source in the past, so this guide outlines how I have approached authorization in my Next projects.
The most important place to enforce authorization in a full-stack application acts as a safeguard for both read and write operations before users can access the data source. This is typically done in the API layer, which is responsible for handling requests and responses between the client and the server.
In a small sized full-stack application, you may only have React Server Components and Server Actions. Since accessing the database directly from a Server Component is not recommended, you will most likely have a data fetching function in between your Server Component and the database. This is the perfect spot to implement authorization.
You can substitute throwing the error with a human readable feedback mechanism.
The same authorization check should happen in a Server Action for write operations. For example, if you have a Server Action that is responsible for creating a post, check if the user is authorized to create a post before inserting it into the database.
The getAuth function performs validation of the session cookie against the database. It worth to note that we are using Reactâs Cache API here, to memoize the result of the getAuth function during one render cycle:
I also use an authorization function on top of getAuth called getAuthOrRedirect . This function will redirect the user to a login page if they are not authenticated instead of throwing an error, because the user is not supposed to access the resource:
This function can be used as replacement for the previous getAuth function usages.
Once your application grows in size and complexity, you will introduce layers between your API and the database. This is where the Service Layer and the Data Access Layer come into play.
In the API Layer, authorization acts as an initial gatekeeper, quickly verifying user authentication and rejecting unauthorized requests before deeper processing.
The Service Layer contains the core authorization logic, applying business-specific rules along with permission-based, owner-based, and role-based access control to ensure that only authorized users can perform read and write operations.
The Data Access Layer acts as a final line of defense for authorization, though its primary focus should be on data manipulation rather than permission checks, which are best handled in the service layer. However, in data-sensitive applications, you may choose to implement an additional layer of security at this stage.
Getting the authorization in front of your Data Access Layer right is the most important part of your application. If you have a solid authorization layer in place, you can be sure that your application is secure and that your data is safe.
From here you will make further authorization improvements to your application, like adding authorization to your routing, UI, and middleware.
Every entry point component in a Next.js application is a Server Component by default. Therefore, you can apply authorization checks in your page components who have access to the database to prevent unauthorized users from accessing certain routes.
If you donât have this defense mechanism in place, unauthorized users could navigate to the page (bad), but should still not be able to read or write data (good) due to the authorization checks in your API, Service, and Data Access Layers.
However, having it in place makes your application more secure and adds an improved user experience, so it is still strongly recommended to have it.
Keeping up with adding authorization to your routing can be a tedious and error-prone task. To make it easier, you can move the authorization into a Layout component that is shared across sibling and descending pages in your application.
The Layout component would be placed in a route group (here: (authenticated) ) folder that contains all the pages that require the first line of defense for authorization.
However: Consolidating route-based authorization from multiple Page components into a single Layout component prioritizes developer convenience over security.
Security Risk: Itâs is proven that Page components as Server Components can be fetched independently from the Layout component when a malicious user wants to access a protected page directly. Therefore, the Layout component is not a reliable place to enforce authorization checks.
But: There are reasons why you might still choose to use a Layout component for authorization. Ultimately, the decision depends on you and your team:
Critical Caveat: Layout authorization is a convenience layer, NOT a security solution. The true, comprehensive authorization must occur in API, Service, and/or Data Access Layers.
Strategy 1: Use Layout checks as a DX improvement, but implement robust, independent authorization checks in backend layers to ensure profound security.
Strategy 2: Implement authorization checks in Page components to ensure security, even if it means sacrificing developer experience and convenience.
Strategy 3: Combine both strategies to achieve a balance between security and developer experience (which may be redundant though).
In a growing application, when using Layout components as first line of defense for authorization, you will eventually implement more Layout components for authorization when roles and permissions come into play.
For example, you might have an AdminLayout component that checks if the user is an admin before rendering the children:
And then you would have a folder structure like this:
In conclusion, authorization in routing is a crucial part of your application. It is the first line of defense for the visual parts of application and should be implemented in a way that is secure and maintainable.
Authorization in the UI is mostly there to provide a better user experience. For example, you might want to hide or disable UI elements based on the userâs authorization status:
However, as said, this serves as a better user experience and not as a security measure, because malicious users can still manipulate the client-side code. Therefore, the authorization checks in the API, Service, and Data Access Layers are the most important part of your application once again.
There is also the middleware which many are using as their authorization layer. In Next.js, you can use middleware to protect any page.tsx or route.ts files in the App Router from unauthorized access:
However, the big caveat here is that the middleware is executed on every request, which can be a performance bottleneck. Therefore, it is recommended to not use the middleware for authorization checks that would hit a (slow) database.
Instead you can perform pre-flight checks in the middleware, like checking if the user has an active session cookie without checking it against the session in the database. This is a good practice for performance reasons.
You could also refresh the cookieâs expiration time in the middleware, which is a common practice to keep the user logged in for a longer period of time.
But at the end of the day, if you donât want to get into the performance pitfalls of middleware, the middleware is not the place to perform database intensive authorization checks. Instead, you should use the API, Service, and Data Access Layers as the critical line of defense.
Other caveats that make working with middleware more complicated:
If you take one thing away from this tutorial, it should be that authorization should be as close as possible to your sensitive data. This means that you should have authorization checks in your API, Service, and Data Access Layers. This is the most important part of your application and should be implemented in a way that is secure and maintainable.