programming

Secure Next.js Applications with Role-Based Authentication Using NextAuth

Introduction:

Next.js is a popular React framework for building full-stack web applications. It provides features such as routing, rendering, data fetching, and styling that make it easy to develop and deploy web apps. However, when it comes to authentication and authorization, Next.js does not have a built-in solution. This is where NextAuth.js comes in handy.

NextAuth.js is an open-source library that simplifies the process of adding authentication and role-based access control (RBAC) to Next.js apps. It supports various sign-in methods, such as OAuth, email, passwordless, and magic links. It also allows you to create custom roles and assign them to users based on their permissions. With NextAuth.js, you can secure your Next.js app and its resources with minimal code and configuration.

In this blog post, we will show you how to use NextAuth.js to implement role-based authentication in a Next.js app. We will create a simple app that has three pages: Home, Dashboard, and Admin. The Home page will allow anyone to access it, the Dashboard page will require the user to be signed in, and the Admin page will only be accessible by users who have the admin role. We will use Google as the OAuth provider for sign-in, and MongoDB as the database for storing user and session data.

Prerequisites

To follow along with this tutorial, you will need:

  • Node.js and npm installed on your machine
  • A Google account and a Google Cloud project with an OAuth client ID and secret
  • A MongoDB account and a database with a users collection
  • A basic understanding of Next.js and React

Setting up the project

First, let’s create a new Next.js app using the create-next-app command:

npx create-next-app nextauth-rbac-demo

This will create a new folder called nextauth-rbac-demo with the default Next.js starter template. Next, let’s install the dependencies we will need for this project:

cd nextauth-rbac-demo
npm install next-auth mongodb

NextAuth.js is the library we will use for authentication and RBAC, and MongoDB is the driver we will use to connect to our database.

Next, let’s create a .env.local file in the root of our project and add the following environment variables:

NEXTAUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=<your-google-client-id>
GOOGLE_CLIENT_SECRET=<your-google-client-secret>
MONGODB_URI=<your-mongodb-connection-string>

The NEXTAUTH_URL variable is the base URL of our app, which NextAuth.js will use to generate the callback URL for OAuth. The GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET variables are the credentials we obtained from Google Cloud for our OAuth app. The MONGODB_URI variable is the connection string to our MongoDB database, which we will use to store user and session data.

Make sure to replace the placeholders with your own values. You can find more information on how to obtain these values from the NextAuth.js documentation1.

Configuring NextAuth.js

Next, let’s create a folder called pages and a file called pages/api/auth/[…nextauth].js. This file will be the entry point for NextAuth.js, where we will configure our authentication options and callbacks. Add the following code to this file:

import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { connectToDatabase } from "../../../lib/db";

export default NextAuth({
  // Configure one or more authentication providers
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    // ...add more providers here
  ],

  // A database is optional, but required to persist accounts in a database
  database: process.env.MONGODB_URI,

  // Define custom pages for sign-in, sign-out, error, etc
  pages: {
    signIn: "/auth/signin",
    signOut: "/auth/signout",
    error: "/auth/error", // Error code passed in query string as ?error=
    // newUser: null // If set, new users will be directed here on first sign in
  },

  // The secret should be set to a reasonably long random string.
  // It is used to sign cookies and to sign and encrypt JSON Web Tokens, unless
  // a separate secret is defined explicitly for encrypting the JWT.
  secret: process.env.SECRET,

  // Use JSON Web Tokens for session instead of database sessions.
  // This option can be used with or without a database for users/accounts.
  // Note: `jwt` is automatically set to `true` if no database is specified.
  // jwt: true,

  // Set to true to use Refresh Tokens. If set to false, the cookie will be used as a long lived session token.
  // useRefreshTokens: true,

  // Events are useful for logging
  // https://next-auth.js.org/configuration/events
  events: {},

  // Callbacks are asynchronous functions you can use to control what happens
  // when an action is performed.
  // https://next-auth.js.org/configuration/callbacks
  callbacks: {
    // async signIn(user, account, profile) { return true },
    // async redirect(url, baseUrl) { return baseUrl },
    // async session(session, user) { return session },
    // async jwt(token, user, account, profile, isNewUser) { return token }
  },

  // Enable debug messages in the console if you are having problems
  debug: false,
});

