🍿 7 min. read

Creating Protected Routes In NextJS With Supabase

Monica Powell

Supabase Auth Overview

Supabase has various methods in their JavaScript client library to handle user authentication and uses JSON Web Tokens (JWT) under the hood to manage authentication. If you want to learn more about how Auth works in Supabase check out the Supabase auth deep-dive video series. In order to have protected routes on our NextJS site, we'll need a way to register and authenticate users. We can perform these user actions and checks with the following methods from the Supabase Auth client. :

  • supabase.auth.signUp - We should give users the ability to create an account (covered in the first article on creating new Supabase users in NextJS)
  • supabase.auth.signIn - We need to give users the ability to sign-in. In this particular article, we'll cover the traditional method of using a username and password for sign-in but Supabase also supports other ways to log in, including OAuth providers (GitHub, Google, etc.) and magic links.
  • supabase.auth.user - We need a way to determine if a user is currently logged-in in order to ensure that logged-out users are not able to view pages that should only be accessible to logged-in users and that the proper information is displayed in various places like the site navigation.
  • supabase.auth.signOut - We should give users the ability to sign out and unauthenticate their session.

Create Protected Route

In order to create a protected route we need to have a particular page component we'd like to protect. For this example let's created a protected page at pages/protected.js that we can view at localhost:3000/protected when our site is running locally. This protected page will make a fetch request to a getUser API route to determine if there is currently an authenticated user loading the page. The API call should return the current user when there is one. We can then use this API response to redirect the page to the login page when there is no current user and only display user-specific information on the protected route when there is a user.

The API request can be made with getServerSideProps() which is a NextJS function that is called before a page renders. This allows us to redirect before the page renders based on the response from the getUser API call.

1import { basePath } from "../utils/siteConfig";
2
3export async function getServerSideProps() {
4 // We need to implement `/api/getUser` by creating
5 // an endpoint in `pages/api` but for now let's just call it
6 const response = await fetch(`${basePath}/api/getUser`).then((response) =>
7 response.json()
8 );
9
10 const { user } = response;
11
12 // If the `getUser` endpoint doesn't have a user in its response
13 // then we will redirect to the login page
14 // which means this page will only be viewable when `getUser` returns a user.
15
16 if (!user) {
17 return {
18 redirect: { destination: "/login", permanent: false },
19 };
20 }
21 // We'll pass the returned `user` to the page's React Component as a prop
22 return { props: { user } };
23}
24export default function Protected({ user }) {
25 return (
26 <p>
27 // Let's greet the user by their e-mail address
28 Welcome {user.email}!{" "}
29 <span role="img" aria-label="waving hand">
30 πŸ‘‹πŸΎ
31 </span>{" "}
32 </p>{" "}
33 You are currently viewing a top secret page!
34 );
35}

Now, we need to actually implement the getUser API route that the protected route is calling by creating a file pages/api/getUser.js. Within this file we will make a request to supabase.auth.user() which returns the current user when there is a user currently logged-in.

1import { supabase } from "../../utils/supabaseClient";
2
3export default async function getUser(req, res) {
4 const user = await supabase.auth.user();
5 return res.status(200).json({ user: user });
6}

The above code assumes that you've already set up a Supabase Client which we covered in the first post of this series. The Supabase client we are using in this instance looks like the below and uses environment variables to determine the Supabase DB URL and associated key:

1import { createClient } from "@supabase/supabase-js";
2
3const supabaseUrl = process.env.SUPABASE_URL;
4const supabaseKey = process.env.SUPABASE_KEY;
5
6export const supabase = createClient(supabaseUrl, supabaseKey);

You can retrieve the API key and database URL associated with your Supabase project from https://app.supabase.io/project/yourprojecturl]/settings/api which can be navigated to by going to your project > settings > API.

a screenshot of the Supabase settings page

Sign-in and Redirect to Protected Page

We'll allow folks to log in and log out of the site using the sitewide navigation. In order to show the appropriate links based on authentication status, we can use the state to track if a user is currently authenticated. As a default, we'll set authentication status to false so that the navigation defaults to thelogged-out view.

When a user is authenticated then we will show the Sign Out text in the nav:

If there is no authenticated user then we will link to the Sign-in and Sign Up pages:

1import Link from "next/link";
2import { useEffect, useState } from "react";
3
4export default function Header() {
5 const router = useRouter();
6 // Let's use state to track if a user is currently authenticated
7 // As a default we'll set this value to false so that the navigation defaults to thelogged-out view
8 const [isAuthed, setAuthStatus] = useState(false);
9
10 // We'll set up the nav, on mount to call the getUser endpoint we just
11 // created to determine if a user is currently logged-in or not
12 useEffect(() => {
13 fetch("./api/getUser")
14 .then((response) => response.json())
15 .then((result) => {
16 setAuthStatus(result.user && result.user.role === "authenticated");
17 });
18 }, []);
19
20 return (
21
22 <nav>
23 <div>
24 // If user is authenticated then we will show the Sign Out text
25 {isAuthed ? (
26 <span>
27 <h3>Sign Out &rarr;</h3>
28 </span>
29 ) : (
30 // If there is no authenticated user then we will link to the Sign-in and Sign Up pages
31 <>
32 <Link href="/signup">
33 <h3>Sign Up &rarr;</h3>
34 </Link>
35 <Link href="/login">
36 <h3>Login &rarr;</h3>
37 </Link>
38 </>
39 )}
40 </div>
41 </nav>
42 );
43}

When a user clicks "Sign In" from the nav we will navigate the user to the login page which contains a form to allow users to sign-in. The form will collect a user's email and password and on submit will fire a function signInUser which makes an API request to an API route for login and passes the email and password values from the form submit event to the API. If all goes well then we will receive a user object and can redirect (using NextJS's client-side router) to the /protected route that serves as a landing page for logged-in users.

