Authentication and authorization are two important steps in protecting your users’ most valuable information, however, developers often confuse the two.
In this tutorial, we’ll learn how to implement a few different authentication strategies in Next.js, for example, using credentials like email and password, as well as GitHub, a third-party service. We’ll also cover implementing authorization using the role-based authorization system in Next.js. Let’s get started!
Jump ahead
Authentication is the act of validating that a user is who they claim to be. Usernames and passwords are the most common authentication factors.
When authenticating a user, if the user enters the correct data, the server assumes that the identity is valid and grants the user access to the server resource.
On the other hand, authorization in a security system is the process of giving the user permission to access a specific resource or function on the server. This term is often used interchangeably with access control or client privilege.
Usually, authentication precedes authorization; users should first prove that their identities are genuine before the backend administrator grants them access to the requested resources.
In this article, we’ll implement both authentication and authorization strategies using NextAuth.js. Out of the box, NextAuth.js supports several different authentication providers, including Atlassian, Auth0, and GitHub. You can check out the docs for a complete list.
Since this article covers both authentication and authorization, we’ll start by learning about authentication, which generally comes first. Building on this knowledge, we’ll then learn how to implement authorization using NextAuth.js.
To follow along with this article, you’ll need the following:
As previously mentioned, NextAuth.js is a robust and feature-packed authentication package for Next.js. In this section, we’ll implement authentication using two separate methods, GitHub and email and password credentials.
To get started, clone this repository to get the starter file. Now, from the app directory, run the following code to bootstrap the application:
// install dependencies npm install // start dev-server npm run dev
If everything was successful, you should see the following:
The application boilerplate above is a simple Next.js and Tailwind CSS application. Going forward, we’ll build on top of this template.
First, install NextAuth.js by running the code below:
npm i next-auth
To add NextAuth.js to your application, create a [...nextauth].js
file in the pages/api/auth
directory and add the following code:
import NextAuth from "next-auth" import GithubProvider from "next-auth/providers/github" export const authOptions = { secret: process.env.NextAuth_SECRET, // Configure one or more authentication providers providers: [ GithubProvider({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, }), // ...add more providers here ], } export default NextAuth(authOptions);
The code above configures NextAuth.js with the GitHub provider. So, to get your client ID and client secret, you’ll need to create a new GitHub OAuth application.
Once this is done, create a .env.local
file in the root directory with the following code:
GITHUB_ID=<!-- Your client ID --> GITHUB_SECRET=<!-- Your client secret --> NextAuth_SECRET=<!-- any text -->
Now, NextAuth.js will automatically handle all requests to /api/auth/*
, including signIn
, callback
, signOut
, etc.
To configure the shared state, we need to expose the session context <SessionProvider />
at the top level of your application. Doing so will enable the use of the useSection()
Hook from React.
<SessionProvider />
keeps the session updated and synced between browser tabs and windows. So, to expose the session context, update the page/_api
component as shown below:
import '../styles/globals.css' import { SessionProvider } from "next-auth/react" export default function App({ Component, pageProps: { session, ...pageProps }, }) { return ( <SessionProvider session={session}> <Component {...pageProps} /> </SessionProvider> ) }
With the setup above, instances of useSession
will have access to the session data
and status
. Consequently, we can check if a user is authenticated or authorized from the session and dynamically render jsx
to the user; we’ll see this in action later.
Next, update the page/index.js
component, as shown below:
import { useSession, signIn, signOut } from "next-auth/react" export default function Home() { const { data: session, status } = useSession() if (status === "authenticated") { return ( <section className="grid h-screen place-items-center"> <div className="max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"> <h2 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Hi {session?.user?.name}</h2><br /> <p className="mb-3 font-normal text-gray-700 dark:text-gray-400">You are signed in as {session?.user?.email}.</p> <button type="button" onClick={() => signOut()} className="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-red-700 rounded-lg hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"> Logout </button> </div> </section> ) } return ( <section className="grid h-screen place-items-center"> <div className="max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"> <h2 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Welcome To LogRocket</h2><br /> <p className="mb-3 font-normal text-gray-700 dark:text-gray-400">You currently not authenticated. Click the login button to get started!</p> <button type="button" onClick={() => signIn()} className="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-green-700 rounded-lg hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800"> Login </button> </div> </section> ); }
In the code above, we destructured data
and renamed it session
and status
from the return value of useSession()
. Using the status
value, we render the appropriate UI depending on the authentication status of a user.
Now, if you restart the dev-server
, you’ll see the following in your browser:
The UI above is shown because you are not currently authenticated. To become authenticated, click the Login button, and you’ll see the default login page created by NextAuth.js, which contains only the GitHub provider:
After clicking the button, GitHub may ask you to authorize the app. Finally, if everything is successful, you’ll see the following:
In this section, we’ll learn how to implement authentication using email and password credentials. To do so, we need a database and some backend logic to handle our login request. But, this is out of the scope of this article, so we’ll improvise by using Next.js API routes.
In the root directory, create a data folder, and inside, create a users.js
file with the following code:
const Users = [ { id: "111", name: "John Doe", email: "[email protected]", password: 1232, role: "user" }, { id: "112", name: "Jane Doe", email: "[email protected]", password: 1234, role: "user" }, { id: "113", name: "Jenny Doe", email: "[email protected]", password: 1235, role: "admin" }, { id: "114", name: "Jude Doe", email: "[email protected]", password: 2222, role: "admin" }, ]; export { Users };
In the code above, we export an array of users
that will act as our database. Now, in the page/api
folder, create a login.js
file with the following code:
import { Users } from "../../data/users"; export default function handler(req, res) { try { if (req.method !== 'POST') { res.status(405).send({ message: 'Only POST requests allowed' }) return } const body = JSON.parse(JSON.stringify(req.body)) const user = Users.find((user) => user.email === body.email && user.password === parseInt(body.password)); if (!user) { res.status(404).send({ message: 'User does not exit!' }) return } res.status(200).json(user); } catch (error) { res.status(405).send({ message: `{error.message}` }) return } };
The Node.js code above represents the Next.js API, which is the backend code that will handle our login request. It finds and returns a user from the users
array using the credentials provided.
Next, update the [...nextauth].js
file as shown below:
import NextAuth from "next-auth" import GithubProvider from "next-auth/providers/github" import CredentialsProvider from "next-auth/providers/credentials"; export const authOptions = { secret: process.env.NextAuth_SECRET, // Configure one or more authentication providers providers: [ GithubProvider({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, }), CredentialsProvider({ // The name to display on the sign in form (e.g. "Sign in with...") name: "Credentials", // `credentials` is used to generate a form on the sign in page. // You can specify which fields should be submitted, by adding keys to the `credentials` object. // e.g. domain, username, password, 2FA token, etc. // You can pass any HTML attribute to the <input> tag through the object. credentials: { email: { label: "Email", type: "text", placeholder: "Enter email", }, password: { label: "Password", type: "password", placeholder: "Enter Password", }, }, async authorize(credentials, req) { const { email, password } = credentials const res = await fetch("http://localhost:3000/api/login", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ email, password, }), }); const user = await res.json(); if (res.ok && user) { return user; } else return null; }, }), // ...add more providers here ], callbacks: { async jwt({ token, user }) { return { ...token, ...user }; }, async session({ session, token, user }) { // Send properties to the client, like an access_token from a provider. session.user = token; return session; }, }, pages: { signIn: '/auth/signin', } } export default NextAuth(authOptions)
In the [...nextauth].js
code above, we added the CredentialsProvider
and configured it to authenticate users with email
and password
. In the authorize async
function, we handled the POST
request made to the Next.js API routes we created earlier using the specified user credentials. Then, we returned the user if the user was found, or null
if the user was not found.
We then handled specific actions using callbacks. In NextAuth.js, callbacks are powerful, async functions that enable us to control what happens when an action occurs. Callbacks enable us to implement access control when we’re not using a database, as is our current case. They also enable us to integrate with external databases or APIs.
In the code above, we used a JWT callback to return a new authenticated user object with its token and a session callback to make the new user with the JWT token available to the client.
Finally, we used the pages
property to overwrite the Next.js default login page by specifying a signin
property with the /auth/signin
value.
Now, in the pages
folder, create a new folder called auth
. Inside the auth
folder, create a new file called signin.js
with the following code:
import { useRef } from "react"; import { getProviders, getSession, signIn } from "next-auth/react" const Signin = ({ providers }) => { const email = useRef(""); const password = useRef(""); return ( <div className="flex items-center min-h-screen p-4 bg-gray-100 lg:justify-center"> <div className="flex flex-col overflow-hidden bg-white rounded-md shadow-lg max md:flex-row md:flex-1 lg:max-w-screen-md" > <div className="p-4 py-6 text-white bg-blue-500 md:w-80 md:flex-shrink-0 md:flex md:flex-col md:items-center md:justify-evenly" > <div className="my-3 text-4xl font-bold tracking-wider text-center"> <a href="#">LOGROCKET</a> </div> <p className="mt-6 font-normal text-center text-gray-300 md:mt-0"> Lorem Ipsum is simply dummy text of the printing and typesetting industry. </p> <p className="flex flex-col items-center justify-center mt-10 text-center"> <span>Don't have an account?</span> <a href="#" className="underline">Get Started!</a> </p> <p className="mt-6 text-sm text-center text-gray-300"> Read our <a href="#" className="underline">terms</a> and <a href="#" className="underline">conditions</a> </p> </div> <div className="p-5 bg-white md:flex-1"> <h3 className="my-4 text-2xl font-semibold text-gray-700">Account Login</h3> <form action="#" className="flex flex-col space-y-5"> <div className="flex flex-col space-y-1"> <label htmlFor="email" className="text-sm font-semibold text-gray-500">Email address</label> <input type="email" id="email" autoFocus className="px-4 py-2 transition duration-300 border border-gray-300 rounded focus:border-transparent focus:outline-none focus:ring-4 focus:ring-blue-200" onChange={(e) => (email.current = e.target.value)} /> </div> <div className="flex flex-col space-y-1"> <div className="flex items-center justify-between"> <label htmlFor="password" className="text-sm font-semibold text-gray-500">Password</label> <a href="#" className="text-sm text-blue-600 hover:underline focus:text-blue-800">Forgot Password?</a> </div> <input type="password" id="password" className="px-4 py-2 transition duration-300 border border-gray-300 rounded focus:border-transparent focus:outline-none focus:ring-4 focus:ring-blue-200" onChange={(e) => (password.current = e.target.value)} /> </div> <div> <button type="button" className="w-full px-4 py-2 text-lg font-semibold text-white transition-colors duration-300 bg-blue-500 rounded-md shadow hover:bg-blue-600 focus:outline-none focus:ring-blue-200 focus:ring-4" onClick={() => signIn("credentials", { email: email.current, password: password.current, })} > Log in </button> </div> <div className="flex flex-col space-y-5"> <span className="flex items-center justify-center space-x-2"> <span className="h-px bg-gray-400 w-14"></span> <span className="font-normal text-gray-500">or login with</span> <span className="h-px bg-gray-400 w-14"></span> </span> <div className="flex flex-col space-y-4"> {providers && Object.values(providers).map(provider => { if (provider.name !== "Credentials") { return ( <div key={provider.name} style={{ marginBottom: 0 }}> <a href="#" className="flex items-center justify-center px-4 py-2 space-x-2 transition-colors duration-300 border border-gray-800 rounded-md group hover:bg-gray-800 focus:outline-none" onClick={() => signIn(provider.id)} > <span> <svg className="w-5 h-5 text-gray-800 fill-current group-hover:text-white" viewBox="0 0 16 16" version="1.1" aria-hidden="true" > <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" ></path> </svg> </span> <span className="text-sm font-medium text-gray-800 group-hover:text-white">Sign in with{' '} {provider.name}</span> </a> </div> ) } })} </div> </div> </form> </div> </div> </div> ) } export default Signin export async function getServerSideProps(context) { const { req } = context; const session = await getSession({ req }); const providers = await getProviders() if (session) { return { redirect: { destination: "/" }, }; } return { props: { providers, }, } }
The code above simply displays a login form to enable login using credentials. Below this form, it displays a list of other providers. Currently, we have only the GitHub provider.
If the login attempt is successful, the user will be redirected to the homepage. Now, if you restart the dev-server
, you should see the following:
You can test the credentials provider implementation by logging in with a username and password from the data array, and the result will be the same as the GitHub provider. With that, our authentication is complete!
In the next section, we’ll learn how to implement authorization.
Authorization checks whether an authenticated user has permission to view a UI or use a feature. In this section, we’ll implement the role-based authorization system. To do so, we need to create a page that requires admin privileges to view. In the page
folder, create a dashboard.js
file with the following code:
import { signOut, useSession } from "next-auth/react" export default function Dashboard() { const { data: session } = useSession() const user = session?.user; if (user?.role !== "admin") { return ( <section className="grid h-screen place-items-center"> <div className="w-25"> <p>You do not have permission to view this page!</p> </div> </section> ); } return ( <section className="grid h-screen place-items-center"> <div className="max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"> <h2 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Hello {session?.user?.name}</h2><br /> <p className="mb-3 font-normal text-gray-700 dark:text-gray-400">You are an admin user currently signed in as {session?.user?.email}.</p> <button type="button" onClick={() => signOut()} className="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-red-700 rounded-lg hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"> Logout </button> </div> </section> ) }
The code above gets the currently logged-in and authenticated user from the session and checks if the user role is admin
. If it is, it renders the page. Otherwise, it renders a UI reading you do not have permission to view this page!
.
You can test this out by logging in with a user that is not an admin and navigating to localhost:3000/dashboard
. You’ll see the following:
Now, log in with an admin user and visit the dashboard. You’ll see the following:
The NextAuth.js library provides built-in support for many popular sign-in services, making the process of API integration quick and easy. In this article, we explored using NextAuth.js to configure a Next.js application to use the OAuth flow for user authorization. We also explored authenticating and authorizing users using email and password credentials in NextAuth.js.
I hope you enjoyed this article. If you have any questions, be sure to leave a comment. Happy coding!
Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your Next.js apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
4 Replies to "How to implement authentication and authorization in Next.js"
Does this have the problem of auth state initially not being available on page load, so that it takes a second for it to switch to authenticated mode?
For me, if I get auth on the server, on the client the session is initially null. So the benefit of having gotten the session on the server is lost.
Hi,
I’m having a little difficulty understanding your question but I’m guessing that you’re asking why the auth state is not available on initial page load.
Regarding that, note that a user’s GitHub information is only retrieved after the user is only retrieved after the user has authorized the app to get said information. On initial load, the page simply shows a dummy profile picture and a text instruction the user to login in with GitHub.
Hope it helps!
Kingsley Ubah,
Author at LogRocket.
Good post, but the title is misleading. This is authentication, not authorization.
I have an issue when im opening a sign in page. TypeError: Cannot read properties of null (reading ‘useRef’)
In file signin.js where we creating a const email = useRef(“”)