Protecting routes with NextAuth in Nextjs
Tags: nextjs, nextauth, react
Published: 2023-01-04 23:16:47

I've been recently using [NextAuth](https://next-auth.js.org/getting-started/introduction) for authenticating users in my projects. NextAuth makes the task super simple and with minimal boilerplate, secure and completely serverless. Love it!

Now that the authentication process is handled with NextAuth, we'll most certainly need to protect some routes, maybe the user details, or any other route that we shouldn't let unauthenticated users access.

While I like protecting routes in Nextjs with its built in [middleware](https://nextjs.org/docs/advanced-features/middleware), this strategy [won't work](https://next-auth.js.org/configuration/nextjs#caveats) with NextAuth if you are using database sessions (only with JWT), or if you don't want to over complicate the process, this alternative will be perfect.


Time to get hands on

I expected that you already have your app with NextAuth, if you don't, check [this app](https://github.com/esponges/next-auth-protected-routes) repository and the [docs](https://next-auth.js.org/errors#signin_oauth_error%20%22ikm%22%22%20must%20be%20an%20instance%20of%20Uint8Array%20or%20a%20string), it shouldn't take you more than 5 minutes.

**The guard wrapper**

We'll create a wrapper component that will use the session status with `useSession` to determine whether the user should be allowed to access, or not.

```tsx
// components/layouts/protectedLayouts.tsx

import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';

type Props = {
  children: React.ReactElement;
};

/*
  add the requireAuth property to the page component
  to protect the page from unauthenticated users
  e.g.:
  OrderDetail.requireAuth = true;
  export default OrderDetail;
 */

export const ProtectedLayout = ({ children }: Props): JSX.Element => {
  const router = useRouter();
  const { status: sessionStatus } = useSession();
  const authorized = sessionStatus === 'authenticated';
  const unAuthorized = sessionStatus === 'unauthenticated';
  const loading = sessionStatus === 'loading';

  useEffect(() => {
    // check if the session is loading or the router is not ready
    if (loading || !router.isReady) return;

    // if the user is not authorized, redirect to the login page
    // with a return url to the current page
    if (unAuthorized) {
      console.log('not authorized');
      router.push({
        pathname: '/',
        query: { returnUrl: router.asPath },
      });
    }
  }, [loading, unAuthorized, sessionStatus, router]);

  // if the user refreshed the page or somehow navigated to the protected page
  if (loading) {
    return <>Loading app...</>;
  }

  // if the user is authorized, render the page
  // otherwise, render nothing while the router redirects him to the login page
  return authorized ? <div>{children}</div> : <></>;
};

```

This wrapper will allow authenticated users to see its children contents or redirect the user if he's not.

The subscription in the dependency array of the `useEffect` hook will make sure that we check this logic every time that there's a navigation or session update.

I've also added a `loading` return statement for the cases where the user reloads or navigates directly to the page so he doesn't get kicked out of the page before being sure about his session status.

The key is that this wrapper will only wrap the components that should be protected. Other components won't be affected by it. How can we make this while not having to manually wrap component by component and thus keeping our code [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)er?

By using the `_app.tsx` component. Remember that this component runs in every route.

```tsx
// pages/_app.tsx

// add requireAuth to AppProps
type AppPropsWithAuth = AppProps & {
  Component: {
    requireAuth?: boolean;
  };
};


export default function App({ Component, pageProps }: AppPropsWithAuth) {
  return <SessionProvider session={pageProps.session}>
    {Component.requireAuth ? (
      <ProtectedLayout>
        <Component {...pageProps} />
      </ProtectedLayout>
    ) : (
      <Component {...pageProps} />
    )}
  </SessionProvider>;
}
```

In the `return` statement from `App` by using a ternary the `ProtectedLayout` component will only wrap on the components where we add the `requireAuth` property. 

I've already updated the `AppProps` so the component will accept this property and the compiler don't scream at us. If you don't use TS this is not necessary.

**The protected components**

Now, in any component where we want it to be protected we should add this property with a `true` value like so:

```tsx
// pages/protected/index.tsx

import { useSession } from 'next-auth/react';

const ProtectedPage = () => {
  const { data: session } = useSession();
  return (
    <div>
      <h1>Protected Page</h1>
      <p>Hi {session?.user?.name}!</p>
    </div>
  );
};

// add the requireAuth property to the page component
ProtectedPage.requireAuth = true;

export default ProtectedPage;
```

If we don't add this property, the ternary logic will be false (since `.requireAuth` is undefined) and the user will be free to navigate to that path regardless of his session status.

**Redirecting after authentication**

Finally, we'll make use of the `redirectUrl` query param that it's passed to the redirection route when the user is not authorized.

We'll create a custom login page and override default one from NextAuth.

Our `signIn` component could be something like this:

```tsx
// pages/auth/signIn/index.tsx
import { signIn, useSession } from 'next-auth/react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';

const SignInPage = () => {
  const [isRedirecting, setIsRedirecting] = useState(false);
  const { data: session } = useSession();
  const router = useRouter();
  
  useEffect(() => {
    if (session && !isRedirecting && router.isReady) {
      // display some message to the user that he is being redirected
      setIsRedirecting(true);
      setTimeout(() => {
        // redirect to the return url or home page
        router.push(router.query.returnUrl as string || '/' );
      }, 2000);
    }
  }, [session, isRedirecting, router]);

  const handleSignIn = () => {
    signIn('discord');
  };

  return (
    <div>
      <h1>Sign In</h1>
      <p>Sign in to access protected pages</p>
      {session ? (
        <div>
          <p>Currently signed in as {session?.user?.name}</p>
          <p>Redirecting to home page...</p>
          <Link href='/'>
            <button>Go to home</button>
          </Link>
        </div>
      ) : (
        <button onClick={handleSignIn}>Sign In</button>
      )}
    </div>
  );
};

export default SignInPage;
```

This page will have two uses, the first one, is of course displaying our login providers options to the user. 

And second, NextAuth will redirect the user back to this login page, and we'll pick the `router.query.returnUrl` string and if present, redirect the user to the page that he was trying to access before.

Remember the useEffect from `components/layouts/protected.tsx`?

Don't forget to update the pathname to our new sign in page — `/auth/signIn`.

```tsx
  useEffect(() => {
    // check if the session is loading or the router is not ready
    if (loading || !router.isReady) return;

    // if the user is not authorized, redirect to the login page
    // with a return url to the current page
    if (unAuthorized) {
      console.log('not authorized');
      router.push({
        pathname: '/auth/signIn',
        query: { returnUrl: router.asPath },
      });
    }
  }, [loading, unAuthorized, sessionStatus, router]);
```

We also must pass the new sign in path to `[...nextAuth].ts` for the override.

```ts
// [...nextauth].ts
export default NextAuth({
  // ...
  pages: {
    signIn: '/auth/signin',  // Displays signin buttons
  },
  // ...
})
```

And that was it!

If you want to see the full code of the sample app please check [the original code](https://github.com/esponges/next-auth-protected-routes).

Sources: 

- https://next-auth.js.org/getting-started/introduction
- https://nextjs.org/docs/advanced-features/middleware
- https://dev.to/ivandotv/protecting-static-pages-in-next-js-application-1e50
- Photo from <a href="https://unsplash.com/@tobiastu?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Tobias Tullius</a> at <a href="https://unsplash.com/es/fotos/4dKy7d3lkKM?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>