Implementing Access Control Based on Login Status Using Next.js and Auth.js
Introduction
Up to the previous article, we set up the database and ORM tool and implemented a registration process to the database from Next.js server actions.
This time, we will determine whether the session for an incoming request is logged in or not, and configure which pages can be accessed in each state. Since this article is relatively long, the actual login process will be covered in the next article.
Designing the authentication system
Although this is a very small system, we will briefly design the authentication mechanism.
You are probably already familiar with logging in using an email address and password, but the question is how far each of the logged-in and non-logged-in states can access the various pages.
The first diagram shows the basic flow of how users use the system. A user who comes to the top page registers an account on the account registration page, then performs the login process on the login page, and if login succeeds, transitions to the settings page (which plays the role of a “My Page” on a typical website; in this case, it is a page where user information is displayed).
(We will create the settings page from here on.)
If the account has already been registered, the user transitions from the top page to the login page and performs the login process.
The settings page is a page where user information is displayed, and it would be problematic if it were accessed while not logged in, so we configure it to deny access.
Also, when a logged-in user accesses the account registration page or login page, there is nothing in particular for them to do there, so we configure it to redirect them to the settings page.
With this kind of image in mind, we will configure page access for the logged-in and non-logged-in states.
You may have pages that you want to show only to logged-in users, and this article explains how to achieve that.
Goal of this article
We will not implement the login process in this article. First, we will configure access control for pages and implement a process where, if a non-logged-in user tries to navigate to a page they cannot access, they are redirected back to the login page.
(The login process will be explained in the next article.)
About Auth.js
Auth.js is an open-source authentication library. It is a library that makes it easy to add authentication features to Next.js applications, and it supports OAuth providers (Google, Facebook, Twitter, etc.), Email, Credentials, and arbitrary custom providers. For session management, cookie-based session management is provided by default, allowing you to easily manage users’ authentication state.
In this case, its role is to determine whether the user who made a request from the browser is logged in or not.
The supported frameworks are listed on this page:
As of now, it supports various frameworks such as Next.js, Astro, Express, Nuxt, Remix, and SvelteKit.
It also provides providers that can integrate with more than 80 services such as Google, GitHub, and Twitter, making it possible to quickly build authentication mechanisms using these.
About Next.js middleware
By integrating middleware into your application, you can greatly improve performance, security, and user experience.
Typical scenarios where middleware is particularly effective include:
- Authentication and authorization: Before allowing access to specific pages or API routes, verify the user’s identity and check session cookies.
- Server-side redirects: Redirect users at the server level based on specific conditions (locale, user role, etc.).
- Path rewriting: Dynamically rewrite paths to API routes or pages based on request properties to support A/B testing, feature rollouts, and legacy paths.
- Bot detection: Protect resources by detecting and blocking bot traffic.
- Logging and analytics: Capture and analyze request data before it is processed by a page or API.
In this case, after Auth.js determines whether the user accessing the site is logged in or not, middleware checks which page they are trying to access and routes them to the appropriate page.
For example, if a non-logged-in user tries to access the settings page, middleware will redirect them to the login page.
Setting up Auth.js
We will basically follow the official Auth.js documentation.
Install the Auth.js library.
$ npm install next-auth@beta
Next, register the AUTH_SECRET environment variable in .env.
This environment variable is used by the library to encrypt tokens and email verification hashes.
Running the command below will return a random string.
openssl rand -base64 33
It will look something like this:
$ openssl rand -base64 33
gw7e/vp3ogJ9/8j3YQVhb+jCjF7aJpaacn20uzqm831i
For example, if the above string is returned, define it in .env as follows:
AUTH_SECRET="gw7e/vp3ogJ9/8j3YQVhb+jCjF7aJpaacn20uzqm831i"
Checking that Auth.js works
Create an auth.ts file at the project root.
import NextAuth from 'next-auth'
import github from 'next-auth/providers/github'
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [github],
})
For testing, we configure the github provider for authentication.
Next, configure the Next.js API route.
Create a route.ts file under the app/api/auth/[[...nextauth]] directory.
import { handlers } from '@/auth'
export const { GET, POST } = handlers
If you access http://localhost:3000/api/auth/providers, a list of providers configured in Auth.js will be returned.
Checking that Next.js middleware works
Create a middleware.ts file at the project root.
First, write the following code for testing:
import type { NextRequest } from 'next/server'
export function middleware(req: NextRequest) {
console.log('middleware', req.nextUrl.pathname)
return
}
export const config = {
matcher: '/auth/:path*',
}
Explanation of the code:
import type { NextRequest } from 'next/server'
export function middleware(req: NextRequest) {
console.log('middleware', req.nextUrl.pathname)
return
}
This defines what processing the middleware will perform.
In practice, it simply receives the request and logs the URL of that request to the console.
export const config = {
matcher: '/auth/:path*',
}
Using matcher, we configure which paths will trigger the middleware.
This time, the middleware will run when there is access to a path that starts with /auth.
So, combining the two pieces:
When there is access to a path that starts with `/auth`, log the request URL to the server-side console
That is what it does.
Actually access http://localhost:3000/auth/register.
If a log like the one below is displayed, it’s working correctly.
$ npm run dev
> tutorial-nextjs-14-auth@0.1.0 dev
> next dev
▲ Next.js 14.0.4
- Local: http://localhost:3000
- Environments: .env.local, .env
✓ Ready in 1766ms
✓ Compiled in 252ms (232 modules)
✓ Compiled /middleware in 201ms (63 modules)
middleware /auth/register
You should also be able to confirm that when you access the top page http://localhost:3000, no message is displayed on the server side.
Implementing access control for non-logged-in users
Now that we have confirmed that Auth.js and Next.js middleware work, let’s move on to the actual implementation.
About the edge runtime
Before implementation, let’s briefly touch on the edge runtime.
For details, please refer to the documentation below.
Edge here is borrowed from the network engineering folks and refers to a compute node (i.e. server) that is located on the edge of a network, i.e. closer to the users.
Here, “edge” is a term borrowed from network engineering and refers to a compute node (i.e., server) located at the edge of the network, meaning closer to the users.
So when we say edge runtimes, we mean a server-side JavaScript runtime that is not Node.js and is optimized to run on these edge compute nodes (servers). That generally means that the code is executing closer to your users on lower power hardware that is optimized for other things like quick startup times, low memory usage, etc.
In other words, “edge runtimes” refers to a server-side JavaScript runtime that is not Node.js and is optimized to run on these edge compute nodes (servers). This generally means that code is executed closer to your users on low-power hardware optimized for things like quick startup times and low memory usage.
However, in many cases, database access cannot currently be run on the edge runtime, so in scenarios like this one, where we assume database access during login, we need to configure it so that it does not run on the edge runtime.
We will separate the logic that runs on the edge runtime from the logic that does not.
Create an auth.config.ts file at the project root.
This file will define what runs on the edge runtime.
import type { NextAuthConfig } from "next-auth";
import github from "next-auth/providers/github";
export default { providers: [github] } satisfies NextAuthConfig;
Next, install the library that allows Auth.js to use Prisma.
$ npm install @auth/prisma-adapter
Rewrite the auth.ts file we used earlier for testing as follows:
+ import { PrismaAdapter } from "@auth/prisma-adapter"
import NextAuth from "next-auth";
+ import authConfig from "./auth.config";
+ import db from "./lib/db";
export const { handlers, signIn, signOut, auth } = NextAuth({
- providers: [github],
+ adapter: PrismaAdapter(db),
+ session: { strategy: "jwt" },
+ ...authConfig,
});
We configure session management to use JWT, and since we defined the GitHub provider in auth.config.ts, we remove it from here.
By splitting the responsibilities into these two files, we can use auth.config.ts in Next.js middleware.
Route configuration
In the “Designing the authentication system” section, we designed different behaviors depending on the route. Since these routes will be used in several places, we will centralize the route configuration.
Create a route.ts file at the project root.
/**
* An array that stores URLs of public pages
* These pages can be accessed without authentication
*/
export const publicRoutes: string[] = ["/"];
/**
* An array that stores URLs of authentication-related pages
* If the user is already logged in, they will be redirected to the settings page
*/
export const authRoutes: string[] = ["/auth/login", "/auth/register"];
export const apiAuthPrefix = "/api/auth";
export const DEFAULT_LOGIN_REDIRECT = "/settings";
Configuring middleware
Using the route.ts above, configure Next.js middleware as follows:
import NextAuth from 'next-auth'
import authConfig from './auth.config'
import {
apiAuthPrefix,
authRoutes,
DEFAULT_LOGIN_REDIRECT,
publicRoutes,
} from './route'
const { auth } = NextAuth(authConfig)
export default auth((req) => {
const { nextUrl } = req
const isLoggedIn = !!req.auth
const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix)
const isPublicRoute = publicRoutes.includes(nextUrl.pathname)
const isAuthRoute = authRoutes.includes(nextUrl.pathname)
if (isApiAuthRoute) {
return
}
if (isAuthRoute) {
if (isLoggedIn) {
return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl))
}
return
}
if (!isLoggedIn && !isPublicRoute) {
return Response.redirect(new URL('/auth/login', nextUrl))
}
return
})
export const config = {
matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
}
Explanation of the code:
import authConfig from './auth.config'
const { auth } = NextAuth(authConfig)
We import the auth.config.ts file and configure it so that Auth.js can be used in middleware.
const { nextUrl } = req
const isLoggedIn = !!req.auth
We determine from the request whether the user is logged in or not. Using Auth.js, you can easily check the login state like this.
const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix)
const isPublicRoute = publicRoutes.includes(nextUrl.pathname)
const isAuthRoute = authRoutes.includes(nextUrl.pathname)
Similarly, we determine from the request which resource is being accessed.
if (isApiAuthRoute) {
return
}
if (isAuthRoute) {
if (isLoggedIn) {
return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl))
}
return
}
if (!isLoggedIn && !isPublicRoute) {
return Response.redirect(new URL('/auth/login', nextUrl))
}
return
- For requests to isApiAuthRoute (
/api/auth), we do nothing. - For access to isAuthRoute (
['/auth/login', '/auth/register']), if the session is logged in, we redirect toDEFAULT_LOGIN_REDIRECT(/settings). If the user is not logged in, we do nothing. - If the user is not logged in and the route is not isPublicRoute (
/), we redirect them to the login page.
With this, the implementation of access control is complete.
Preparing the settings page
Finally, we will prepare a settings page for testing.
Create a layout.tsx file under /app/(protected).
const ProtectedLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="flex h-full items-center justify-center bg-gradient-to-br from-sky-100 to-blue-300">
{children}
</div>
)
}
export default ProtectedLayout
Next, create a layout.tsx file under /app/(protected)/page.
const SettingsPage = () => {
return <div>Settings page</div>
}
export default SettingsPage
Checking behavior
Currently, since we are in a non-logged-in state, accessing http://localhost:3000/settings will redirect us to the login page.
This alone may be hard to visualize, so let’s comment out the redirect logic in the middleware and see how the behavior changes.
if (isAuthRoute) {
if (isLoggedIn) {
return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl))
}
return
}
- if (!isLoggedIn && !isPublicRoute) {
- return Response.redirect(new URL('/auth/login', nextUrl))
- }
+ // if (!isLoggedIn && !isPublicRoute) {
+ // return Response.redirect(new URL('/auth/login', nextUrl))
+ // }
return
})
Access http://localhost:3000/settings again, and if the settings page is displayed, it’s working. After confirming the behavior, restore the commented-out middleware code.
Right now, the settings page is displaying raw JSON and is hard to read, but in the later sections we plan to increase the items returned as JSON and rebuild it into a more user-friendly UI.
Conclusion
In this article, we determined whether the session for an incoming request is logged in or not, and configured which pages can be accessed in each state.
Next time, we will create the actual login screen and verify that a logged-in session can access pages that are only available to logged-in users.
Here is the next article
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Chat App (with Image/PDF Sending and Video Call Features)
2024/07/15Practical Component Design Guide with React × Tailwind CSS × Emotion: The Optimal Approach to Design Systems, State Management, and Reusability
2024/11/22Management Dashboard Features (Graph Display, Data Import)
2024/06/02Tutorial for Implementing Authentication with Next.js and Auth.js
2024/09/13Thorough Comparison of the Best ORMs for the Next.js App Router: How to Choose Between Prisma / Drizzle / Kysely / TypeORM [Part 1]
2025/03/13Test Strategy in the Next.js App Router Era: Development Confidence Backed by Jest, RTL, and Playwright
2025/04/22Done in 10 minutes. Easy deployment procedure for a Next.js app using the official AWS Amplify template
2024/11/05Building an Integrated Next.js × AWS CDK Environment: From Local Development with Docker to Production Deployment
2024/05/11







