Web Development

Google OAuth Integration with React and Supabase

March 25, 2025
15 min read

Google Authentication and Gmail/Calendar API Integration with React & Supabase

Integrating Google OAuth with Supabase to enable Gmail and Calendar functionality in your React application is a powerful way to enhance user experience. This comprehensive guide walks you through the entire setup process, from Google Cloud configuration to implementing the authentication flow in your React app.

Objective

Enable your application to:

  • Authenticate users with Google via Supabase
  • Send emails via Gmail API
  • Read and write events via Google Calendar API

Step 1: Enable Google Login in Supabase

  1. Open your Supabase Dashboard
  2. Navigate to Authentication → Providers → Google
  3. Paste your Client ID and Client Secret
  4. Click Save
  5. Copy the callback URL displayed (you'll need this for Google Cloud Console)

The callback URL will look like:

https://<your-supabase-project>.supabase.co/auth/v1/callback

Step 2: Create a Google Cloud Project

  1. Go to Google Cloud Console
  2. Click the Project Selector from the top menu
  3. Click New Project
  4. Name your project and click Create
  5. Select your new project from the top bar

Step 3: Enable Required APIs

  1. Navigate to APIs & Services → Library

  2. Enable the following APIs:

    • Gmail API
    • Google Calendar API
  3. Confirm they appear under APIs & Services → Enabled APIs & services

💡 Enabling an API allows your app to make calls to that service.

  1. Go to Google Auth Platform → OAuth Consent Screen

  2. Configure the following:

    • User Type: External (recommended for customer-facing apps)
    • App Name: Your application name
    • Logo: Upload your app logo (optional)
    • Support Email: Your support email address
    • Authorized domains: Add your domains (e.g., supabase.co, yourdomain.com)
    • Developer Contact Information: Add your email
  3. Click Save and Continue

Step 5: Create OAuth Client Credentials

  1. Go to APIs & Services → Credentials
  2. Click + Create Credentials → OAuth client ID
  3. Choose Application Type: Web Application
  4. Provide a recognizable name for your OAuth client
  5. Under Authorized redirect URIs, add:
    https://<your-supabase-project>.supabase.co/auth/v1/callback
    
  6. Click Create
  7. Important: Note down the Client ID and Client Secret - you'll need them for Supabase

Step 6: Add Scopes (Data Access Permissions)

  1. In the left sidebar, go to Google Auth Platform → Data Access

  2. Click the blue "Add or remove scopes" button

  3. A panel called "Update selected scopes" will open

  4. Make sure the required APIs are already enabled under APIs & Services → Library

    • Enable ✅ Gmail API
    • Enable ✅ Google Calendar API
  5. Inside the Update selected scopes panel, use the search bar to filter for each API:

    • Type gmail to see available Gmail scopes
    • Type calendar to see available Calendar scopes
  6. If you don't see the scopes you need, scroll to the bottom to find "Manually add scopes"

  7. Paste the following lines into the text box:

    https://www.googleapis.com/auth/gmail.readonly
    https://www.googleapis.com/auth/gmail.send
    https://www.googleapis.com/auth/calendar.readonly
    https://www.googleapis.com/auth/calendar.events
    
  8. Click Add scopes → then Save

  9. Your newly added scopes will appear under "Your sensitive scopes" because they allow access to private user data (Gmail and Calendar)

Required Scopes Summary

APIScopePurpose
Gmail APIhttps://www.googleapis.com/auth/gmail.sendSend emails on user's behalf
Google Calendar APIhttps://www.googleapis.com/auth/calendar.eventsCreate and edit calendar events

Step 7: Add Test Users (if in Testing Mode)

  1. In OAuth Consent Screen, go to Test Users
  2. Click + Add Users
  3. Enter your Gmail and team emails
  4. Save the list

⚠️ Only these users can authenticate while the app is in testing mode.

Step 8: Implement Google Login Flow

When the user clicks "Continue with Google", execute the following function to initiate the Supabase OAuth flow:

const handleGoogleLogin = async () => {
  try {
    setIsLoading(true);
    const { error } = await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        redirectTo: `${window.location.origin}/auth/callback`,
        scopes:
          'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/gmail.send https://www.googleapis.com/auth/calendar.events openid',
      },
    });
    if (error) throw error;
  } catch (error: any) {
    toast({
      title: 'Error connecting with Google',
      description: error.message,
      variant: 'destructive',
    });
    setIsLoading(false);
  }
};

This function:

  • Redirects the user to Google's OAuth screen
  • Requests access to the following scopes:
    • userinfo.email and userinfo.profile – basic identity
    • gmail.send – send emails on behalf of the user
    • calendar.events – create and edit Calendar events
    • openid – required for Supabase to map Google accounts

