Set up local Supabase OAuth logins

One of the benefits of Supabase is its integrated login system that supports many OAuth providers, including Google and GitHub. While there is plenty of documentation explaining how to set up OAuth providers in hosted Supabase, the instructions for local Supabase are fairly terse and are missing several steps.

First, create a callback endpoint in your application, for example, /auth/callback. This is the callback that will receive the OAuth information from Supabase. Specifically, you should receive either a code query string parameter that will allow the user to login or an error parameter indicating there was an error logging in. Note that for server-side authentication you must use @supabase/ssr. Here’s an example written using Astro.

// Astro example
import { createServerClient, parseCookieHeader } from "@supabase/ssr";

const supabaseUrl = import.meta.env.SUPABASE_URL;
const supabaseAnonKey = import.meta.env.SUPABASE_ANON_KEY;

export const GET: APIRoute = async ({ url, request, cookies, redirect }) => {

    // if there's no code then redirect to login
    const code = url.searchParams.get("code");
    if (!code) {
        redirect("/login?error=no-code");
    }
    
    // there is a code, try to log in
    const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
        cookies: {
            getAll() {
                return parseCookieHeader(request.headers.get("cookie") || "");
            },
            setAll(cookiesToSet) {
                cookiesToSet.forEach(({ name, value, options }) =>
                    cookies.set(name, value, options)
                );
            },
        },
    });
    
    const { data, error } = await supabase.auth.exchangeCodeForSession(code);
    if (error || !data?.session) {
        return redirect("/login?error=unauthorized");
    }

    return redirect("/");
}

With the callback set up, you now need to let Supabase know how to call it. In general, there are three steps to the OAuth login process for Supabase:

  1. Your application links off to the OAuth provider’s authentication service.
  2. The OAuth provider redirects to the Supabase auth service.
  3. Supabase redirects to your application callback.

To ensure that happens locally, you need to edit the config.toml file. First, locate the [auth] section at the top and make sure the value for site_url is your local application URL and additional_redirect_urls contains the full URL for the callback endpoint. Here’s an example:

[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://localhost:4321"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["http://localhost:4321/auth/callback"]

(Without specifying additional_redirect_urls, you’ll always have to redirect back to the application homepage.)

Next, create an entry for your OAuth provider including the client ID, client secret, and redirect URI. For GitHub, it would look like this:

[auth.external.github]
enabled = true
client_id = "env(GITHUB_CLIENT_ID)"
secret = "env(GITHUB_CLIENT_SECRET)"
redirect_uri = "http://localhost:54321/auth/v1/callback"

The redirect_uri here needs to be the local Supabase URL for OAuth callbacks. The default port is 54321, so double-check the port.

After editing config.toml, you need to restart Supabase to reload the configuration:

npx supabase stop
npx supabase start

Next, you need to generate the OAuth URL to use in your application. Here’s an example generating a URL to login with GitHub:

import { createServerClient, parseCookieHeader } from "@supabase/ssr";

const supabaseUrl = import.meta.env.SUPABASE_URL;
const supabaseAnonKey = import.meta.env.SUPABASE_ANON_KEY;

const supabaseUrl = import.meta.env.SUPABASE_URL;
const supabaseAnonKey = import.meta.env.SUPABASE_ANON_KEY;

export const GET: APIRoute = async ({ url, request, cookies, redirect }) => {

    // if there's no code then redirect to login
    const code = url.searchParams.get("code");
    if (!code) {
        redirect("/login?error=no-code");
    }
    
    // there is a code, try to log in
    const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
        cookies: {
            getAll() {
                return parseCookieHeader(request.headers.get("cookie") || "");
            },
            setAll(cookiesToSet) {
                cookiesToSet.forEach(({ name, value, options }) =>
                    cookies.set(name, value, options)
                );
            },
        },
    });
        
    const { data, error } = await supabase.auth.signInWithOAuth({
        provider: "github",
        options: {
            // must be listed as additional_redirect_urls
            redirectTo: `http://localhost:4321/auth/callback`
        }
    });

    if (error || !data?.url) {
        // handle error
    }

    redirect(data.url);
}

Everything is now wired up for the correct end-to-end flow.

Master JavaScript Promises

Free E-book - Understanding JavaScript Promises

What You'll Learn

  • Promise fundamentals and concepts
  • Real-world promise implementations
  • Best practices and patterns
  • Error handling techniques

Demystify JavaScript promises with practical examples and expert insights.

Download Your Free Copy

The community edition arrives in your inbox within minutes.