This code imports NextAuth.js and the Google provider, and exports a default function that calls NextAuth.js with some options. The options we are using are:

  • providers: An array of authentication providers, in our case, only Google.
  • database: The connection string to our MongoDB database, where NextAuth.js will store user and session data.
  • pages: An object that defines custom pages for sign-in, sign-out, error, etc. We will create these pages later.
  • secret: A secret string that is used to sign cookies and JSON Web Tokens. You can generate a random string using a tool like this2.
  • callbacks: An object that defines custom functions that are executed when certain actions are performed, such as sign-in, redirect, session, etc. We will use these callbacks later to implement RBAC.

You can find more information about the NextAuth.js options and how to customize them from the documentation3.

Creating custom pages

Next, let’s create some custom pages for our app. We will create four pages: Home, Dashboard, Admin, and Sign-in. The Home page will be accessible by anyone, the Dashboard page will require the user to be signed in, the Admin page will only be accessible by users who have the admin role, and the Sign-in page will allow the user to sign in with Google.

Home page

Create a file called pages/index.js and add the following code:

import { useSession, signIn, signOut } from "next-auth/react";

export default function Home() {
  const { data: session } = useSession();

  return (
    <div>
      <h1>Home</h1>
      <p>This is the home page. Anyone can access it.</p>
      {session ? (
        <>
          <p>Signed in as {session.user.email}</p>
          <button onClick={() => signOut()}>Sign out</button>
        </>
      ) : (
        <>
          <p>Not signed in</p>
          <button onClick={() => signIn()}>Sign in</button>
        </>
      )}
    </div>
  );
}

This code imports the useSession, signIn, and signOut hooks from NextAuth.js, and uses them to display the user’s email and a sign-in or sign-out button depending on the session state.

Dashboard page

Create a file called pages/dashboard.js and add the following code:

import { useSession, signIn } from "next-auth/react";

export default function Dashboard() {
  const { data: session, status } = useSession();

  if (status === "loading") return <p>Loading...</p>;
  if (status === "unauthenticated") {
    signIn();
    return null;
  }

  return (
    <div>
      <h1>Dashboard</h1>
      <p>This is the dashboard page. You must be signed in to access it.</p>
      <p>Your role is {session.user.role}</p>
    </div>
  );
}

This code imports the useSession and signIn hooks from NextAuth.js, and uses them to check the session status. If the status is loading, it displays a loading message. If the status is unauthenticated, it redirects the user to the sign-in page. If the status is authenticated, it displays the dashboard page and the user’s role.

Admin page

Create a file called pages/admin.js and add the following code:

import { useSession, signIn } from "next-auth/react";
import { useRouter } from "next/router";

export default function Admin() {
  const { data: session, status } = useSession();
  const router = useRouter();

  if (status === "loading") return <p>Loading...</p>;
  if (status === "unauthenticated") {
    signIn();
    return null;
  }

  if (session.user.role !== "admin") {
    router.push("/");
    return null;
  }

  return (
    <div>
      <h1>Admin</h1>
      <p>This is the admin page. You must have the admin role to access it.</p>
    </div>
  );
}

This code imports the useSession and signIn hooks from NextAuth.js, and the useRouter hook from Next.js, and uses them to check the session status and the user’s role. If the status is loading, it displays a loading message. If the status is unauthenticated, it redirects the user to the sign-in page. If the status is authenticated, but the user’s role is not admin, it redirects the user to the home page. If the status is authenticated and the user’s role is admin, it displays the admin page.

Sign-in page

Create a file called pages/auth/signin.js and add the following code:

import { getProviders, signIn } from "next-auth/react";

