Back

Authentication and Authorization with SSR in Next.js

Authentication and Authorization with SSR in Next.js

Authentication and authorization are two basic security requirements, and while there are many ways to go about them, this article will show a modern way, using SSR (Server Side Rendering) in a React app built with Next.js

How to Handle Authentication and Authorization in Next.js with Server-Side Rendering

The importance of authentication and authorization cannot be overemphasized in every web application. Authentication and authorization is an integral part of a development process. Authentication is verifying a user’s identity; it simply checks if a user is who they claim to be. A typical example of authentication is using an ID to buy age-restricted items in a store. There are various methods of authenticating your users in web applications, including usernames and passwords, social logins, and token-based authentication. Authorization works hand in hand with authentication, while authentication verifies who a user is, authorization determines what a user can access or do with the application. Authentication and authorization ensure that users’ data are secure and private and get personalized experiences tailored to their accounts. In this article, we will learn how to handle authentication and authorization in Next.js with server-side rendering.

Authentication Approaches in Next.js

Next.js is a popular framework for building Single-Page Applications (SPAs). It primarily offers two approaches to authentication: client-side authentication and server-side authentication.

Client-Side Authentication

In client-side authentication, user credentials and logic are handled entirely in the browser. Typically, when this process happens, a loading indicator shows the user that a process is running in the background; this enhances the user experience in your application. It is simply the process of verifying a user’s identity within the client-side application without needing server-side interaction during the initial authentication step.

Server-Side Authentication

Server-side authentication is the process of verifying users’ identities on the server side of an application. In this approach, authentication logic is implemented and executed on the server, which is responsible for validating the credentials provided by the client and granting access accordingly.

Implementing Authentication in Next.js with NextAuth.js

NextAuth.js is an open-source library explicitly designed for authentication in Next.js applications. It offers a secure and flexible way to handle user logins, signups, and authorization within your Next.js project. To start with NextAuth.js, we first need to create a next.js project by running the following command on your terminal.

npx create-next-app@latest my-nextjs-app

After successfully creating the Next.js project, we have to install NextAuth.js in our project. We can install it by running the following command on our terminal

npm install next-auth

Adding Authentication Provider

NextAuth.js provides various ways of authentication; in our app, we will be using a built-in OAuth provider, Github OAuth. Github OAuth allows users to sign in to your application using a GitHub account. First, we need to create a Github OAuth app. Click on Register a new app and fill out the form.

Application name: This is simply your application’s name; it should be descriptive and able to identify your application to both users and GitHub.

  • Homepage URL: The homepage URL is the full URL to the homepage of our app; since we are still in development mode, it should be http://localhost:3000.
  • Authorization callback URL: This is the URL where GitHub will redirect users after they have authorized your application. The URL should be http://localhost:3000/api/auth/callback, your homepage, plus /api/auth/callback. After you fill in the fields, Github will provide you with a Client ID and a Client Secret.

Configure NextAuth.js.

To add nextauth.js to your project, create a [...nextauth].js file in your pages/api/auth directory and add the following code:

import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";

export default NextAuth({
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
  secret: process.env.NEXTAUTH_SECRET,
});

In the code above, we configured nextauth.js with the GitHub provider. The clientId and client secret are obtained from the GitHub OAuth app we registered earlier. The JWTs (JSON WEB TOKENS) are used by NextAuth.js to securely manage user sessions and authorization.

When a user successfully signs in using GitHub, NextAuth.js creates a JWT containing user information; however, this dynamically generated token won’t be persistent across server restarts. This simply means that whenever your server restarts, a newly generated token is made, thereby invalidating all your signed-in tokens and sessions. Therefore, using your own NEXTAUTH_SECRET variable is highly recommended for consistent session and token management in production environments.

To generate your own NEXTAUTH_SECRET, enter the following command on your terminal. openssl rand -base64 32. Once this is done, create a .env.local file in the root folder of your application and store the environment variables. This ensures you don’t expose your environment’s secrets.

