DEV Community

Cover image for TLDR; Google Auth in ReactJS using Supabase
Chaudhry Talha 🇵🇸
Chaudhry Talha 🇵🇸

Posted on • Edited on

TLDR; Google Auth in ReactJS using Supabase

My project was created using vite + react-js.

Let's get the required keys from Google Cloud Console. I have these two URLs form which I'll be sending the google sign-in request from Local host i.e. http://localhost:5173 and a Production URL https://MY_DOMAIN_NAME.com.

Go to Google Cloud Console create/select a new proejct → in search bar search OAuth consent screen and select it:

In Branding you'll need to add you https://MY_DOMAIN_NAME.com in the Authorized domains section:

You can keep the app in test mode or Publish App. I'll keep it in test mode and click on + Add users towards the end under Test users. These users will be allowed to login into the app only.

If you decides to publish you'll need to have links for privacy policy and terms of service to your website.

Click Clients on the left sidebar. Click Create OAuth client.

Fill out name of your project

Authorized redirect URIs: Paste your exact Supabase callback URL. It looks exactly like this:

https://<YOUR_PROJECT_ID>.supabase.co/auth/v1/callback
Enter fullscreen mode Exit fullscreen mode

You can find the ...supabase.co/auth/v1/... from Supabase Dashboard -> Authentication -> Sign In / Providers > Google

Click Create. Copy the Client ID and Client Secret it gives you.

--

Next, back in your Supabase Dashboard -> Authentication -> Sign In / Providers > Google, toggle it on.

Paste the Client ID and Client Secret you just created in GCP, and click Save.

Google client IDs and client secret already configured

Next, Go to Authentication -> URL Configuration.

Site URL: Enter https://MY_DOMAIN_NAME.com

Redirect URLs: Click "Add URL" and add these two exact lines so auth works locally and in production:

  • http://localhost:5173/**
  • https://MY_DOMAIN_NAME.com/**

I have the URL whitelisted in my Supabase project as well:


Below is how I'll implement it in my ReactJS project. I'll be using context API to keep the logged in state (feel free to use zustand or any you prefer).

Install this first:

yarn add @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode

You should have your:

VITE_SUPABASE_URL=https://<YOUR_PROJECT_ID>.supabase.co
VITE_SUPABASE_ANON_KEY=your_long_anon_key_here
Enter fullscreen mode Exit fullscreen mode

in your .env file. If you haven't check the Extras section at the end.

Add a file named supabaseClient.ts and in it add:

import { createClient } from "@supabase/supabase-js";

const supabaseUrl = import.meta.env.YOUR_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.YOUR_SUPABASE_ANON_KEY;

if (!supabaseUrl || !supabaseAnonKey) {
  throw new Error("Missing Supabase environment variables. Please set YOUR_PUBLIC_SUPABASE_URL and YOUR_SUPABASE_ANON_KEY.");
}

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

Enter fullscreen mode Exit fullscreen mode

As I'm using Context API, we'll need to create three files to add a context for Auth:

  • AuthContext.tsx
  • AuthProvider.tsx
  • useAuth.ts

In AuthContext.tsx add this code:

import type { Session, User } from "@supabase/supabase-js";
import { createContext } from "react";

export type UserProfile = {
  id: string;
  email: string | null;
  full_name: string | null;
  photo_url: string | null;
};

type AuthContextType = {
  user: User | null;
  profile: UserProfile | null;
  session: Session | null;
  loading: boolean;
  signInWithGoogle: () => Promise<void>;
  signOut: () => Promise<void>;
};

export const AuthContext = createContext<AuthContextType | null>(null);

Enter fullscreen mode Exit fullscreen mode

The create a file AuthProvider.tsx add:

import type { Session, User } from "@supabase/supabase-js";
import { useEffect, useState } from "react";
import { supabase } from "../../lib/supabase";
import { AuthContext, type UserProfile } from "./AuthContext";

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [session, setSession] = useState<Session | null>(null);
  const [user, setUser] = useState<User | null>(null);
  const [profile, setProfile] = useState<UserProfile | null>(null);
  const [loading, setLoading] = useState(true);

