GSD

A Complete Guide to Next.js Authentication

Posted by Nefe Emadamerho on September 20, 2023

An important aspect of app development is ensuring that apps are secure and protected against data breaches and other cybercrimes. One way you can secure your applications is by carrying out authentication. Authentication is critical, as developers must ensure that their apps are secure and can only be accessed by verified users.

Next.js is a React framework for developing web applications with ease. It has many incredible features like file-based routing, server-side rendering, image optimization, prefetching, dynamic components, static exports, dynamic components, and built-in CSS and Sass support.

In this article, you will learn about Next.js authentication, the different authentication patterns for Next.js authentication considerations, and how to authenticate Next.js applications with NextAuth.

What does authentication mean for Next.js?

To understand what authentication means for Next.js, you must first understand how Next.js renders things. 

With Next.js, three types of rendering are available, and they are:

In client-side rendering (CSR), the browser makes an initial request to a content delivery network (CDN), and only the barebones static HTML and CSS for a page are rendered by the server. The logic, data fetching, templating, and routing required to display content on the page are handled by JavaScript code that executes in the browser/client. CSR became popular as a method of building single-page applications.

CSR is one of the best choices for a PWA serving dynamic content. An example of a client-side rendered website is Facebook, where the page automatically refreshes itself with new data without the user having to send a request.

Server-side rendering (SSR) is one of the oldest methods of rendering web content. With SSR, the full HTML for the page content is rendered on the server upon a user’s request and then sent to the browser. This method of rendering is appropriate for dynamic content that changes frequently.

SSR pages have a higher search engine optimization (SEO) performance. They rank higher in Google because they are more accessible to search engine bots and crawlers and are indexed faster than CSR pages. It also improves performance as the page will render faster on the browser because the content has already been rendered on the server.

A downside of SSR is that it is more complex to configure your cache on SSR sites than on CSR sites. Also, SSR often requires a bigger and more powerful server to perform better than CSR.

Static site generation (SSG)

Like SSR, static site generation builds HTML on the server, but instead of rendering on each request, it renders static HTML at build time. The rendered HTML is then renewed on each request. With this method, the content is created just once and reused on every request to the page. SSG is the rendering method recommended by the Next.js team.

SSG is most suitable for use on any page where you have to pre-render data that can be generated before a user request takes place. It is ideal for pages where you want to present static content or provide excellent SEO capabilities. Examples of such pages are blogs or marketing sites containing content from a headless CMS, which is not updated very often. You can also use it for e-commerce product listings, help pages, and documentation.

Some advantages of using SSG are that you can boost performance using CDN caching, your page loads much faster than a server-side rendered page, and the backend serves only static files, which reduces the load on the server.

With SSG, if your content changes, you always have to trigger a rebuild for the changes to reflect on your frontend, and the build time may be very long if you have a large website.

Netlify diagram of static site generation

Source: Netlify

Authentication for Next.js means you can leverage the rendering patterns and use them to secure an application on the client, the server, or both. Not only that, but you can also secure API routes and create protected pages. This way, you have several ways to safeguard against hacks while creating a unique experience for the user.

Common Next.js authentication patterns

As a full-stack framework, Next.js is more flexible when it comes to authentication than React, where authentication takes place only on the client-side (browser).

Next.js has three patterns of authenticating apps: client-side, server-side, and API route authentication. It is important to choose the patterns that best fit your use case and application requirements.

Client-side authentication

Client-side authentication is when the authentication process is performed in the browser. With this pattern, the authentication logic takes place in the browser after the page has loaded. Developers use a loading animation or skeleton to show that the authentication check is ongoing in the background. 

If the check passes, the user is taken to their profile page or dashboard, and if the check fails, they are redirected to the login page and will see errors explaining why the check failed.

In addition to being able to deploy the app onto a CDN, this pattern ensures that users will see something rendered immediately on the screen due to non-blocking rendering.

This pattern has the disadvantage of displaying secure content to an unauthenticated user.

