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:
-
Go to GitHub Settings:
- Log in to your GitHub account.
- Click on your profile icon in the top-right corner and select Settings.
-
Navigate to Developer Settings:
- Scroll down on the settings page and click on Developer settings from the sidebar.
-
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
Register the Application: Once all the fields are filled, click Register Application.
-
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
-
Go to Supabase Dashboard:
- Log in to your Supabase Dashboard.
- Select your project.
-
Navigate to Authentication Settings:
- In the project dashboard, click on Authentication from the sidebar.
- Go to the Providers tab and find GitHub.
-
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=
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
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;
};
- 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,
},
}
);
- 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();
}
);
- 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('/');
};
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,
};
},
}),
}
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>
- 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>
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
};
},
}),
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 (2)
Thank you for the article!
I faced a problem caused by
ViewTransition
, redirect viaAstro.redirect
doesn't work with non-app URLs (with Supabase callback URL in this case). I made this workaround, but please let me know if you have a better solution, I'm sure there should be some config for transitions, but I couldn't find it.Hello @sillycoon ! You could just do it via client side js like:
Just check exactly what data is. I didn't try it myself, but it should work!