Understanding the Google OAuth Flow with Supabase

1. User Initiates Login

User clicks "Login with Google". App sends an OAuth request to Supabase with:

  • Google as the provider
  • Required Google scopes
  • access_type=offline + prompt=consent
  • Redirect URL

2. Supabase Generates the Google OAuth URL

Supabase creates the OAuth URL, including:

  • Requested Google scopes
  • Redirect URL
  • Offline access (refresh token)
  • Consent prompt
  • PKCE + state

User is redirected to Google.

Google presents the consent screen. User selects account and approves access.

4. Google Redirects Back to Supabase

Google redirects to Supabase's OAuth callback with:

  • Authorization code
  • State

5. Supabase Exchanges Authorization Code

Supabase sends the code to Google and receives:

  • Google access token (expires ~1 hour)
  • Google refresh token (long-lived)
  • Google ID token
  • Token expiry information

6. Supabase Stores Provider Tokens

Supabase saves the Google provider tokens in its Auth system:

  • provider_token (Google access token)
  • provider_refresh_token
  • expires_at
  • Auth user is created or updated

⚠️ Important: Supabase tokens and Google tokens are separate (explained below).

7. Supabase Redirects Back to Your App

Supabase redirects the user to the frontend callback URL. The frontend retrieves the authenticated session via:

  • Supabase session token (for app authentication)
  • Google provider tokens (for Google API access)

8. App Stores Google Provider Tokens

Your app extracts the provider tokens from the session and saves them into your own oauth_tokens table for long-term use.

This is required because:

  • Supabase does not refresh Google tokens automatically
  • Your backend needs persistent access to call Google APIs

9. User Is Authenticated

User is logged into your app using Supabase session tokens. User is redirected to the Dashboard.

Supabase Token vs Google Token (Important Difference)

Supabase Token

Used for:

  • Authenticating users inside your application
  • Maintaining login state
  • Accessing Supabase services (RLS, tables, storage)

Managed by Supabase:

  • Auto-refreshes
  • Expires & renews without user interaction
  • Used only within your system

Google Token

Used for:

  • Calling Google APIs such as Gmail, Calendar, Drive, People API
  • Sending emails, reading calendars, etc.
  • Backend-only usage

Managed by your backend, not Supabase:

  • Access tokens expire in ~1 hour
  • Refresh tokens remain valid long-term
  • Stored in your oauth_tokens table
  • Refreshed automatically using backend logic

⚠️ Supabase does not refresh Google tokens.

10. Calling Google APIs (Backend Flow)

When your system needs to call a Google API (example: send email):

  1. Your frontend calls your Edge Function (e.g., /send-email)
  2. Edge Function calls getGoogleToken(userId) to obtain a valid access token
  3. getGoogleToken checks if the stored token is expired or about to expire. If it is expired, using Google refresh token, the provider token will be refreshed
  4. The Google API request is executed with the valid access token

All Google API calls go through this backend flow.

Component Implementation

1. AuthCallback Component

This page runs immediately after Supabase returns from Google OAuth.

Purpose:

  • Validate OAuth response
  • Retrieve Supabase session
  • Show loading & toast notifications
  • Redirect based on success/failure

Code:

const AuthCallback = () => {
  const navigate = useNavigate();
  const { toast } = useToast();
  const [statusMessage, setStatusMessage] = useState('Processing login...');

  useEffect(() => {
    const handleCallback = async () => {
      try {
        // 1. Handle potential OAuth errors in callback
        const hashParams = new URLSearchParams(window.location.hash.substring(1));
        const error = hashParams.get('error');
        const errorDescription = hashParams.get('error_description');

        if (error) {
          toast({ title: 'OAuth error', description: errorDescription });
          navigate('/auth');
          return;
        }

        // 2. Retrieve Supabase session
        const { data: { session }, error: sessionError } = await supabase.auth.getSession();
        if (sessionError) throw sessionError;

        // 3. Success → Redirect to dashboard
        if (session) {
          toast({ title: 'Auth Success' });
          setTimeout(() => navigate('/dashboard', { replace: true }), 1200);
        } else {
          toast({ title: 'Auth Failed' });
          setTimeout(() => navigate('/auth', { replace: true }), 2500);
        }
      } catch (err) {
        toast({ title: 'Error Processing' });
        setTimeout(() => navigate('/auth', { replace: true }), 2000);
      }
    };

    handleCallback();
  }, []);

  return <div>Loading...</div>;
};

Why this is needed:

  • Supabase OAuth callback returns using a hash /#access_token=...
  • We must manually detect errors
  • We retrieve session using supabase.auth.getSession()
  • Redirect flows are controlled here