// If you want to store user data in a supabase table as well, for example I have user_data table table example here in upsertProfile
  const upsertProfile = async (u: User) => {
    const data = {
      id: u.id,
      email: u.email ?? null,
      full_name: u.user_metadata?.full_name ?? null,
      photo_url: u.user_metadata?.avatar_url ?? null,
    };

    const { data: saved } = await supabase
      .from("user_data")
      .upsert(data, { onConflict: "id" })
      .select()
      .single();

    setProfile(saved ?? data);
  };

  useEffect(() => {
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session);
      setUser(session?.user ?? null);
      if (session?.user) upsertProfile(session.user);
      setLoading(false);
    });

    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session);
      setUser(session?.user ?? null);
      if (session?.user) upsertProfile(session.user);
      else setProfile(null);
    });

    return () => subscription.unsubscribe();
  }, []);

  const signInWithGoogle = async () => {
    await supabase.auth.signInWithOAuth({
      provider: "google",
      options: { redirectTo: window.location.origin },
    });
  };

  const signOut = async () => {
    await supabase.auth.signOut();
  };

  return (
    <AuthContext.Provider
      value={{ user, profile, session, loading, signInWithGoogle, signOut }}
    >
      {children}
    </AuthContext.Provider>
  );
}

Enter fullscreen mode Exit fullscreen mode

and finally the hook, create a file useAuth.ts and in it add:

import { useContext } from "react";
import { AuthContext } from "./AuthContext.tsx";

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used within AuthProvider");
  return ctx;
}

Enter fullscreen mode Exit fullscreen mode

Now you are finally ready to add the changes to app. I have seen people putting the auth context directly in App.tsx, or there are other mechanisms as well, so it depends on your app specific needs.

In this article, I'll take a simple approach, we'll wrap the whole <App /> in the AuthProvider so open main.tsx that has your app's ...<StrictMode><App />... and wrap the <App /> with the AuthProvider:

//... Your imports

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <AuthProvider>
      <App />
    </AuthProvider>
  </StrictMode>,
);
Enter fullscreen mode Exit fullscreen mode

Your routing will be in the App.tsx file. As an example say you have two pages Home.tsx and Profile.tsx.

  • Home.tsx can be accessed by both authenticated and not authenticated users. Some parts of the pages will be available for public.
  • Profile.tsx Only accessible by the Authenticated users

For Home.tsx

import { useAuth } from "./AuthProvider";
//... other imports

export default function Home() {

const {
    user,
    profile,
    isLoading: isAuthLoading,
    signInWithGoogle,
    signOut,
  } = useAuth();

//... other code

return (
<div className="flex items-center justify-between gap-4">

      <h1>Home</h1>
      <p>This page is public.</p>

{user && profile ? (
              <div>
                <p>Welcome, {profile.name}!</p>
                <p>{profile.email}</p>
                <p>{profile.avatarUrl}</p>
                <button onClick={signOut}>Sign Out</button>
              </div>
            ) : (
               <button onClick={() => {
                  void signInWithGoogle();
                }}>Login with Google</button>
            )}

</div>
)
}
Enter fullscreen mode Exit fullscreen mode

For Profile.tsx:

import { useNavigate } from "react-router-dom";
//... Other imports

export default function Profile() {
  const { user, loading } = useAuth();
  const navigate = useNavigate();

  useEffect(() => {
    if (!loading && !user) navigate("/", { replace: true });
  }, [user, loading, navigate]);

  if (loading || !user) return null;

  return <div>Profile</div>;
}

Enter fullscreen mode Exit fullscreen mode

NOTE: Using the above approach in Profile.tsx, is a common and safe pattern. The only edge case to know about: There's a brief moment where an unauthenticated user could see the page render before the redirect fires — your if (loading || !user) return null handles that by rendering nothing until auth state is resolved, so nothing sensitive is ever shown.

The loading guard is the key safety net here. As long as you return null while loading and redirect when !user, there's no flash of protected content.


Extras

Getting Supabase Anon Key and other required variables for .env

In Supabse project goto Project Settings and then API Keys

The default key under Publishable key That is your new "anon" key, Copy that top one and paste it into your .env as VITE_SUPABASE_ANON_KEY.

(You don't need the "Secret key" anywhere so leave it as is).

To get VITE_SUPABASE_URL click on Data API under INTEGRATIONS menu:

This is what you'll paste for your VITE_SUPABASE_URL.

Top comments (0)