OWolf

2024-09-09 web, development, javascript

Supabase Auth REST API with Next.js

By O. Wolfson

To create a persistent auth using the Supabase REST API with Next.js and the App Router, you can follow these general steps:

  1. Create a login page component that allows users to enter their email and password.

  2. When the user submits the login form, send a request to the Supabase session endpoint to create a new session for the user.

  3. If the login is successful, store the refresh_token securely on the client side using a secure storage mechanism such as an HTTP-only cookie or localStorage.

  4. Create a higher-order component (HOC) that wraps your protected pages and checks for the presence of a valid access_token before rendering the protected page.

  5. If the access_token is not present or has expired, use the refresh_token to obtain a new access_token from the Supabase token endpoint.

  6. If the refresh_token is invalid or has expired, redirect the user to the login page.

  7. When the user logs out, delete the refresh_token from the client-side storage.

js
// pages/login.tsx

import { useState } from "react";
import { useRouter } from "next/router";
import { createSession } from "../lib/supabase";

export default function Login() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const router = useRouter();

  const handleSubmit = async (event) => {
    event.preventDefault();
    const { error } = await createSession(email, password);
    if (error) {
      console.error(error);
    } else {
      router.push("/");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email:
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </label>
      <label>
        Password:
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </label>
      <button type="submit">Log in</button>
    </form>
  );
}

In this example, we define a Login component that allows users to enter their email and password. When the user submits the login form, we call the createSession function to create a new session for the user. If the login is successful, we use the useRouter hook to navigate to the home page.

js
// lib/supabase.ts

import { createClient } from "@supabase/supabase-js";

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

export const createSession = async (email, password) => {
  const { error } = await supabase.auth.signIn({ email, password });
  return { error };
};

export const refreshAccessToken = async (refreshToken) => {
  const { data, error } = await supabase.auth.api.refreshAccessToken(
    refreshToken
  );
  return { data, error };
};

In this example, we define a supabase client object using the createClient function from the @supabase/supabase-js library. We also define a createSession function that calls the signIn method on the auth object to create a new session for the user. Finally, we define a refreshAccessToken function that calls the refreshAccessToken method on the auth.api object to obtain a new access_token using a refresh_token.

js
// lib/withAuth.tsx

import { useEffect } from "react";
import { useRouter } from "next/router";
import { supabase, refreshAccessToken } from "./supabase";

const withAuth = (Component) => {
  const AuthComponent = (props) => {
    const router = useRouter();

    useEffect(() => {
      const accessToken = supabase.auth.session()?.access_token;
      const refreshToken = localStorage.getItem("refresh_token");

      if (!accessToken && !refreshToken) {
        router.push("/login");
      } else if (!accessToken && refreshToken) {
        refreshAccessToken(refreshToken).then(({ data, error }) => {
          if (error) {
            console.error(error);
            localStorage.removeItem("refresh_token");
            router.push("/login");
          } else {
            supabase.auth.setSession(data);
          }
        });
      }
    }, []);

    return <Component {...props} />;
  };

  return AuthComponent;
};

export default withAuth;

In this example, we define a higher-order component (HOC) called withAuth that wraps our protected pages and checks for the presence of a valid access_token before rendering the protected page. If the access_token is not present or has expired, the HOC uses the refresh_token to obtain a new access_token from the Supabase token endpoint. If the refresh_token is invalid or has expired, the HOC redirects the user to the login page.

To use the withAuth HOC, you can wrap your protected pages like this:

js
// pages/dashboard.tsx

import withAuth from "../lib/withAuth";

const Dashboard = () => {
  return (
    <div>
      <h1>Dashboard</h1>
      <p>This is a protected page.</p>
    </div>
  );
};

export default withAuth(Dashboard);

In this example, we define a Dashboard component that is wrapped with the withAuth HOC. This ensures that the Dashboard component is only rendered if the user has a valid access_token.

Finally, when the user logs out, you can delete the refresh_token from the client-side storage like this:

js
// pages/logout.tsx

import { useEffect } from "react";
import { useRouter } from "next/router";

export default function Logout() {
  const router = useRouter();

  useEffect(() => {
    localStorage.removeItem("refresh_token");
    router.push("/login");
  }, []);

  return null;
}

In this example, we define a Logout component that removes the refresh_token from the client-side storage and navigates the user to the login page.

Supabase Auth REST API Sign up

To create a sign-up page using the Supabase REST API, you can follow these general steps:

  1. Create a sign-up page component that allows users to enter their email and password.

  2. When the user submits the sign-up form, send a request to the Supabase auth endpoint to create a new user.

  3. If the sign-up is successful, store the refresh_token securely on the client side using a secure storage mechanism such as an HTTP-only cookie or localStorage.

  4. Redirect the user to the home page or a confirmation page.

Here's an example of how you could implement these steps using Next.js and the App Router:

js
// pages/signup.tsx

import { useState } from "react";
import { useRouter } from "next/router";
import { supabase } from "../lib/supabase";

export default function Signup() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const router = useRouter();

  const handleSubmit = async (event) => {
    event.preventDefault();
    const { error } = await supabase.auth.signUp({ email, password });
    if (error) {
      console.error(error);
    } else {
      router.push("/");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email:
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </label>
      <label>
        Password:
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </label>
      <button type="submit">Sign up</button>
    </form>
  );
}

In this example, we define a Signup component that allows users to enter their email and password. When the user submits the sign-up form, we call the signUp function to create a new user account. If the sign-up is successful, we use the useRouter hook to navigate to the home page.

js
// lib/supabase.ts

import { createClient } from "@supabase/supabase-js";

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

export const refreshAccessToken = async (refreshToken) => {
  const { data, error } = await supabase.auth.api.refreshAccessToken(
    refreshToken
  );
  return { data, error };
};

In this example, we define a supabase client object using the createClient function from the @supabase/supabase-js library. We also define a refreshAccessToken function that calls the refreshAccessToken method on the auth.api object to obtain a new access_token using a refresh_token.

Finally, when the user logs out, you can delete the refresh_token from the client-side storage like this:

js
// pages/logout.tsx

import { useEffect } from "react";
import { useRouter } from "next/router";

export default function Logout() {
  const router = useRouter();

  useEffect(() => {
    localStorage.removeItem("refresh_token");
    router.push("/login");
  }, []);

  return null;
}

In this example, we define a Logout component that removes the refresh_token from the client-side storage and navigates the user to the login page.