54
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.
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;
setAuthCookie
behaves like this:
- The request
req
must bePOST
request. - The request body must contain two elements: a
session
and anevent
. - The
session
contains session data (as is provided bysupabase.auth.session()
for example). - The
event
is eitherSIGNED_IN
indicating a sign in orSIGNED_OUT
indicating a sign out.
Getting this data is easy.
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 }),
});
}
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
.
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: {} };
}
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;
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();
When we go to /protected
now, we are either redirected to /signin
when we are not signed in or the ProtectedPage
is rendered.
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 🙏
When I started out with Supabase, I read:
It's a great post and the first time I saw the setAuthCookie
/getUserByCookie
combination. Give it a read, it's an excellent post!
54