Setting up Supabase Server-Side OAuth for Next.js

May 4, 2024

I'm currently working on a Next.js project and have been using Supabase as my database. Although I've been relying on Supabase for data management, I initially used NextAuth 5.0 for authentication. While NextAuth is fantastic, I wanted to consolidate all my tools under one roof.

Up until now, using NextAuth meant I had to create my own model/schema for authentication. However, since Supabase offers its own authentication system, I decided to switch over. Transitioning from NextAuth to Supabase wasn't straightforward—it required a hefty amount of refactoring and a deep dive into new concepts.

One area that particularly tripped me up was cookie-based authentication. The confusion stemmed mainly from Supabase's createClient and createBrowserClient functions, the latter being part of the @supabase/ssr package. With this new SSR package, it seems there's no longer a need for the old createClient from @supabase/supabase-js.

I'll walk you through setting up OAuth authentication with GitHub. These steps are adaptable to any OAuth provider you prefer

Prerequisites

Let's kick things off by installing the necessary packages. I'll be working with Next.js, and for package management, I'm using pnpm.

pnpm add @supabase/supabase-js @supabase/ssr

With the packages installed, the next step is to set up the Supabase database using environment variables.

Setting Up Supabase Environment Variables

Start by creating a .env.local file in the root directory of your project.

In this file, you'll need to define your NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY:

NEXT_PUBLIC_SUPABASE_URL = your_supabase_url_here;
NEXT_PUBLIC_SUPABASE_ANON_KEY = your_anon_key_here;

You can find these values in your Supabase dashboard. The project URL corresponds to NEXT_PUBLIC_SUPABASE_URL, and the ANON key to NEXT_PUBLIC_SUPABASE_ANON_KEY. We prefix these with NEXT_PUBLIC\* to ensure these environment variables are accessible on the client side.

Accessing Supabase in Next.js with Two Types of Clients

In a Next.js application, depending on where your components run (browser or server), you'll need to create two distinct types of Supabase clients.

Browser Client

For components that run in the browser, we'll set up a client that uses the browser's capabilities.

  • Location: Add the following setup in /lib/utils/supabase/client.ts.
import { createBrowserClient } from "@supabase/ssr";

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}

This function leverages createBrowserClient to initialize a Supabase client specifically for client-side operations

Server Client

For server-side components, server actions, and route handlers, we need a client that can handle server-specific operations. Set this up as follows:

  • Location: Put this code in /lib/utils/supabase/server.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";

export function supabaseServerClient() {
  const cookieStore = cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options });
          } catch (error) {
            // This error can be ignored if middleware is refreshing user sessions.
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: "", ...options });
          } catch (error) {
            // This error can be ignored as well if middleware is managing sessions.
          }
        },
      },
    }
  );
}

This setup configures a server-based Supabase client with cookie handling capabilities essential for authentication and session management in server-side environments.

Creating the middleware.ts for Supabase Authentication in Next.js

When working with Server Components in Next.js, you can't directly write cookies, which can be a bit of a challenge if you need to manage authentication tokens. To handle this, you'll need to set up a middleware that refreshes expired Auth tokens and manages cookie storage efficiently.

The Role of Middleware

Here’s what the middleware is essentially responsible for:

  • Refreshing the Auth token: This is done by calling supabase.auth.getUser(). It's crucial because it prevents Server Components from trying to refresh tokens themselves.
  • Passing the refreshed token to Server Components: This ensures they operate with a current token and don't handle the token lifecycle.
  • Updating the token in the browser: The refreshed token needs to be sent back to the browser to replace the old one. This is handled by setting cookies on the response object.

Setup and File Structure

  1. Root Middleware File: Create middleware.ts in the root of your project. This will help intercept and manage HTTP requests at the server level.

  2. Middleware Utility: The core functionality resides in /lib/utils/supabase/middleware.ts. Here, you define how the middleware interacts with Supabase and manages cookies.

Code Implementation

Here's what your middleware.ts in the root should look like:

import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";

export async function middleware(request: NextRequest) {
  return await updateSession(request);
}

// Config matcher
export const config = {
  matcher: ["['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)']"],
};

This configuration ensures the middleware does not run on static routes or image handling routes, focusing only on those that require active Supabase interaction.

The updateSession Function

create a file at /lib/utils/supabase/middleware.ts and set up the function that handles the session updates:

import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";

export async function updateSession(request: NextRequest) {
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          request.cookies.set({ name, value, ...options });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({ name, value, ...options });
        },
        remove(name: string, options: CookieOptions) {
          request.cookies.set({ name, value: "", ...options });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({ name, value: "", ...options });
        },
      },
    }
  );

  await supabase.auth.getUser();

  return response;
}

Setting Up an API Endpoint for OAuth Code Exchange in Next.js

