Protected Routes with Supabase and Next.js
Tags: supabase, nextjs, react, auth
Published: 2021-12-03 12:44:02

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.

```typescript
// 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;
```

`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`).

```typescript
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 }),
    });
}
```

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`.

```typescript
export async function getServerSideProps({ req }) {
    const { user } = await supabase.auth.api.getUserByCookie(req);
    
    if (!user) {
        return { props: {}, redirect: { destination: '/signin' } };
    }
    
    return { props: {} };
}
```

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.

```typescript
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;
```

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

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

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

export const getServerSideProps = enforceAuthenticated();
```

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](https://twitter.com/SRuhleder) 🙏

## Credits

When I started out with Supabase, I read:

[Magic Link Authentication and Route Controls with Supabase and Next.js](https://dev.to/dabit3/magic-link-authentication-and-route-controls-with-supabase-and-next-js-leo) by [Nader Dabit](https://twitter.com/dabit3)

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