1import { useRouter } from "next/router";
2
3export default function Form() {
4 const router = useRouter();
5 const signInUser = async (event) => {
6 event.preventDefault();
7
8 const res = await fetch(`/api/login`, {
9 body: JSON.stringify({
10 email: event.target.email.value,
11 password: event.target.password.value,
12 }),
13 headers: {
14 "Content-Type": "application/json",
15 },
16 method: "POST",
17 });
18
19 const { user } = await res.json();
20 if (user) router.push(`/protected`);
21 };
22
23 return (
24 <form onSubmit={signInUser}>
25 <label htmlFor="email">Email</label>
26 <input
27 id="email"
28 name="email"
29 type="email"
30 autoComplete="email"
31 required
32 />
33 <label htmlFor="password">Password</label>
34
35 <input
36 type="password"
37 id="password"
38 name="password"
39 required
40 />
41 <button type="submit">Login</button>
42 </form>
43 );
44}

The login API route will use supabase.auth.signIn to sign-in a user. If a user is successfully signed in then the API will return a 200 response, or else the API will return a 401 response. The form is not yet set up to handle this 401 response but ideally, we'd want to return some type of message to the user informing them that their credentials were invalid and prompt them to attempt to sign-in again or reset their password. However, as this app is currently being built the functionality to reset password does not yet exist so this error path cannot be fully handled yet.

1import { supabase } from "../../utils/supabaseClient";
2
3export default async function registerUser(req, res) {
4 const { email, password } = req.body;
5 let { user, error } = await supabase.auth.signIn({
6 email: email,
7 password: password,
8 });
9 if (error) return res.status(401).json({ error: error.message });
10 return res.status(200).json({ user: user });
11}

Sign Out and Redirect to Homepage

Let's update the Sign Out link in the header to be functional by creating a signOut function that fires on click of the Sign Out text.

1<span onClick={signOutUser}>
2 <h3>Sign Out &rarr;</h3>
3</span>

We'll also want to import a router from next/router to handle our client-side redirect.

1import { useRouter } from "next/router";

For signOutUser let's make a call to a logout API route that sets the authStatus to false when a user is successfully signed out. We also want to ensure that when a user is not logged-in they are not viewing an authenticated page by redirecting to the homepage if a user logs out on a page other than the homepage. Without explicitly redirecting to the homepage when a user signs out, the state of authStatus would change in the nav as well as the logged-in vs.logged-out specific text however, the actual page regardless of authentication would continue showing protected information for unauthenticated users which we don't want.

1const signOutUser = async () => {
2 const res = await fetch(`/api/logout`);
3 if (res.status === 200) setAuthStatus(false);
4 // redirect to homepage when logging out users
5 if (window.location !== "/") router.push("/");
6 };

Now we need to create the /api/logout route so that we can actually use it when the signOutUser function fires.

1import { supabase } from "../../utils/supabaseClient";
2
3export default async function logoutUser(req, res) {
4 let { error } = await supabase.auth.signOut();
5
6 if (error) return res.status(401).json({ error: error.message });
7 return res.status(200).json({ body: "User has been logged out" });
8}

Summary

So in conclusion, we created a protected route by creating a page component in NextJS that calls a getUser endpoint in getServerSideProps() and redirects to the log in page, instead of loading the protected route, when there is not a user returned. We also set up client-side routing to redirect users to /protected when they successfully logged-in and to the homepage / when they logged out. The core functionality to update and check authentication was handle in API routes using Supabase's various auth methods (signIn, signOut, user).

Example Code on GitHub

You can view the full source code for the example code at: https://github.com/M0nica/protected-routes-with-supabase-nextjs-example

Looking Ahead

I am looking forward to sharing more about the app development as I progress through my journey of developing Shine Docs . As I wrap up the authentication for this site I am considering adding additional functionality like magic links or other auth providers, which are natively supported by Supabase. Before I extend the auth functionality to support additional ways of authenticating I will need to update the site to give users the ability to reset their own password and better handle authentication errors to ensure that the sign-in (are the user credentials invalid? did something go wrong during sign-in?) and sign up (has an e-mail already been claimed? is a password not secure enough?) flow are as seamless as possible.

This article was published on March 13, 2021.


Don't be a stranger! πŸ‘‹πŸΎ

Thanks for reading "Creating Protected Routes In NextJS With Supabase". Join my mailing list to be the first to receive my newest web development content, my thoughts on the web and learn about exclusive opportunities.

    Β 

    I won’t send you spam. Unsubscribe at any time.

    Webmentions

    103
    • Ataman
    • ll↓
    • Prince Wilson
    • Diego M. G. Vieira
    • Tatsuya Nakadai
    • Joel is working β›ˆ
    • Michael Ye
    • Horacio A Fernandes
    • πŸ¦€ :party-corgi: πŸ¦€
    • Fabian Brash
    • γŠγŠγ¨γ‚οΌ γ‚¨γƒ³γ‚·γ‚™γƒ‹γ‚’ | 𝔇𝔦𝔀𝔦-𝔄𝔫𝔀𝔩𝔒𝔯
    • Jon Meyers
    • Fatou πŸ‘©πŸΎβ€πŸ’»
    • Vaibhav Acharya
    • Lauro Silva
    • Maxime Richard
    • Supabase
    • Paul Copplestone
    • Anna Rankin πŸ–€BLM πŸ–€
    • Lara Littlefield
    • Leo Hui
    • AncientSwordRage, Wear a 😷; Cover your πŸ‘ƒ
    • Ant Wilson