How to protect routes in Next.js next-auth?
Asked Answered
P

3

22

I am trying to integrate authentication with next-auth library in an Application. I have been following the official tutorial given here https://github.com/nextauthjs/next-auth-example/. The problem with the given example is that I need to check if there is a session on every page that requires authentication like this.

    import { useState, useEffect } from 'react';
    import { useSession } from 'next-auth/client'
    
    export default function Page () {
      const [ session, loading ] = useSession()
      
      // Fetch content from protected route
      useEffect(()=>{
        const fetchData = async () => {
          const res = await fetch('/api/examples/protected')
          const json = await res.json()
        }
        fetchData()
      },[session])
    
      // When rendering client side don't display anything until loading is complete
      if (typeof window !== 'undefined' && loading) return null
    
      // If no session exists, display access denied message
      if (!session) { return  <Layout><AccessDenied/></Layout> }
    
      // If session exists, display content
      return (
        <Layout>
          <h1>Protected Page</h1>
          <p><strong>{content || "\u00a0"}</strong></p>
        </Layout>
      )
    }

or like this for Server-side checking

    import { useSession, getSession } from 'next-auth/client'
    import Layout from '../components/layout'
    
    export default function Page () {
      // As this page uses Server Side Rendering, the `session` will be already
      // populated on render without needing to go through a loading stage.
      // This is possible because of the shared context configured in `_app.js` that
      // is used by `useSession()`.
      const [ session, loading ] = useSession()
    
      return (
        <Layout>
          <h1>Server Side Rendering</h1>
          <p>
            This page uses the universal <strong>getSession()</strong> method in <strong>getServerSideProps()</strong>.
          </p>
          <p>
            Using <strong>getSession()</strong> in <strong>getServerSideProps()</strong> is the recommended approach if you need to
            support Server Side Rendering with authentication.
          </p>
          <p>
            The advantage of Server Side Rendering is this page does not require client side JavaScript.
          </p>
          <p>
            The disadvantage of Server Side Rendering is that this page is slower to render.
          </p>
        </Layout>
      )
    }
    
    // Export the `session` prop to use sessions with Server Side Rendering
    export async function getServerSideProps(context) {
      return {
        props: {
          session: await getSession(context)
        }
      }
    }

This is a lot of headaches as we need to manually right on every page that requires auth, Is there any way to globally check if the given route is a protected one and redirect if not logged in instead of writing this on every page?

Politicking answered 16/5, 2021 at 19:8 Comment(0)
P
53

Yes you need to check on every page and your logic is okay ( showing spinner untll the auth state is available) however,you can lift authentication state up, so you don't repeat the auth code for every page, _app component is a perfect place for this, since it naturally wraps all other components (pages).

      <AuthProvider>
        {/* if requireAuth property is present - protect the page */}
        {Component.requireAuth ? (
          <AuthGuard>
            <Component {...pageProps} />
          </AuthGuard>
        ) : (
          // public page
          <Component {...pageProps} />
        )}
      </AuthProvider>

AuthProvider component wraps logic for setting up third party providers (Firebase, AWS Cognito, Next-Auth)

AuthGuard is the component where you put your auth check logic. You will notice that AuthGuard is wrapping the Component (which is the actual page in Next.js framework). So AuthGuard will show the loading indicator while querying the auth provider, and if auth is true it will show the Component if auth is false, it could show a login popup or redirect to the login page.

About Component.requireAuth this is a handy property that is set on every page to mark the Component as requiring auth, if that prop is false AuthGuard is never rendered.

I've written about this pattern in more detail: Protecting static pages in Next.js application

And I've also made an example demo app (source)