export default function SignIn({ providers }) {
  return (
    <div>
      <h1>Sign in</h1>
      <p>Please choose one of the following providers to sign in:</p>
      {Object.values(providers).map((provider) => (
        <div key={provider.name}>
          <button onClick={() => signIn(provider.id)}>
            Sign in with {provider.name}
          </button>
        </div>
      ))}
    </div>
  );
}

// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context) {
  const providers = await getProviders();
  return {
    props: { providers },
  };
}

This code imports the getProviders and signIn functions from NextAuth.js, and uses them to fetch the available providers and display a sign-in button for each one. It also uses the getServerSideProps function to pre-render the providers on the server side.

Implementing RBAC

Now that we have created our custom pages, we need to implement RBAC to assign roles to users and restrict access to certain pages based on their roles. To do this, we will use the callbacks option of NextAuth.js, which allows us to customize the behavior of NextAuth.js when certain actions are performed.

Assigning roles to users

To assign roles to users, we will use the signIn callback, which is executed when a user signs in for the first time or when a user signs in with a new account. In this callback, we will check the user’s email and assign them a role based on a predefined list of admin emails. We will also store the role in the user document in the database, so that we can access it later. Add the following code to the callbacks option in pages/api/auth/[…nextauth].js:

callbacks: {
  async signIn(user, account, profile) {
    // Define a list of admin emails
    const adminEmails = ["admin@example.com", "admin2@example.com"];

    // Check if the user's email is in the list
    const isAdmin = adminEmails.includes(user.email);

    // Assign the user a role based on their email
    user.role = isAdmin ? "admin" : "user";

    // Connect to the database
    const { db } = await connectToDatabase();

    // Update the user document with the role
    await db.collection("users").updateOne(
      { email: user.email },
      { $set: { role: user.role } },
      { upsert: true }
    );

    // Return true to allow sign in
    return true;
  },
},

This code defines a list of admin emails, checks if the user’s email is in the list, assigns the user a role of admin or user, connects to the database, and updates the user document with the role. It then returns true to allow the sign in to proceed.

Adding roles to sessions

To add roles to sessions, we will use the session callback, which is executed whenever a session is created or updated. In this callback, we will fetch the user’s role from the database and add it to the session object, so that we can access it from the client side. Add the following code to the callbacks option in pages/api/auth/[…nextauth].js:

callbacks: {
  // ...other callbacks
  async session(session, user) {
    // Connect to the database
    const { db } = await connectToDatabase();

    // Find the user document by email
    const userDoc = await db.collection("users").findOne({ email: user.email });

    // Add the user's role to the session object
    session.user.role = userDoc.role;

    // Return the updated session
    return session;
  },
},

This code connects to the database, finds the user document by email, adds the user’s role to the session object, and returns the updated session.

Testing the app

We are now ready to test our app and see how it works. To do this, run the following command in your terminal:

npm run dev

This will start the development server on http://localhost:3000. Open this URL in your browser and navigate to the different pages. You should see something like this:

![Home page]

![Sign-in page]

![Dashboard page]

![Admin page]

You can sign in with your Google account and see your role on the dashboard page. If your email is in the list of admin emails, you should be able to access the admin page. Otherwise, you should be redirected to the home page.

Conclusion

In this blog post, we have shown you how to use NextAuth.js to implement role-based authentication in a Next.js app. We have created a simple app that has three pages: Home, Dashboard, and Admin. We have used Google as the OAuth provider for sign-in, and MongoDB as the database for storing user and session data. We have also used the callbacks option of NextAuth.js to assign roles to users and add them to sessions, and used the useSession hook to check the session status and the user’s role on the client side.

We hope you have enjoyed this tutorial and learned something new. If you have any questions or feedback, please feel free to leave a comment below. Thank you for reading! 😊

: [https://next-auth.js.org/getting-started/introduction] : [https://www.random.org/strings/] : [https://next-auth.js.org/configuration/options]

Leave a Comment

Your email address will not be published. Required fields are marked *