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;
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.
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 }),
});
}
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: {} };
}
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.
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!
Top comments (7)
Your article helped me out. Thank you for the taking the time.
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!
Hi Franz 👋
That's strange indeed! For me, this still works flawlessly...
Here are some things you can check:
supabase
client you use insupabase.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.updateSupabaseCookie
method defined in the post? This is necessary to set the cookie and if it's never set,getUserByCookie
will indeed returnnull
.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
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.
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.
Thanks,
I would definitely check out the video.
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.