DEV Community

Sebastian Ruhleder
Sebastian Ruhleder

Posted on

Protected Routes with Supabase and Next.js

Some routes of your web application are meant for authenticated users only. For example, a /settings page can only be used if the user is signed in.

You could solve this client-side: Once the page renders, you check whether a user is signed in; if they are not, you redirect the user to the sign in page.

There is a problem with this, though. The page will start to render, so you either have to prevent everything from rendering until this check is done or you will see a partially rendered page suddenly redirected to the sign in page.

Luckily with Next.js, we can do this check server-side. Here's an outline of how we're going to do it:

  • Write an API route /api/auth to set a cookie based on whether a user signs in or out.
  • Register a listener with Supabase's onAuthStateChange to detect a sign in or sign out and call this API route.
  • Extract a function enforceAuthenticated to protect a route with one line of code.

Setting an Auth Cookie

Supabase provides a setAuthCookie function defined in @supabase/gotrue-js. This function takes a Next.js (or Express) request and response and sets or removes an auth cookie.

To make use of it, we introduce an API route /api/auth and simply call setAuthCookie, passing it the request and response objects.

// pages/api/auth.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { supabase } from './../../components/supabaseClient';

const handler = (req: NextApiRequest, res: NextApiResponse) => {
    supabase.auth.api.setAuthCookie(req, res);
};

export default handler;
Enter fullscreen mode Exit fullscreen mode

setAuthCookie behaves like this:

  • The request req must be POST request.
  • The request body must contain two elements: a session and an event.
  • The session contains session data (as is provided by supabase.auth.session() for example).
  • The event is either SIGNED_IN indicating a sign in or SIGNED_OUT indicating a sign out.

Getting this data is easy.

Updating the Auth Cookie

To keep the auth cookie up to date, we have to listen to changes in the authentication state of Supabase. On every change, we have to call the /api/auth endpoint to update the cookie accordingly.

For this, Supabase provides the onAuthStateChange function, which allows us to register a listener. This listener is called whenever a user signs in or out.

The following snippet should be used within the App component (usually _app.tsx or _app.jsx).

useEffect(() => {
    const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
        updateSupabaseCookie(event, session);
    });

    return () => {
        authListener?.unsubscribe();
    };
});

async function updateSupabaseCookie(event: AuthChangeEvent, session: Session | null) {
    await fetch('/api/auth', {
        method: 'POST',
        headers: new Headers({ 'Content-Type': 'application/json' }),
        credentials: 'same-origin',
        body: JSON.stringify({ event, session }),
    });
}
Enter fullscreen mode Exit fullscreen mode

The listener is passed two arguments when the authentication state changes: an event indicating whether the user signed in or out and the current session. This is exactly what the /api/auth endpoint needs to update the auth cookie. Using fetch, we send a simple POST request to it to reflect this change.

👉 I recommend extracting this code into a custom hook (which you can call useUpdateAuthCookie for example).

Changes in the authentication state in the frontend are now reflected in the auth cookie. Why do we update such a cookie? So we can use it server-side when using functions like getServerSideProps.

Protecting Routes

We can now protect a route by checking the auth cookie in getServerSideProps. If the user is signed in, we simply return; otherwise, we redirect the user to a sign in page.

Let's assume this sign in page can be found at /signin.

export async function getServerSideProps({ req }) {
    const { user } = await supabase.auth.api.getUserByCookie(req);

    if (!user) {
        return { props: {}, redirect: { destination: '/signin' } };
    }

    return { props: {} };
}
Enter fullscreen mode Exit fullscreen mode

Depending on how many routes you must protect, it's a good idea to extract this code and reuse it. For my projects, I use a function called enforceAuthenticated. This function takes an optional getServerSideProps function and delegates to it in the case that the user is signed in.

import { GetServerSideProps } from 'next';
import { supabase } from './supabaseClient';

const enforceAuthenticated: (inner?: GetServerSideProps) => GetServerSideProps = inner => {
    return async context => {
        const { req } = context;
        const { user } = await supabase.auth.api.getUserByCookie(req);

        if (!user) {
            return { props: {}, redirect: { destination: '/signin' } };
        }

        if (inner) {
            return inner(context);
        }

        return { props: {} };
    };
};

export default enforceAuthenticated;
Enter fullscreen mode Exit fullscreen mode

With this, quickly protecting a route becomes a one-liner:

// pages/protected.tsx
import enforceAuthenticated from '../components/enforceAuthenticated';

export default function ProtectedPage() {
    return <div>Protected Page</div>
}

export const getServerSideProps = enforceAuthenticated();
Enter fullscreen mode Exit fullscreen mode

When we go to /protected now, we are either redirected to /signin when we are not signed in or the ProtectedPage is rendered.

Recap

Here's what we did:

  • We created an API route /api/auth which updates an auth cookie based on a session and an event indicating a sign in or sign out.
  • We created a listener in the App component to send every update to the authentication state to the /api/auth endpoint, thereby updating the auth cookie.
  • In our server-side code, we used the getUserByCookie function to determine whether a user is signed in or out. Based on this, we either render the page or redirect the user to a sign in page.
  • We introduced a function enforceAuthenticated to reuse this functionality on as many routes as we want.

If you enjoyed this post, you can follow me on Twitter 🙏

Credits

When I started out with Supabase, I read:

Magic Link Authentication and Route Controls with Supabase and Next.js by Nader Dabit

It's a great post and the first time I saw the setAuthCookie/getUserByCookie combination. Give it a read, it's an excellent post!

Discussion (6)

Collapse
fb profile image
Franz Bewerunge • Edited on

Excellent Article!

However, are you sure this is still up to date? I always receive an empty user object when I call this on server side in getServerSideProps:

const { user } = await supabase.auth.api.getUserByCookie(req);

Looking into the supabase discussions forum this seems to be a known issue:

github.com/supabase/supabase/issue...

Any ideas what I might be doing wrong?

Thanks!

Collapse
sruhleder profile image
Sebastian Ruhleder Author

Hi Franz 👋

That's strange indeed! For me, this still works flawlessly...

Here are some things you can check:

  • Is the supabase client you use in supabase.auth.api.getUserByCookie the one using the anon key? I'm not sure if this also works with the service key, but I'm using the "regular" Supabase client that I also use in frontend calls.
  • Is the frontend calling the updateSupabaseCookie method defined in the post? This is necessary to set the cookie and if it's never set, getUserByCookie will indeed return null.

But as the issue suggests, there might still be something wrong on Supabase's side...

Thanks for reading, and hopefully this will resolve your problem!

Best,
Sebastian

Collapse
shar51 profile image
A. Ashar

Hi sebastian, great toturial.

One question, is there any way to use getServerSideProps to work with getStaticProps? I'm new to nextjs and it tells me i can't use it both.

Collapse
sruhleder profile image
Sebastian Ruhleder Author

Hi! 👋

getStaticProps is evaluated at build time, so it's for static content. getServerSideProps is evaluated every time the page is loaded, so it's for dynamic content.

Here's a good introduction on data fetching from the Next.js docs. I also found this video particularly helpful when I started out with Next.js.

Collapse
shar51 profile image
A. Ashar

Thanks,

I would definitely check out the video.

Thread Thread
sannajammeh profile image
Sanna Jammeh

NextJS edge middleware gives you this functionality. All authorization happens in the middleware and your page static renders as usual with getStaticProps. The middleware can prevent access to the static page.