Google OAuth Integration with React and Supabase
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
- Open your Supabase Dashboard
- Navigate to Authentication → Providers → Google
- Paste your Client ID and Client Secret
- Click Save
- 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
- Go to Google Cloud Console
- Click the Project Selector from the top menu
- Click New Project
- Name your project and click Create
- Select your new project from the top bar
Step 3: Enable Required APIs
-
Navigate to APIs & Services → Library
-
Enable the following APIs:
- ✅ Gmail API
- ✅ Google Calendar API
-
Confirm they appear under APIs & Services → Enabled APIs & services
💡 Enabling an API allows your app to make calls to that service.
Step 4: Configure OAuth Consent Screen
-
Go to Google Auth Platform → OAuth Consent Screen
-
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
-
Click Save and Continue
Step 5: Create OAuth Client Credentials
- Go to APIs & Services → Credentials
- Click + Create Credentials → OAuth client ID
- Choose Application Type: Web Application
- Provide a recognizable name for your OAuth client
- Under Authorized redirect URIs, add:
https://<your-supabase-project>.supabase.co/auth/v1/callback - Click Create
- Important: Note down the Client ID and Client Secret - you'll need them for Supabase
Step 6: Add Scopes (Data Access Permissions)
-
In the left sidebar, go to Google Auth Platform → Data Access
-
Click the blue "Add or remove scopes" button
-
A panel called "Update selected scopes" will open
-
Make sure the required APIs are already enabled under APIs & Services → Library
- Enable ✅ Gmail API
- Enable ✅ Google Calendar API
-
Inside the Update selected scopes panel, use the search bar to filter for each API:
- Type
gmailto see available Gmail scopes - Type
calendarto see available Calendar scopes
- Type
-
If you don't see the scopes you need, scroll to the bottom to find "Manually add scopes"
-
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 -
Click Add scopes → then Save
-
Your newly added scopes will appear under "Your sensitive scopes" because they allow access to private user data (Gmail and Calendar)
Required Scopes Summary
| API | Scope | Purpose |
|---|---|---|
| Gmail API | https://www.googleapis.com/auth/gmail.send | Send emails on user's behalf |
| Google Calendar API | https://www.googleapis.com/auth/calendar.events | Create and edit calendar events |
Step 7: Add Test Users (if in Testing Mode)
- In OAuth Consent Screen, go to Test Users
- Click + Add Users
- Enter your Gmail and team emails
- 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.emailanduserinfo.profile– basic identitygmail.send– send emails on behalf of the usercalendar.events– create and edit Calendar eventsopenid– 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.
3. User Approves Google Consent
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_tokenexpires_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_tokenstable - 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):
- Your frontend calls your Edge Function (e.g.,
/send-email) - Edge Function calls
getGoogleToken(userId)to obtain a valid access token getGoogleTokenchecks if the stored token is expired or about to expire. If it is expired, using Google refresh token, the provider token will be refreshed- 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/callbackmust remain publicly accessibleProtectedRoutewraps 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
- Always store Google tokens separately: Don't rely on Supabase to manage Google API tokens
- Implement token refresh logic: Google access tokens expire in ~1 hour
- Use backend for Google API calls: Never call Google APIs directly from the frontend
- Handle errors gracefully: OAuth flows can fail for various reasons
- Test with multiple Google accounts: Ensure your scopes work correctly
- 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! 🚀
Want to read more articles?