DEV Community

Cover image for How to add Supabase Auth to Astro
Mihai-Adrian Andrei
Mihai-Adrian Andrei

Posted on

How to add Supabase Auth to Astro

Supabase Authentication in Astro

In this guide, we will integrate Supabase authentication into an Astro project using Supabase's server-side client and Astro middleware.

Prerequisite: Create a GitHub OAuth App and Set it in the Supabase Dashboard

Before integrating GitHub authentication in your Astro app, you need to create a GitHub OAuth app and configure it in the Supabase dashboard.

1. Create a GitHub OAuth App

Follow these steps to create an OAuth app on GitHub:

  1. Go to GitHub Settings:

    • Log in to your GitHub account.
    • Click on your profile icon in the top-right corner and select Settings.
  2. Navigate to Developer Settings:

    • Scroll down on the settings page and click on Developer settings from the sidebar.
  3. Create a New OAuth App:

    • Under OAuth Apps, click New OAuth App.
    • Fill in the following fields:
      • Application Name: Give your app a name (e.g., "Astro App Auth").
      • Homepage URL: Set this to your app's base URL, e.g., http://localhost:4321 (for local development) or your production URL.
      • Authorization Callback URL: Set this to https://yourProjectId.supabase.co/auth/v1/callback
  4. Register the Application: Once all the fields are filled, click Register Application.

  5. Copy Your Client ID and Secret:

    • After registering, you'll be provided with a Client ID.
    • Click Generate a new client secret to get your Client Secret.
    • Save both these values securely, as you'll need them in Supabase.

2. Configure GitHub OAuth in Supabase

  1. Go to Supabase Dashboard:

  2. Navigate to Authentication Settings:

    • In the project dashboard, click on Authentication from the sidebar.
    • Go to the Providers tab and find GitHub.
  3. Set GitHub OAuth Details:

    • Toggle Enable GitHub.
    • Enter the Client ID and Client Secret you copied from GitHub.
    • Ensure that the callback URL you set in GitHub matches the one provided in the Supabase project under the GitHub provider settings.
    • Save the changes.

3. Set Environment Variables

In your project, ensure that you set the following environment variables with the corresponding values:

SUPABASE_URL=
SUPABASE_PUBLIC_KEY=
SUPABASE_SECRET_KEY=
Enter fullscreen mode Exit fullscreen mode

App code

Now, let's get to the code part. First, we will need to install the dependencies.

0. Install the dependencies

npm i @supabase/ssr @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode

1. Create a Supabase Server Instance

In server-side environments (e.g., in an Astro middleware), we need to create a Supabase instance that manages user sessions using cookies. Here's how to set up this instance:

// src/lib/supabase.ts

import { createServerClient, parseCookieHeader, type CookieOptionsWithName } from '@supabase/ssr';
import type { AstroCookies } from 'astro';
import type { Database } from 'database.types';

export const cookieOptions: CookieOptionsWithName = {
    path: '/',
    secure: true,
    httpOnly: true,
    sameSite: 'lax',
};

export const createSupabaseServerInstance = (context: {
    headers: Headers;
    cookies: AstroCookies;
}) => {
    const supabase = createServerClient<Database>(
        import.meta.env.SUPABASE_URL!,
        import.meta.env.SUPABASE_PUBLIC_KEY!,
        {
            cookieOptions,
            cookies: {
                getAll() {
                    return parseCookieHeader(context.headers.get('Cookie') ?? '');
                },
                setAll(cookiesToSet) {
                    cookiesToSet.forEach(({ name, value, options }) =>
                        context.cookies.set(name, value, options),
                    );
                },
            },
        },
    );

    return supabase;
};
Enter fullscreen mode Exit fullscreen mode
  • Supabase Server Client: createServerClient is used to create a Supabase client that is aware of the cookies used for authentication. This client will manage the authentication state server-side.
  • Cookie Management: The cookieOptions object ensures that the cookies are secure, HTTP-only, and follow the SameSite policy to protect against CSRF attacks. The getAll and setAll methods help Supabase interact with cookies.
  • Context: The context contains request headers and cookies, which are essential for reading and writing the user's session information during authentication.

2. Create a Supabase Admin Instance

This instance is used for server-to-server communication. Unlike the server-side instance that interacts with user sessions, the admin instance uses the SUPABASE_SECRET_KEY and doesn't manage user sessions but it is used to query the database ignoring the row level security.

// src/lib/supabaseAdmin.ts

import { createClient } from "@supabase/supabase-js";
import { type Database } from "database.types";

export const dbClient = createClient<Database>(
  import.meta.env.SUPABASE_URL,
  import.meta.env.SUPABASE_SECRET_KEY,
  {
    auth: {
      autoRefreshToken: false,
      persistSession: false,
      detectSessionInUrl: false,
    },
  }
);
Enter fullscreen mode Exit fullscreen mode
  • Admin Client: The admin client (createClient) is used to access Supabase services without relying on user authentication. This is useful for performing actions like querying data or interacting with other Supabase services from the server side.
  • Auth Options: We've disabled session persistence and auto token refresh because this client is meant for server-side use only, where user sessions are not relevant.

3. Add Middleware for Authentication

Middleware allows us to check the user's session on each request and store user information in locals to make it accessible throughout the app.

import { createSupabaseServerInstance } from "@lib/supabase";
import { dbClient } from "@lib/supabaseAdmin";
import { defineMiddleware } from "astro:middleware";

const PATHS_TO_IGNORE = ["/ignore"];