Pictor answered 17/5, 2021 at 12:5 Comment(10)
Just wanted to say, this answer is fantastic and is exactly what I've been searching for for a very long time. Thanks @Ivan!Isabellisabella
@J.Jackson Thanks! Make sure to check out the repository code I'm keeping it updated.Pictor
yep that's exactly what I've been looking over! Working on removing some of the hard-coded auth.ts stuff and fixing some TS strict errors I'm seeing.Isabellisabella
What is the differnce between doing Component.requireAuth and using the get staticProps function?Archambault
@Archambault requireAuth determines if the component (page) should be checked for authPictor
@IvanV. I understand that, but im just wondering where that key (requireauth) on a component is coming from? does it have any relation to getStaticProps?Archambault
@Archambault No, requireAuth is a key that you add to your component (page), and if the key exists that means that the component is protected. It has no relation to getStaticPropsPictor
thanks, its just something custom you have addedArchambault
I guess these days you could also solve this by middleware, introduced in Next 12.0 ?Scottie
One pattern i'm exploring as well is checking adding routes that should not be validated to an array and checking if the current route is on that list or not. If its not on the list, i know it should be validated.Schuller
B
7

I'm late to the party, but I ran into this issue myself a few days ago. Since I'm on Next.js v12.2+, I (and next-auth) have access to middleware. I'll post my middleware code below.

There's a caveat though: next-auth only supports middleware auth checks with the JWT strategy. However, I use sessions; not JWTs. The caveat is that I don't actually validate the session token. The thing is: I don't need to. I validate the token in the backend when I'm fetching data, as all the user data is retrieved through that token. This middleware only does one thing, and that's redirect unauthenticated users to the login page if they're trying to access a page that requires authentication. A tech savvy user might add a cookie themselves and would be able to reach the page, but that doesn't matter, as they'd get a 403 or similar because the data fetching would fail (the token is invalid so there's no user data that can be fetched).

I think this approach is faster because you don't have to fetch the page (and render it if it's not static) you go to, do a request to your backend to fetch the session data, do a call to your store (db, redis, whatever), get the data back and then determine the session's invalid, before you redirect the user to the login page. With this, before the page is even touched, the middleware already knows there's no session, so it (almost) instantly redirects the user to the login page.

Here's the code:

import { withAuth } from 'next-auth/middleware';

const publicFileRegex = /\.(.*)$/;
const anonymousRoutes = ['/', '/login', '/register', '/auth/error', '/auth/verify-request']; // The whitelisted routes

export default withAuth({
    callbacks: {
        authorized: ({ req }) => {
            const { pathname } = req.nextUrl;

            // Important! The below only checks if there exists a token. The token is not validated! This means
            // unauthenticated users can set a next-auth.session-token cookie and appear authorized to this
            // middleware. This is not a big deal because we do validate this cookie in the backend and load
            // data based off of its value. This middleware simply redirects unauthenticated users to the login
            // page (and sets a callbackUrl) for all routes, except static files, api routes, Next.js internals,
            // and the whitelisted anonymousRoutes above.
            return Boolean(
                req.cookies.get('next-auth.session-token') || // check if there's a token
                    pathname.startsWith('/_next') || // exclude Next.js internals
                    pathname.startsWith('/api') || //  exclude all API routes
                    pathname.startsWith('/static') || // exclude static files
                    publicFileRegex.test(pathname) || // exclude all files in the public folder
                    anonymousRoutes.includes(pathname)
            );
        },
    },
    // If you have custom pages like I do, these should be whitelisted!
    pages: {
        error: '/auth/error',
        signIn: '/login',
        verifyRequest: '/auth/verify-request',
    },
});

Credit partly goes to this discussion: https://github.com/vercel/next.js/discussions/38615

Buckden answered 25/9, 2022 at 14:57 Comment(0)
D
1

you can use middleware.ts or middleware.js file. Create a middleware.js file in your root directory and paste the following code.

export { default } from 'next-auth/middleware'


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

In the above file, you can specify the routes that you wanted to protect. If you don't export config, the middleware will apply for all the routes present in the application. It checks for the session while routing to each page.

And ofcourse, you need to have [...nextauth].js file in your api/auth directory. The file should contain providers and callbacks (session,jwt)

Diggins answered 25/6, 2023 at 6:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.