✒️
Start your developer blog today!
✉️
Questions?
Email me

How to Add Global Authentication Checks in Next.js with NextAuth

Apr 17, 2021 4 min read

We often will want to redirect all pages to some login page if a user is unauthenticated (no valid session). There are some very solid, clever solutions for global authentication checks in Next.js, most notably the solutions proposed here (a post I refer back to from time to time).

The most notable takeway from this post is the utility of simple higher order components (HOC) to authenticate on the page-level.

I want to propose a similar solution for global authentication checks in Next.js using next-auth.

Specify Auth Requirements in Pages

The goal here is to perform as little authentication work on the page-level and to add the majority of the code in a reusable component.

For each page on which we want to force authentication, we will add a custom auth property to the component function.

// pages/protected.jsx
const Protected = () => {
  const [session] = useSession();
  return (
    <>
      <h1>A Protected Page</h1>
      <span>My name is {session.user.name}</span>
    </>
  );
};
Protected.auth = true;
export default Protected;

This is all we will need on the page-level.

Notice how we won’t need to access the loading variable of useSession(), since session will never be null while inside this component.

If session is null or loading is true, then we don’t want this component to be rendered at all, but rather redirected to a /login page.

Implement Auth Check

In another file (or even inside _app.jsx itself), we want to add our authentication check.

// components/auth.jsx
import { useRouter } from "next/router";
import { useSession } from "next-auth/client";
const Auth = ({ children }) => {
  const [session, loading] = useSession();
  const hasUser = !!session?.user;
  const router = useRouter();
  useEffect(() => {
    if (!loading && !hasUser) {
      router.push("/login");
    }
  }, [hasUser, loading]);
  if (loading || !hasUser)
    return <div>Waiting for session...</div>;
  return children;
};

We’ll first grab session and loading from useSession().

Whenever either session or loading changes, we want to check if we’ve finished loading (!loading) and have no session (!hasUser). In this case, we want to redirect to our /login page.

Notice that we’ll render a loading screen (Waiting for session...) in two scenarios:

  1. We are still loading (no final session value yet)
  2. The user is not authenticated yet (not needed, functions more as a safeguard)

If we are done loading, then two things can happen:

  1. The useEffect() hook will redirect us to the login page
  2. The user is authenticated, and the child component is properly rendered.

Conditionally Run Auth Check

We’ll first want to import this auth component into our _app.jsx.

We’ll wrap our <Component /> with the <Auth> component we wrote whenever the rendered component declares the auth flag to be true.

// pages/_app.jsx
import { Provider } from "next-auth/client";
import { Auth } from "@/components/auth";
const App = ({ Component, pageProps }) => (
  <Provider session={pageProps.session}>
    {Component.auth ? (
      <Auth>
        <Component {...pageProps} />
      </Auth>
    ) : (
      <Component {...pageProps} />
    )}
  </Provider>
);
export default App;

Possible Modifications

The beauty of this approach is that it is easily extendable to include page-level role-based access control (RBAC), custom routes, custom loading screens, etc.

For instance, in an authenticated page, we can change this added property to be an object that holds information specific to the page.

If we were designing a site for authors to create blog posts, we might want multiple roles.

  1. user: can read and subscribe to specific authors
  2. author: can post articles for users
  3. admin: can add and remove authors

We may want different roles to have access to different pages.

First, we would set the expected role for each authenticated page.

// pages/protected.jsx
Protected.auth = {
  role: "author",
};

Then, we would pass the required role for the currently accessed page.

// pages/_app.jsx
<Auth role={Component.auth.role}>
  <Component {...pageProps} />
</Auth>

Finally, we can make comparisons against the current user.

// components/auth.jsx
const Auth = ({ children, role }) => {
  useEffect(() => {
    if (!loading) {
      if (!hasUser || !hasAccess(session, role)) {
        router.push("/login");
      }
    }
  }, [hasUser, loading]);
};

More JavaScript Articles