2. useAuth Hook (Global Authentication Manager)

This hook keeps the entire app in sync with Supabase's auth state.

Responsibilities:

  • ✔ Load initial session
  • ✔ Listen for SIGNED_IN/SIGNED_OUT events
  • ✔ Auto-refresh session every 50 minutes
  • ✔ Provide the user and loading state everywhere

Code:

export const useAuth = () => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Initial session check
    const getInitialSession = async () => {
      const { data: { session } } = await supabase.auth.getSession();
      setUser(session?.user ?? null);
      setLoading(false);
    };

    getInitialSession();

    // Listen for auth changes: SIGNED_IN, SIGNED_OUT
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        console.log('Auth state changed:', event);
        setUser(session?.user ?? null);
        setLoading(false);
      }
    );

    // Auto-refresh token every 50 mins
    const refreshInterval = setInterval(async () => {
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) await supabase.auth.refreshSession();
    }, 50 * 60 * 1000);

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

  return { user, loading };
};

Why this is needed:

  • Supabase doesn't automatically sync session state to React
  • Each component can call useAuth() to instantly know:
    • Is user logged in?
    • Is the session still loading?

3. ProtectedRoute Component

Used to secure pages like /profile and /dashboard.

Responsibilities:

  • ✔ Prevents unauthorized access
  • ✔ Redirects to /auth
  • ✔ Shows loading screen during session check

Code:

export const ProtectedRoute = ({ children }) => {
  const { user, loading } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();

  useEffect(() => {
    if (!loading && !user) {
      navigate('/auth', { state: { from: location.pathname } });
    }
  }, [user, loading]);

  if (loading) {
    return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
  }

  if (!user) return null;

  return <>{children}</>;
};

Why this is needed:

  • Prevents unauthorized access
  • Works seamlessly with React Router
  • Blocks rendering until authentication is confirmed

4. Router Configuration

Routes Overview:

const router = createBrowserRouter([
  {
    path: "/",
    children: [
      { index: true, element: <Index /> },
      { path: "auth", element: <Auth /> },
      { path: "auth/callback", element: <AuthCallback /> },
      { 
        path: "profile",
        element: <ProtectedRoute><Profile /></ProtectedRoute>
      },
      { 
        path: "dashboard",
        element: <ProtectedRoute><Dashboard /></ProtectedRoute>
      },
    ],
  },
]);

Key Points:

  • /auth/callback must remain publicly accessible
  • ProtectedRoute wraps sensitive pages
  • / automatically redirects logged-in users to /dashboard

5. Index Page (Public Landing Page)

Responsibilities:

  • ✔ Show a public landing page
  • ✔ Auto-redirect logged-in users to Dashboard
  • ✔ Handle loading state
const Index = () => {
  const navigate = useNavigate();
  const { user, loading } = useAuth();

  useEffect(() => {
    if (!loading && user) navigate('/dashboard');
  }, [user, loading]);

  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-gradient-to-br">
        <div className="animate-spin h-12 w-12 border-b-2 border-primary"></div>
        <p>Loading...</p>
      </div>
    );
  }

  return <div>Public Page (Guest User Page)</div>;
};

Best Practices

  1. Always store Google tokens separately: Don't rely on Supabase to manage Google API tokens
  2. Implement token refresh logic: Google access tokens expire in ~1 hour
  3. Use backend for Google API calls: Never call Google APIs directly from the frontend
  4. Handle errors gracefully: OAuth flows can fail for various reasons
  5. Test with multiple Google accounts: Ensure your scopes work correctly
  6. Monitor token expiration: Set up alerts for token refresh failures

Common Issues and Solutions

Issue: "Redirect URI mismatch"

Solution: Ensure the redirect URI in Google Cloud Console exactly matches Supabase's callback URL.

Issue: "Invalid scope"

Solution: Verify all required APIs are enabled and scopes are correctly added in the OAuth consent screen.

Issue: "Token expired"

Solution: Implement automatic token refresh using the refresh token before making API calls.

Issue: "Access denied"

Solution: Check that test users are added if your app is in testing mode.

Conclusion

Integrating Google OAuth with Supabase for Gmail and Calendar functionality requires careful setup but provides powerful capabilities for your application. By following this guide, you'll have a robust authentication system that enables seamless Google API integration.

Remember to:

  • Keep Google tokens separate from Supabase tokens
  • Implement proper token refresh logic
  • Use backend functions for all Google API calls
  • Test thoroughly with multiple accounts

Happy coding! 🚀

Tags:
React
Supabase
Google OAuth
Gmail API
Calendar API
Authentication
Integration

Want to read more articles?