export const onRequest = defineMiddleware(
  async ({ locals, cookies, url, request, redirect }, next) => {
    if (PATHS_TO_IGNORE.includes(url.pathname)) {
      return next();
    }
    const supabase = createSupabaseServerInstance({
      cookies,
      headers: request.headers,
    });

    const { data } = await supabase.auth.getUser();

    // here you can use the dbClient to query supabase if you want aditional fields / logic. For ex, query a profile table

    if (data.user) {
      locals.user = {
        email: data.user.email,
        id: data.user.id,
      };
    }
    return next();
  }
);

Enter fullscreen mode Exit fullscreen mode
  • Define Middleware: We define middleware that runs on every request unless the URL is in the PATHS_TO_IGNORE list. This is useful for paths where authentication is not required.
  • Supabase Session: We use the server-side Supabase instance to get the current user session (supabase.auth.getUser()).
  • Setting Locals: If a user session is found, we store the user's email and ID in locals for later use (e.g., rendering user-specific content on pages).

4. Add callback for auth in pages/api/auth/callback.ts.

You will need to specify this inside supabase for callback url.

import { createSupabaseServerInstance } from '@lib/supabase';
import type { APIRoute } from 'astro';

export const GET: APIRoute = async ({ cookies, request, url, redirect }) => {
    const authCode = url.searchParams.get('code');
    if (!authCode) {
        return new Response('No code provided', { status: 400 });
    }

    const supabase = createSupabaseServerInstance({ cookies, headers: request.headers });
    const { error } = await supabase.auth.exchangeCodeForSession(authCode);

    if (error) {
        return new Response(error.message, { status: 500 });
    }
    return redirect('/');
};
Enter fullscreen mode Exit fullscreen mode

5. Create actions

Define the actions for logging in and logging out, using Supabase's OAuth and session management. You can read more about actions here.

// src/actions.ts

import { createSupabaseServerInstance } from "@lib/supabase";
import type { Provider } from "@supabase/supabase-js";
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro:schema";
import { createSupabaseServerInstance } from "@lib/supabase";

export function getBaseUrl(): string {
  return (
    process.env.PUBLIC_URL ||
    `http://localhost:${process.env.PORT ?? 4321}`
  );
}

export const server = {
    login: defineAction({
    accept: "form",
    input: z.object({
      provider: z.string(),
    }),
    handler: async (input, context) => {
      const baseUrl = getBaseUrl();
      const supabase = createSupabaseServerInstance({
        headers: context.request.headers,
        cookies: context.cookies,
      });
      const { data, error } = await supabase.auth.signInWithOAuth({
        provider: input.provider as Provider,
        options: {
          redirectTo: `${baseUrl}/api/auth/callback`,
        },
      });
      if (error) {
        throw new ActionError({
          code: "BAD_REQUEST",
          message: "Failed to sign in",
        });
      }
      return {
        url: data.url,
      };
    },
  }),
  logout: defineAction({
    accept: "form",
    handler: async (_, context) => {
      const supabase = createSupabaseServerInstance({
        headers: context.request.headers,
        cookies: context.cookies,
      });
      const { error } = await supabase.auth.signOut();
      if (error) {
        throw new ActionError({
          code: "BAD_REQUEST",
          message: "Failed to sign in",
        });
      }
      return {
        ok: true,
      };
    },
  }),
}

Enter fullscreen mode Exit fullscreen mode

6. Create a Login Page

---
// pages/login.astro

import Layout from "@layouts/Layout.astro";
import { actions } from 'astro:actions';

const result = Astro.getActionResult(actions.login);
if (result && !result.error) {
  return Astro.redirect(result.data.url);
}
---

<Layout title="Welcome to Astro.">
    <div
        class="mx-auto flex items-center justify-center rounded-xl bg-gray-900 px-4 py-8 text-white shadow-sm"
    >
        <div class="mx-auto max-w-md space-y-6">
            <div class="space-y-2 text-center">
                <h1 class="text-3xl font-bold">Sign In</h1>
                <p class="text-gray-500 dark:text-gray-400">
                    Sign in to your account using one of the options below.
                </p>
            </div>
            <form class="space-y-4" action={actions.login} method="POST">
                <button
                    class="flex w-full items-center justify-center gap-2 rounded-md p-2 hover:bg-gray-700 hover:text-white"
                    value="github"
                    name="provider"
                >
                    Sign in with GitHub
                </button>
            </form>
        </div>
    </div>
</Layout>
Enter fullscreen mode Exit fullscreen mode
  • Login Form: The form triggers the login action (defined next) via a POST request. It includes a button for GitHub OAuth login.
  • OAuth Flow: When the user submits the form, Astro will handle the OAuth flow and, upon success, redirect the user to the appropriate URL.

7. Protect Pages with Authentication

In protected pages, check if a user is authenticated by using Astro.locals. If no user is found, redirect them to the login page.
In astro files:

---
// pages/protected.astro

const { user } = Astro.locals;
if(!user){
  return Astro.redirect('/login')
}
---
<h1>Protected page</h1>
Enter fullscreen mode Exit fullscreen mode

In actions:

testAction: defineAction({
    input: z.object({
      data: z.string(),
    }),
    handler: async (input, context) => {
      if (!context.locals.user) {
        throw new ActionError({
          code: "UNAUTHORIZED",
          message: "Unauthorized",
        });
      }
      // now user is logged in
      return {
        input
      };
    },
  }),
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's it! By following these steps, you can successfully integrate Supabase authentication into your Astro project using GitHub OAuth. Pretty easy right?

Next, I'll show you how to add Stripe to your Astro app. Stay tuned! 👋

Top comments (0)