Here’s some sample code of how client-side authentication works:

export default function UserProfile() {
  // Fetch the user on the client-side
  const { user } = useUser({ redirectTo: "/login" });
 
  // Server-render loading skeleton
  if (!user) {
    return <LoadingSkeleton />;
  }
  // Once the user request finishes, show the user's name
  return <p>{user.name}</p>;
}

The code fetches the user on the client and shows a loading skeleton while that goes on. Once the fetch is complete, we render the details of the signed-in user. 

Banner CTA: See how Butter's simple content API works with your Next.js app

Server-side authentication

In this pattern, the request is sent from the browser to a server-side page on our Next.js app. The SSR page calls a backend API to authenticate the user, and if there is a valid user, the server pre-renders the requested page on the backend and sends it to the browser. If the user is not an authenticated one, they are redirected to the login page.

Unlike client-side authentication, with this pattern, there will be no flash of unauthenticated content (FOUC) or a need to use a loading indicator.

One disadvantage is that the rendering will be blocked until there is a response from the backend API handling the authentication. This is because getServerSideProps is blocking until the request resolves. So if the authentication API is slow, it will result in slow page load times and an unpleasant experience for the user.

 Here’s some sample code of how server-side authentication works:

export default function UserProfile({ user }) {
  return <p>{user.name}</p>;
}
 
export async function getServerSideProps({ req, res }) {
  const session = await getUserSession(req);
  if (!session) {
    //If no user, redirect to login page
    res.writeHead(307, { Location: "/login" });
    res.end();
    return { props: {} };
  }
 
  //If there is a session, return the current session
  return {
    props: { session },
  };
}

The core authentication logic goes on in getServerSideProps. The code checks if there is a user session and, if none, redirects the user to the login page. If there is a session, you pass that session data to the client and render the user’s name in the browser.

Page authentication

When building applications, there are specific pages only authenticated users should be able to access, like www.myapp.com/profile. A use case for page authentication is a college website where certain pages can only be accessed by professors and admins.

You will learn how to implement page authentication when you work with NextAuth later in the article.

API routes authentication

Next.js API routes are Node.js serverless functions that we can add to our applications. Next.js uses the file system to treat files inside the pages/api folder as API endpoints. This means that if we have a pages/api/user.js file, Next.js will convert it to http://localhost:3000/api/user, which you can access and get a response from. Similar to pages, you can also set up authentication for them.

Let’s see some sample code of how that works.

export default async (req, res) => {
  const session = await getAuth();
 
  if (session) {
    res.send({
      content: "Welcome to the secret page",
    });
  } else {
    res.send({
      error: "You need to sign in to use this endpoint.",
    });
  }
};

To protect this API endpoint, the code checks if there is a user session and if there is none, it returns an error message. This way, only an authenticated user can access the API route.

What is NextAuth, and what does it do for us?

Authentication is complex. There are several things to consider, like the right authentication methods to use—whether it’s magic links, OTPs, phone calls, email and password, or social sign-ins such as Google, Facebook, or GitHub buttons. We will also have to set up a database where we will store authenticated users. Managing all these ourselves will be expensive and time-consuming; it’s a full-time job in itself. 

An easier and more effective option is to leave the responsibility of authentication to third-party libraries and solutions, often called ‘authentication providers’ or ‘identity providers’. 

One such library is NextAuth, a full-stack and easy-to-implement open-source authentication library for Next.js. It can be used with or without a database and has default support for popular databases such as MySQL, MongoDB, PostgreSQL, and MariaDB. NextAuth comes with support for over 50 authentication providers, including GitHub, Google, Facebook, Coinbase, and many more.

Tutorial: Authenticating a Next.js app with NextAuth

For this tutorial, you will learn how to authenticate a Next.js app with NextAuth, using GitHub’s social sign-in as our OAuth provider.

Prerequisites

The following are required to follow along with this tutorial:

  • Knowledge of JavaScript, React, and Next.js
  •  ClientID and Client Secret credentials from our GitHub account

