Supabase and GitHub App Authentication done right
Ever wondered how to use GitHub to authenticate on your site? Or why your provider_refresh_token
was null
when using OAuth GitHub authentication for your Supabase backend?
In this article, I share how I was able to configure Supabase and GitHub App (not OAuth app!) to work together. This allows me to authenticate users on my website with a GitHub, and then to use their GitHub provider token to access GitHub. And provider_refresh_token
is properly set!
These tokens are not saved by Supabase, so we need to create a table and glue code to save them in a database. This article shows code examples for that.
Enable GitHub authentication on Supabase
Go to Authentication > Providers > GitHub, toggle checkbox.
If you do not have a GitHub App created and have no Client ID/Client Secret, see the next section.
Enter your client ID and secret.
Create GitHub App
First, go to https://github.com/settings/apps and create a new GitHub App.
Specify App name
Homepage URL:
<your supabase url instance>
such as https://ybiwkmuwzdxxxxxxxxxx.supabase.coCallback URL:
<your supabase url instance>
Make sure
Expire user authorization tokens
checkbox is toggled.Disable WebHook (uncheck "Active") temporarily. This has nothing to do with a Supabase.
Permissions > Account Permissions > Email Address enable email read-only permission. Enable other permissions if needed.
Create profile
table in a database
Since Supabase does not store provider tokens in its database for security reasons, we should do it ourselves!
Create a new table profile
which will store provider_token
and provider_refresh_token
. Also enable RLS on this table, and provide 2 new policies:
User can SELECT their profile.
User can UPDATE their profile.
-- Create a `profile` table
DROP TABLE IF EXISTS public.profile;
CREATE TABLE public.profile (
-- user id
id UUID REFERENCES auth.users NOT NULL,
-- add custom attributes - in my case it's a user login
login VARCHAR NOT NULL,
-- add tokens, they can be null!
github_provider_token VARCHAR NULL,
github_provider_refresh_token VARCHAR NULL,
PRIMARY KEY (id)
);
-- Enable RLS
ALTER TABLE public.profile
ENABLE ROW LEVEL SECURITY;
-- User can select their profile.
CREATE POLICY
"Can only view own profile data."
ON public.profile
FOR SELECT
USING ( auth.uid() = id );
-- User can update their profile (to update tokens)
CREATE POLICY
"Can only update own profile data."
ON public.profile
FOR UPDATE
USING ( auth.uid() = id );
-- Create a postgres function that will automatically create new `profile`
-- row when new user is authenticated for the first time.
-- This new row will contain only `id` (uuid) and `login` (github username)
-- fields.
DROP FUNCTION IF EXISTS public.create_profile_for_new_user CASCADE;
CREATE FUNCTION
public.create_profile_for_new_user()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO public.profile (id, login)
VALUES (
NEW.id,
NEW.raw_user_meta_data ->> 'user_name'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Create new trigger to call create_profile_for_new_user when new
-- user is authenticated.
DROP TRIGGER IF EXISTS create_profile_on_signup ON auth.users CASCADE;
CREATE TRIGGER
create_profile_on_signup
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE PROCEDURE
public.create_profile_for_new_user();
At this step, if a new user is authenticated for the first time, new row will be inserted into a table auth.users
, then our trigger is executed and new public.profile
row is created. Initially, both tokens are null
, but we will populate this info later by ourselves.
Setup frontend
It is not important how exactly you authenticate with github
provider, in my case I use pre-built @supabase/auth-ui-react
component:
"use client"
import * as React from "react"
import { ThemeSupa } from '@supabase/auth-ui-shared'
// https://supabase.com/docs/guides/auth/auth-helpers/nextjs-server-components#creating-a-supabase-client
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
// generated with pnpx supabase@latest gen types typescript --project-id XXXX --schema public > src/generated/database.types.ts
import { type Database } from '@/generated/database.types';
import { Auth } from "@supabase/auth-ui-react";
export function AuthForm() {
const supabase = createClientComponentClient<Database>();
return (
<Auth
supabaseClient={supabase}
providers={['github']}
appearance={{ theme: ThemeSupa }}
theme="dark"
showLinks={false}
// our auth callback route:
redirectTo={`http://localhost:3000/api/auth/callback`}
onlyThirdPartyProviders
/>
)
}
Setup a callback route
Create new file at /src/app/api/auth/callback/route.ts
:
// src/app/api/auth/callback/route.ts
// tell bundler this file can be used by a server only.
import "server-only";
import { NextRequest, NextResponse } from "next/server";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
export async function GET(req: NextRequest) {
const cookieStore = cookies();
const supabase = createRouteHandlerClient({ cookies: () => cookieStore });
const { searchParams } = new URL(req.url);
const code = searchParams.get("code");
if (code) {
// this call authenticates
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
if (error) {
// handle error.
throw error;
}
const { session } = data;
{ // create new scope to reuse `error` name
if (!session.provider_token && !session.provider_refresh_token) {
throw new Error("Both provider_token and provider_refresh_token are missing")
}
// save auth token & refresh token for a current user profile.
const { error } = await supabase
.from("profile")
.update({
github_provider_token: session.provider_token,
github_provider_refresh_token: session.provider_refresh_token,
})
.eq("id", session.user.id)
.select()
.single();
if (error) {
// handle error.
throw error;
}
}
}
return NextResponse.redirect(new URL("/", req.url));
}
Summary
That's it! Hope you find it useful. Please comment if you find any issues, I will fix.