GITHUB_ID=<!-- Your client ID -->
GITHUB_SECRET=<!-- Your client secret -->
NEXTAUTH_URL = <!-- http://localhost:3000 -->
NEXTAUTH_SECRET=<!-- your generated JWT secret  -->

Configure shared state

We have to configure the state at the top level of our application. To do this, create a provider.js file in your app/provider.folder with the following code:

"use client";

import { SessionProvider } from "next-auth/react";

export default function Providers({ children }) {
  return <SessionProvider>{children}</SessionProvider>;
}

And then wrap the provider component in your layout.js file:

import type, { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Link from "next/link";
import Providers from "./providers";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

In the code above, the <SessionProvider/> component from nextauth, provides session data across all components in our application. It takes a session object as a prop and makes it available to all components in our application. This session object contains information about the authenticated user, such as their ID, email, name, and other relevant user data.

With the help of the usesession hook, we can now access the section data and status within any component. The useSessionhook allows us to check if a user is authenticated and accesses their information. To access the session data, create a login.js file in your app folder and copy the following code:

"use client";

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

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

  return session ? (
    <>
      <div className="flex flex-col items-center justify-center h-screen">
        <div className="text-center">
          <p className="mb-4">Welcome, {session.user.name}!</p>
          <img
            src={session.user.image}
            className="w-24 h-24 rounded-full mx-auto mb-4"
          />
          <button
            onClick={() => signOut()}
            className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
          >
            Sign out
          </button>
        </div>
      </div>
    </>
  ) : (
    <>
      <div className="text-center">
        <p className="mb-4">You need to sign in to access this page</p>
        <button
          onClick={() => signIn("github")}
          className="bg-gray-800 hover:bg-gray-900 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
        >
          Sign in with GitHub
        </button>
      </div>
    </>
  );
}

We used the useSession hook to access the user’s data. The sign in hook redirects a user to Github, while the sign-out hook clears the user’s authentication and logs the user out. If you have successfully reached this stage, you should be able to log in with your GitHub account.

Create-Next-App (7)

After clicking the button, GitHub will ask you to authorize your application, and when you do, you will see the image below: Create-Next-App (6)

Authorization in Next.js With SSR

Role-based access control (RBAC) is a security strategy that restricts access to features and functionalities within your application based on a user’s assigned role. This ensures that only authorized users can perform specific actions or view sensitive information. An example of (RABC) is an employee management application. HRs can access all employees’ data, while employees can access their personal information. One primary benefit of RBAC is the improved data security it provides.

Role-Based Authorization with Next.js Middleware

In Next.js, middleware refers to a function that runs before handling a request. It allows you to intercept and manipulate incoming HTTP requests and outgoing responses.

Middleware allows you to run code before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly- Nextjs official doc

Middleware functions are commonly used for authentication, authorization, logging, error handling, and data parsing. Here, we will focus on using middleware to authorize our users and control their access to specific parts of our app. Now, let’s add middleware to our application that can restrict access to some specific users. First, we need to create some pages. In your app folder, create an admin folder, inside it a page.js file, and add the following code to it.

export default function Admin() {
  return (
    <div className="items-center flex flex-col justify-center mb-4">
      This is the Admin page!
    </div>
  );
}

Also, do the same for the profile route:

export default function Profile() {
  return (
    <div className="items-center flex flex-col justify-center mb-4">
      This is the Profile page!
    </div>
  );
}

Next is to define middleware in our application, create a middleware.js file in the root directory of our project, and add the following code:

import { withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";

export default withAuth(
  function middleware(req) {
    if (
      req.nextUrl.pathname === "/admin" &&
      req.nextauth.token?.role !== "admin"
    ) {
      return new NextResponse(
        "Permission Denied⚠️⚠️⚠️ , this page can only be viewed by an admin user !"
      );
    }
  },
  {
    callbacks: {
      authorized: (params) => {
        let { token } = params;
        return !!token;
      },
    },
  }
);

export const config = { matcher: ["/admin", "/profile"] };

If you look closely at the code above, we imported withAuth from the next-auth/middleware package and NextResponsefrom next/server used for handling authentication and HTTP response in Nextjs. Within the withAuth function, we have a middleware function that takes a req as a parameter. It checks if the pathname of the requested URL is /admin, and if the user’s role obtained from the authentication token is not admin, it returns a response with a message indicating permission denial. It also takes a configuration object as its second argument.

This object contains an authorized callback function that determines whether a user is authorized based on the parameters it receives, particularly the authentication token. It returns true if the tokenexists, indicating that the user is authorized.Next is to update our [...nextauth].js file with the following callback function to text our authorization logic.

import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";

export default NextAuth({
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
  callbacks: {
    async jwt({ token }) {
      token.role = "member";
      return token;
    },
  },
});

In the code above, we defined our jwt callback function, and we can access the token from the parameters and set the token.role = "member" to test our code; when a user clicks on the admin, it should restrict access because it is not an admin user. Finally, we must add the route pages as links for easy accessibility. In your layout.js file update it with the following code:

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Link from "next/link";
import Providers from "./providers";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Providers>
          <nav className="flex space-x-4 px-6 py-4">
            <Link href="/">Home</Link>
            {` `}
            <Link href="/admin">Admin </Link>
            {` `}
            <Link href="/profile">Profile</Link>
          </nav>

          {children}
        </Providers>
      </body>
    </html>
  );
}

If you have reached this stage, you should be able to log in, and when you click on the admin, it should show Permission denied because we set the role to member.localhost-3000-admin (1)

Authentication using Credentials Provider

So far, we have done authentication with Github OAuth and authorization with Next.js middleware. Now we need to add authentication using credentials provider, that is, email and password. And for this, we will need an API to handle our login request. In our case, we will be using an external API. To add a credentials provider to our already existing provider, update your pages/api/auth/[...nextauth].js file with the following code:

import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";
import { login } from "@/services/apiServices";
export default NextAuth({
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
    CredentialsProvider({
      credentials: {
        email: { label: "Email", type: "text" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials, req) {
        try {
          const res = await login(credentials.email, credentials.password);
          if (res) {
            return {
              id: res.id,
              name: res.name,
              email: res.email,
              role: res.role,
            };
          } else {
            return null;
          }
        } catch (error) {
          // Handle any errors
          console.error("Authentication error:", error);
          return null;
        }
      },
    }),
  ],
  callbacks: {
    async jwt({ token }) {
      token.role = token.role || "member";
      return token;
    },
  },
});

In the code above, The authorize callback function is defined for CredentialsProvider, which is responsible for authenticating users based on the provided credentials that is email and password. It calls the login function from our apiServices and if successful, returns an object containing user information (id, name, email, and role). If authentication fails, it returns null.

Next, we must access the session in the serverside component. To access the session in a Severside component, in your pages directory, create a file, api/public/page.js and copy the following code:

import { getServerSession } from "next-auth/next";
import { GetServerSidePropsContext } from "next";
import { login } from "@/services/apiServices";

export default function Protected({ user }) {
  return (
    <div>
      <div>
        {user ? (
          <h1>Hi {user.name}!</h1>
        ) : (
          <a href="/api/auth/signin">Sign in</a>
        )}
      </div>
    </div>
  );
}

export async function getServerSideProps(context) {
  const session = await getServerSession(context);

  const user = session ? session.user : null;

  return {
    props: {
      user,
    },
  };
}

In the code above, the getServerSideProps function fetches the user session on the server side before rendering the page; with getServerSession, we can get the current user’s session information. If a user session is found, the user object is extracted from the session and passed as props to the Protected component. If no user session is found, the sign-in link is displayed to prompt the user to authenticate.

Conclusion

Authentication and authorization remain crucial parts of the application development process. NextAuth.js provides various easy ways to integrate authentication into our applications. In this article, we have implemented authentication with NextAuth.js using Github OAuth, a credentials provider, and have also seen how we can use middleware in Nextjs to authorize our users. Thanks for reading this far. Happy coding!!!

Secure Your Front-End: Detect, Fix, and Fortify

Spot abnormal user behaviors and iron out the bugs early with OpenReplay. Dive into session replays and reinforce your front-end against vulnerabilities that hackers search for.

OpenReplay