Getting started

Create a Next.js project by running the command below in our terminal.

npx create-next-app butter-next-auth

Next, navigate into the project directory. 

cd product-review-app

Then, run the command below to start the application.

 npm run dev

We will use NextAuth later in the article, so install that also.

npm i next-auth

Creating the dynamic API route for NextAuth

Create a file called [...nextauth].js in the pages/api/auth directory and paste the following code:

import NextAuth from "next-auth";
import Providers from "next-auth/providers";
const options = {
  providers: [
    Providers.GitHub({
      clientId: "your-github-client-id",
      clientSecret: "your-github-client-secret",
    }),
  ],
};
export default (req, res) => NextAuth(req, res, options);

Here, we did the following:

  • Added the dynamic route required by NextAuth. By adding this route, all requests to /api/auth/signIn, /api/auth/signOut, /api/auth/callback, etc will automatically be handled by NextAuth.
  • Imported the Providers module from next-auth, which is used to configure the different providers NextAuth supports.
  • Configured GitHub as our OAuth provider by passing in our GitHub clientId and clientSecret. NextAuth supports several other built-in providers, but we used GitHub for this article’s demo.

Integrating NextAuth into the application

To integrate NextAuth into the application, navigate to the pages/_app.js file and update it with the code below:

import "../styles/globals.css";
import { Provider } from "next-auth/client";
 
function MyApp({ Component, pageProps }) {
  return (
    <Provider session={pageProps.session}>
      <Component {...pageProps} />
    </Provider>
  );
}
 
export default MyApp;

Here, you imported Provider from the next-auth client module and used it to wrap the root of our application. This way, you can use NextAuth throughout the application. NextAuth uses React’s Context API to share the logged-in user session across all components. This improves performance, reduces network calls, and avoids page flickers when rendering.

Creating the signup page

Navigate to the pages/index.js file and update it with the code below.

import Head from "next/head";
import { signIn, signOut, useSession } from "next-auth/client";
 
export default function Home() {
  const [session] = useSession();
  return (
    <div>
      <Head>
        <title>NextAuth Example</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
 
      <main>
        {!session && (
          <>
            <h1> Sign in to continue</h1>
            <button onClick={signIn}>Sign In</button>
          </>
        )}
        {session && (
          <>
            <h1>Successfully signed in as {session.user.email}</h1>
            <button onClick={signOut}>sign out</button>
          </>
        )}
      </main>
    </div>
  );
}

Here, you did the following:

  • Imported signIn, signOut, and useSession from the next-auth client module. The signIn method is used to sign a user into the application, and the signOut method signs them out. 
  • The useSession hook is the easiest way to check if someone is signed in. It provides a session object containing the data of the signed-in user.
  • In the HTML <main> tag, you performed two conditional renderings. First, if there is no session (i.e., no signed-in user), render a CTA and a button for the user to sign in. Secondly, if there is a session (i.e., a signed-in user exists), access the user email from the session object and provide a button so they can sign out.

With that, you have successfully authenticated a Next.js application with NextAuth and GitHub, and the images below show the authentication flow.

First step of authentication flow: Sign in page.

Second step of authentication flow: Sign into GitHub

Third step of authentication flow: Authorize ButterCMS NextAuth

Fourth step of authentication flow: Successful login page

Creating a Protected Route

You can take the authentication one step further by creating a protected route or page that can only be accessed by a logged-in user. Create a file called pages/protected-route.js and update it with the code below.

import { getSession, useSession } from "next-auth/client";
 
export default function ProtectedRoute() {
  const [session] = useSession();
  return (
    <div>
      <h1>Welcome to the protected route {session.user.name}</h1>
    </div>
  );
}
 
export async function getServerSideProps(context) {
  const session = await getSession(context);
  if (!session) {
    return {
      redirect: {
        destination: "/",
      },
    };
  }
  return {
    props: {
      session: session,
    },
  };
}