One critical part of this process is setting up an endpoint to handle the OAuth code exchange. This exchange is necessary to swap an authorization code for a user session token, which can then be used to authenticate future requests made to Supabase.

Creating the Endpoint

Let’s break down the steps to create this endpoint, which not only receives the authorization code but also exchanges it for a session token and sets it as a cookie:

  1. Create the Endpoint File: Begin by creating a new file at app/api/auth/callback/route.ts. This will house our code exchange logic.

  2. Populate the File: Add the following code to handle the OAuth code exchange:

import { supabaseServerClient } from "@/lib/utils/supabase/server";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const requestUrl = new URL(request.url);
  const code = requestUrl.searchParams.get("code");

  if (code) {
    // re-use the server client
    const supabase = await supabaseServerClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);
    if (!error) {
      // Redirect to the dashboard if the code exchange was successful
      return NextResponse.redirect(`${requestUrl.origin}/dashboard`);
    }
  }

  // Redirect to an error page if the code exchange fails
  return NextResponse.redirect(`${requestUrl.origin}/api/auth/auth-code-error`);
}

Setting Up GitHub OAuth with Supabase in Next.js

Now that our OAuth code exchange API is ready, it's time to dive into creating the server actions that will actually handle signing into and signing out of GitHub. This functionality is crucial for integrating GitHub OAuth with your application.

Organizing Server Actions

To keep our project neat and maintainable, I've set up a server-action folder at the root of our project. This is where all our server actions will live. Inside this folder, there's a file named auth-action.ts, which contains the necessary functions for our authentication process.

GitHub Sign-In Action

Here’s how we can set up the function to sign users into GitHub using OAuth:

"use server";

import { supabaseServerClient } from "@/lib/utils/supabase/server";
import { headers } from "next/headers";

export const oAuthSignIn = async () => {
  const supabaseClient = supabaseServerClient();
  const origin = headers().get("origin");

  const { error, data } = await supabaseClient.auth.signInWithOAuth({
    provider: "github",
    options: {
      redirectTo: `${origin}/api/auth/callback`,
    },
  });

  return { data, error };
};

GitHub Sign-Out Action

Signing out is just as important as signing in. Here's the simple function that handles sign-out:

export async function signOut() {
  const supabaseClient = supabaseServerClient();
  const error = await supabaseClient.auth.signOut();
  return error;
}

Personalizing OAuth Providers

While this setup uses GitHub for authentication, you can easily adapt it to other OAuth providers. For instance, replacing github with google in the signInWithOAuth function allows you to use Google for authentication instead. This flexibility makes it easy to cater to different user preferences or requirements.

Setting Up OAuth in Supabase

For the OAuth sign-in to work, you’ll need to configure it via the Supabase dashboard. Whether it’s GitHub, Google, or another provider, setting this up correctly is vital. Supabase provides comprehensive guides for this, and you can find the guide for setting up GitHub authentication here: Supabase GitHub Auth Setup Guide.

Using Server Actions for GitHub Sign-In and Sign-Out in Your Next.js Application

After setting up our server actions for OAuth authentication, here's how you can implement them in your Next.js components. We have a simple setup for both signing out and signing in with GitHub.

Implementing the Sign-Out Action

To create a logout button that signs out the user and then redirects them to the home page, you can use the following component:

<Button
  onClick={async ()=> {
    await signOut();
    await router.push("/");
  }}
>
  Log out
</Button>

This button, when clicked, triggers the signOut function and then uses the Next.js router to navigate back to the / page.

Implementing GitHub Sign-In

For signing in with GitHub,we will use a shadcn/ui button. When clicked, it will call the loginWithAuth function specifically for GitHub authentication:

<Button
  variant="outline"
  className="w-full"
  onClick={()=> loginWithAuth("github")}
>
  Login with Github
</Button>

Here’s the loginWithAuth function, which handles the actual sign-in process:

async function loginWithAuth(type: string) {
  switch (type) {
    case "github":
      const { data, error } = await oAuthSignIn();
      if (error) {
        return toast({
          title: "Error Signing in with Github",
          description: error.message,
        });
      }
      if (data) {
        router.push(data.url ?? "http://localhost:3000/dashboard");
      }
      break;
  }
}

This function attempts to sign in using the oAuthSignIn function from our server actions. If there's an error, it displays a toast notification with the error details. If the sign-in is successful, it redirects the user to the URL provided by Supabase or to the dashboard as a fallback.

In summary, transitioning to Supabase from nextAuth streamlined my development process, and consolidates tool. If you are using Supabase for your database, I highly recommend leveraging its authentication system as well. It simplifies the process and ensures all your tools are in sync.

References

Setting up server side auth for Next.js
OAuth with PKCE flow of SSR