Here, you did the following:

  • Imported getSession and useSession from next-auth/react.
  • In the getServerSideProps, we used the getSession method to check if a session exists (i.e., if there is a logged-in user). The user will be redirected to the login page if there is no session. However, if there is a session, you return that session.

With that, you have set up a protected route that can only be accessed by logged-in users.

If non-authenticated users navigate to that route, they are redirected to the login page. However, if an authenticated user navigates to that route, they will see the image below.

Protected route welcome page

Authenticating API Routes

Authenticating Next.js API routes with NextAuth is straightforward. Let’s see how it works. Create a pages/api/secure-api.js file and paste the code below.

import { getSession } from "next-auth/client";
 
export default async (req, res) => {
  const session = await getSession({ req });
 
  if (session) {
    res.status.json({
     	name: "John Doe",
	message: "Welcome authenticated user",
    });
  } else {
    res.status(403).json({
      error: "You must sign-in to view the content on this page.",
    });
  }
};

Here, you did the following:

  • Imported  getSession from next-auth/client module
  • Trapped the incoming request in a session variable
  • Checked if a session exists or not. If it exists, you returned a dummy JSON response containing a success message and a name. However, in real-world applications, you would provide meaningful data. If a session doesn’t exist, you returned an error.

Creating a custom NextAuth sign-in page

By default, NextAuth.js comes baked with a generic and minimal user interface that shows sign-in options based on the authentication providers we supplied during configuration. 

However, there are circumstances where you would prefer to create a custom login page that matches the identity and representation of your brand, and NextAuth allows you to do that.

To create a custom page, you need to make some updates to the [...nextauth].js file in the pages/api/auth directory.

import NextAuth from "next-auth";
import Providers from "next-auth/providers";
 
const options = {
  providers: [
    Providers.GitHub({
      clientId: "your-github-client-id",
      clientSecret: "your-github-client-secret",
    }),
  ],
  pages: { //added
    signIn: "/sign-in",
  },
  database: {
    type: "sqlite",
    database: ":memory:",
    synchronize: true,
  },
};
 
export default (req, res) => NextAuth(req, res, options);

The page you defined, signIn: "/sign-in" means that you must have a sign-in page in the app, so let’s set that up.

Create a pages/sign-in.js file and paste the code below.

import { signIn } from "next-auth/client";
 
export default function CustomSignInPage() {
  return (
    <div>
      <h1>Welcome to your custom sign-in page</h1>
      <button onClick={signIn}>Cool Custom GitHub Button</button>
    </div>
  );
}

Here, you imported the signIn method from NextAuth and passed it to the button’s onClick event handler.

With that, you get a cool branded sign-in page instead of the generic one. Beyond creating custom sign-in pages, you can also create pages for signing out, displaying errors, verifying users, and new user authentications.

Welcome to your custom sign in page

Storing signed-In user data

While NextAuth is a great library that makes authenticating Next.js applications a breeze, it is not a full-fledged solution. One thing it does not handle is the storing of signed-in user data. You may not need to store user data for internal tools, but it is important for customer-facing applications where you need to persist user info (e.g., for billing, to contact customers, etc.).

You have to set up a database yourself and store the user info. NextAuth supports databases like MySQL, Postgres, MongoDB, SQLite, and compatible databases (e.g., MariaDB, Amazon Aurora, Amazon DocumentDB). It also provides an Adapter API that allows you to connect it to any database of your choice. You can get more info on the list of official adaptersBanner CTA: See how Butter's simple content API works with your Next.js app

Questions about Next.js authentication

Should you build your own authentication system?

Building a functional and robust authentication system is hard. Here are some things to consider when deciding if we should build an in-house authentication system or use a third-party service.

  • Business priorities and goals: While you could create a custom solution, it is not advisable to do so if it does not meet the bottom line of your business. For example, a ride-hailing business could spin up a custom authentication solution, but in doing so, they have diverted their energy to develop projects that don’t meet their needs as a business or drive productivity in the right direction.
  • Technical expertise and resources: Building great products takes time, and the same holds true for security software. You may need to invest a large amount of time to build a secure and hacker-proof authentication system. Not only that, but authentication solutions require heavy technical expertise, which is expensive to get. Also, the continual management and upgrading will drive costs up. Depending on the number of users the solution will authenticate, a team of specialists may be required, which implies higher operational costs.
  • Scale and complexity: Applications often start simple and grow complex over time. The authentication solution may start with email and password login, but over time more options may be added, like adding passwordless logins through social sign-ins or magic links to accommodate more users. Not only do these new features increase costs, but they also add to the complexity of the project. The cost of maintaining your own identity management may be much higher than you’re expecting.
  • Regulatory compliance: The identity industry is heavily regulated because it involves the processing and storage of confidential user data. Building with compliance in mind is one more consideration that will increase the complexity of the building process, and these regulations are always evolving.

I recommend that you do not build your own system unless you are going into the identity management system and building an Authentication-as-a-Service (AaaS) business like Auth0, Okta, or Ory. Where possible, build on top of existing solutions and ship faster with industry-trusted experts.

What is the right pattern to authenticate Next.js applications?

When it comes to programming, there is no one-size-fits-all response, so the best answer to questions like this is usually ‘it depends’. Neither pattern is wrong, so there is no ‘right pattern’. Your focus should be on making sure that your applications are fully secure.

Which third-party authentication provider should I use?  

Some great third-party solutions businesses love, and you can choose, are Okta, Ory, Auth0, NextAuth, and LoginRadius.

You should consider the following when deciding what third-party service to use:

  • Available sign-in methods: The right solution should give your users the freedom to choose which identity provider they use to sign in to your application. Some basic sign-in methods are email and password, social login, and magic links.
  • Database availability: Some solutions like Auth0 come with a database where you can store the data of your authenticated users. Others like NextAuth do not provide any database, meaning you would have to handle that yourself.
  • Features: When choosing, you should look out for features like multi-factor authentication (MFA), single sign-on (SSO), analytics and reports system, and user management. Generally, the more features a provider supports, the better, particularly if you intend on sticking with them for a long time.
  • Pricing: No matter how great a provider is, they must fall within your budget. Pricing is an important factor, particularly considering the scale of your application and the number of users you plan to authenticate.
  • Technical support: What kind of support do they offer? This is important because services can fail at any time of the day. What is their response time, and how long does it take to resolve a problem or fix a bug? What communication lines do you provide (e.g., email, video calls, etc.), and are you satisfied with those channels?
  • Documentation and learning curve: No matter the provider you choose, one thing they will all have in common is that developers are the ones who will handle the integration. The documentation must be easy to navigate and clear. A great product that is difficult to operate is of no use.
  • Integration points: APIs are the most popular way of integrating third-party services. However, SDKs and libraries that support different languages also significantly improve the developer experience because they enable speedy integration. The faster you can integrate, the better, particularly for fast-paced businesses.
  • Tech stack: Any service that limits your developers is not the right one for your product. The provider you pick should complement and integrate smoothly with your tech stack and enable you to do your best work, not fight against it.

Closing Thoughts

In this article, you learned about Next.js’ rendering methods, authentication patterns, and how to set up authentication with NextAuth. A great thing about Next.js is that you are free to choose the authentication pattern that fits your requirements. By pairing it with NextAuth, you'll get a seamless and hassle-free authentication for your applications.

The process of integrating third-party identity vendors like GitHub, Facebook, etc., is straightforward, empowering users to choose their preferred provider and removing every restriction. And as you've seen in the tutorial above, Next.js and NextAuth are two excellent and well-thought-out technologies to work with. 

To dive deeper into these concepts please refer to the following resources:

Get the latest Butter product updates and tutorials delivered directly to your inbox.
    
Nefe Emadamerho

Nefe is a developer and technical writer who enjoys learning new things and sharing his knowledge with others.

ButterCMS is the #1 rated Headless CMS

G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award

Don’t miss a single post

Get our latest articles, stay updated!