Merge pull request #1044 from escapade-mckv/fix-google-oauth

attempt to fix google oauth
This commit is contained in:
Bobbie 2025-07-22 22:15:37 +05:30 committed by GitHub
commit 9ec1a4e5ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 313 additions and 168 deletions

156
docs/GOOGLE_OAUTH_SETUP.md Normal file
View File

@ -0,0 +1,156 @@
# Google OAuth Setup Guide
This guide will help you configure Google Sign-In for your Suna application to avoid common errors like "Access blocked: This app's request is invalid".
## Prerequisites
- A Google Cloud Console account
- Your Supabase project URL and anon key
## Step 1: Create a Google Cloud Project
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Click "Select a project" → "New Project"
3. Enter a project name (e.g., "Suna App")
4. Click "Create"
## Step 2: Configure OAuth Consent Screen
1. In the Google Cloud Console, go to "APIs & Services" → "OAuth consent screen"
2. Select "External" user type (unless you have a Google Workspace account)
3. Click "Create"
4. Fill in the required fields:
- **App name**: Your application name (e.g., "Suna")
- **User support email**: Your email address
- **App logo**: Optional, but recommended
- **App domain**: Your domain (for local dev, skip this)
- **Authorized domains**: Add your domain(s)
- **Developer contact information**: Your email address
5. Click "Save and Continue"
6. **Scopes**: Click "Add or Remove Scopes"
- Select `.../auth/userinfo.email`
- Select `.../auth/userinfo.profile`
- Select `openid`
- Click "Update"
7. Click "Save and Continue"
8. **Test users**: Add test email addresses if in testing mode
9. Click "Save and Continue"
10. Review and click "Back to Dashboard"
## Step 3: Create OAuth 2.0 Credentials
1. Go to "APIs & Services" → "Credentials"
2. Click "Create Credentials" → "OAuth client ID"
3. Select "Web application" as the application type
4. Configure the client:
- **Name**: "Suna Web Client" (or any name you prefer)
- **Authorized JavaScript origins**:
- Add `http://localhost:3000` (for local development)
- Add `https://yourdomain.com` (for production)
- Add your Supabase URL (e.g., `https://yourproject.supabase.co`)
- **Authorized redirect URIs**:
- Add `http://localhost:3000/auth/callback` (for local development)
- Add `https://yourdomain.com/auth/callback` (for production)
- Add your Supabase auth callback URL: `https://yourproject.supabase.co/auth/v1/callback`
5. Click "Create"
6. **Important**: Copy the "Client ID" - you'll need this
## Step 4: Configure Supabase
1. Go to your [Supabase Dashboard](https://app.supabase.com)
2. Select your project
3. Go to "Authentication" → "Providers"
4. Find "Google" and enable it
5. Add your Google OAuth credentials:
- **Client ID**: Paste the Client ID from Step 3
- **Client Secret**: Leave empty (not needed for web applications)
6. **Authorized Client IDs**: Add your Client ID here as well
7. Click "Save"
## Step 5: Configure Your Application
1. Add the Google Client ID to your environment variables:
**Frontend** (`frontend/.env.local`):
```env
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-client-id-here
```
2. Restart your development server
## Step 6: Test Your Setup
1. Open your application in a browser
2. Click the "Continue with Google" button
3. You should see the Google sign-in popup
4. Select an account and authorize the application
5. You should be redirected back to your application and logged in
## Common Issues and Solutions
### "Access blocked: This app's request is invalid"
This error usually means:
- **Missing redirect URI**: Make sure all your redirect URIs are added in Google Cloud Console
- **Wrong Client ID**: Verify you're using the correct Client ID
- **OAuth consent screen not configured**: Complete all required fields in the consent screen
### "redirect_uri_mismatch"
- Check that your redirect URIs in Google Cloud Console exactly match your application URLs
- Include the protocol (`http://` or `https://`)
- Don't include trailing slashes
- For local development, use `http://localhost:3000`, not `http://127.0.0.1:3000`
### "invalid_client"
- Verify your Client ID is correct in the environment variables
- Make sure you're using the Web application client ID, not a different type
- Check that the OAuth client hasn't been deleted in Google Cloud Console
### Google button doesn't appear
- Check browser console for errors
- Verify `NEXT_PUBLIC_GOOGLE_CLIENT_ID` is set in your environment
- Make sure the Google Identity Services script is loading
## Production Deployment
When deploying to production:
1. Update Google Cloud Console:
- Add your production domain to "Authorized JavaScript origins"
- Add your production callback URL to "Authorized redirect URIs"
- Update the OAuth consent screen with production information
2. Update your production environment variables:
- Set `NEXT_PUBLIC_GOOGLE_CLIENT_ID` in your deployment platform
3. Verify Supabase settings:
- Ensure Google provider is enabled
- Confirm the Client ID is set correctly
## Security Best Practices
1. **Never commit your Client ID to version control** - always use environment variables
2. **Use HTTPS in production** - Google requires secure connections for OAuth
3. **Restrict your OAuth client** - Only add the domains you actually use
4. **Review permissions regularly** - Remove unused test users and unnecessary scopes
5. **Monitor usage** - Check Google Cloud Console for unusual activity
## Publishing Your App
If you want to remove the "unverified app" warning:
1. Go to "OAuth consent screen" in Google Cloud Console
2. Click "Publish App"
3. Google may require verification for certain scopes
4. Follow the verification process if required
## Need Help?
If you're still experiencing issues:
1. Check the browser console for detailed error messages
2. Verify all URLs and IDs are correctly copied
3. Ensure your Supabase project is properly configured
4. Try using an incognito/private browser window to rule out cache issues

View File

@ -1,196 +1,197 @@
'use client';
import { useEffect, useCallback, useState } from 'react';
import { useEffect, useState } from 'react';
import Script from 'next/script';
import { createClient } from '@/lib/supabase/client';
import { useAuthMethodTracking } from '@/lib/stores/auth-tracking';
import { toast } from 'sonner';
import { FcGoogle } from "react-icons/fc";
import { Loader2 } from 'lucide-react';
import { toast } from 'sonner';
declare global {
interface Window {
handleGoogleSignIn?: (response: GoogleSignInResponse) => void;
google: {
google?: {
accounts: {
id: {
initialize: (config: GoogleInitializeConfig) => void;
prompt: (
callback?: (notification: GoogleNotification) => void,
) => void;
cancel: () => void;
initialize: (config: any) => void;
renderButton: (element: HTMLElement, config: any) => void;
prompt: (notification?: (notification: any) => void) => void;
};
};
};
}
}
interface GoogleSignInResponse {
credential: string;
clientId?: string;
select_by?: string;
}
interface GoogleInitializeConfig {
client_id: string | undefined;
callback: ((response: GoogleSignInResponse) => void) | undefined;
nonce?: string;
use_fedcm?: boolean;
context?: string;
itp_support?: boolean;
}
interface GoogleNotification {
isNotDisplayed: () => boolean;
getNotDisplayedReason: () => string;
isSkippedMoment: () => boolean;
getSkippedReason: () => string;
isDismissedMoment: () => boolean;
getDismissedReason: () => string;
}
interface GoogleSignInProps {
returnUrl?: string;
}
export default function GoogleSignIn({ returnUrl }: GoogleSignInProps) {
const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
const [isLoading, setIsLoading] = useState(false);
const [isGoogleLoaded, setIsGoogleLoaded] = useState(false);
const { wasLastMethod, markAsUsed } = useAuthMethodTracking('google');
const supabase = createClient();
const handleGoogleSignIn = useCallback(
async (response: GoogleSignInResponse) => {
const handleGoogleResponse = async (response: any) => {
try {
setIsLoading(true);
console.log('Starting Google sign in process');
markAsUsed();
const { error } = await supabase.auth.signInWithIdToken({
const { data, error } = await supabase.auth.signInWithIdToken({
provider: 'google',
token: response.credential,
});
if (error) throw error;
if (error) {
const redirectTo = `${window.location.origin}/auth/callback${returnUrl ? `?returnUrl=${encodeURIComponent(returnUrl)}` : ''}`;
console.log('OAuth redirect URI:', redirectTo);
console.log(
'Google sign in successful, preparing redirect to:',
returnUrl || '/dashboard',
);
setTimeout(() => {
console.log('Executing redirect now to:', returnUrl || '/dashboard');
window.location.href = returnUrl || '/dashboard';
}, 500);
} catch (error) {
console.error('Error signing in with Google:', error);
setIsLoading(false);
toast.error('Google sign-in failed. Please try again.');
}
const { error: oauthError } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo,
},
[returnUrl, markAsUsed],
});
if (oauthError) {
throw oauthError;
}
} else {
window.location.href = returnUrl || '/dashboard';
}
} catch (error: any) {
console.error('Google sign-in error:', error);
if (error.message?.includes('redirect_uri_mismatch')) {
const redirectUri = `${window.location.origin}/auth/callback`;
toast.error(
`Google OAuth configuration error. Add this exact URL to your Google Cloud Console: ${redirectUri}`,
{ duration: 10000 }
);
} else {
toast.error(error.message || 'Failed to sign in with Google');
}
const handleCustomGoogleSignIn = useCallback(() => {
if (isLoading) return;
setIsLoading(false);
}
};
if (!window.google || !googleClientId || !isGoogleLoaded) {
console.error('Google sign-in not properly initialized');
toast.error('Google sign-in not ready. Please try again.');
useEffect(() => {
const initializeGoogleSignIn = () => {
if (!window.google || !process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) return;
window.google.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
callback: handleGoogleResponse,
auto_select: false,
cancel_on_tap_outside: false,
});
setIsGoogleLoaded(true);
};
if (window.google) {
initializeGoogleSignIn();
}
}, [returnUrl, markAsUsed, supabase]);
const handleScriptLoad = () => {
if (window.google && process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) {
window.google.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
callback: handleGoogleResponse,
auto_select: false,
cancel_on_tap_outside: false,
});
setIsGoogleLoaded(true);
}
};
const handleGoogleSignIn = () => {
if (!window.google || !isGoogleLoaded) {
toast.error('Google Sign-In is still loading. Please try again.');
return;
}
try {
window.google.accounts.id.prompt((notification: any) => {
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
console.log('One Tap not displayed, using OAuth flow');
setIsLoading(true);
const timeoutId = setTimeout(() => {
console.log('Google sign-in timeout - resetting loading state');
setIsLoading(false);
toast.error('Google sign-in popup failed to load. Please try again.');
}, 5000);
window.google.accounts.id.prompt(async (notification) => {
clearTimeout(timeoutId);
const redirectTo = `${window.location.origin}/auth/callback${returnUrl ? `?returnUrl=${encodeURIComponent(returnUrl)}` : ''}`;
console.log('OAuth redirect URI:', redirectTo);
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
console.log('Google sign-in was not displayed or skipped:',
notification.isNotDisplayed() ? notification.getNotDisplayedReason() : notification.getSkippedReason()
);
await supabase.auth.signInWithOAuth({
supabase.auth.signInWithOAuth({
provider: 'google',
options: {
queryParams: {
prompt: 'select_account',
},
redirectTo: `${window.location.origin}/dashboard`,
redirectTo,
},
}).then(({ error }) => {
if (error) {
console.error('OAuth error:', error);
if (error.message?.includes('redirect_uri_mismatch')) {
const redirectUri = `${window.location.origin}/auth/callback`;
toast.error(
`Google OAuth configuration error. Add this exact URL to your Google Cloud Console: ${redirectUri}`,
{ duration: 10000 }
);
} else {
toast.error(error.message || 'Failed to sign in with Google');
}
setIsLoading(false);
}
});
setIsLoading(false);
} else if (notification.isDismissedMoment()) {
console.log('Google sign-in was dismissed:', notification.getDismissedReason());
setIsLoading(false);
}
});
} catch (error) {
console.error('Error showing Google sign-in prompt:', error);
console.error('Error triggering Google sign-in:', error);
setIsLoading(true);
const redirectTo = `${window.location.origin}/auth/callback${returnUrl ? `?returnUrl=${encodeURIComponent(returnUrl)}` : ''}`;
supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo,
},
}).then(({ error }) => {
if (error) {
console.error('OAuth error:', error);
if (error.message?.includes('redirect_uri_mismatch')) {
const redirectUri = `${window.location.origin}/auth/callback`;
toast.error(
`Google OAuth configuration error. Add this exact URL to your Google Cloud Console: ${redirectUri}`,
{ duration: 10000 }
);
} else {
toast.error(error.message || 'Failed to sign in with Google');
}
setIsLoading(false);
toast.error('Failed to start Google sign-in. Please try again.');
}
}, [googleClientId, isGoogleLoaded, isLoading]);
useEffect(() => {
window.handleGoogleSignIn = handleGoogleSignIn;
if (window.google && googleClientId && !isGoogleLoaded) {
window.google.accounts.id.initialize({
client_id: googleClientId,
callback: handleGoogleSignIn,
use_fedcm: true,
context: 'signin',
itp_support: true,
});
setIsGoogleLoaded(true);
}
return () => {
delete window.handleGoogleSignIn;
};
}, [googleClientId, handleGoogleSignIn, isGoogleLoaded]);
if (!googleClientId) {
if (!process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) {
return (
<button
disabled
className="w-full h-12 flex items-center justify-center gap-2 text-sm font-medium tracking-wide rounded-full bg-background border border-border opacity-60 cursor-not-allowed"
>
<FcGoogle className="w-4 h-4 mr-2" />
Google Sign-In Not Configured
</button>
<div className="w-full text-center text-sm text-gray-500 py-3">
Google Sign-In not configured
</div>
);
}
return (
<>
<div
id="g_id_onload"
data-client_id={googleClientId}
data-context="signin"
data-ux_mode="popup"
data-auto_prompt="false"
data-itp_support="true"
data-callback="handleGoogleSignIn"
style={{ display: 'none' }}
/>
<div className="relative">
<button
onClick={handleCustomGoogleSignIn}
onClick={handleGoogleSignIn}
disabled={isLoading || !isGoogleLoaded}
className="w-full h-12 flex items-center justify-center text-sm font-medium tracking-wide rounded-full bg-background text-foreground border border-border hover:bg-accent/30 transition-all duration-200 disabled:opacity-60 disabled:cursor-not-allowed font-sans"
aria-label={
isLoading ? 'Signing in with Google...' : 'Sign in with Google'
}
aria-label={isLoading ? 'Signing in with Google...' : 'Sign in with Google'}
type="button"
>
{isLoading ? (
@ -208,24 +209,12 @@ export default function GoogleSignIn({ returnUrl }: GoogleSignInProps) {
<div className="w-full h-full bg-green-500 rounded-full animate-pulse" />
</div>
)}
</div>
<Script
src="https://accounts.google.com/gsi/client"
strategy="afterInteractive"
onLoad={() => {
if (window.google && googleClientId && !isGoogleLoaded) {
window.google.accounts.id.initialize({
client_id: googleClientId,
callback: handleGoogleSignIn,
use_fedcm: true,
context: 'signin',
itp_support: true,
});
setIsGoogleLoaded(true);
}
}}
onLoad={handleScriptLoad}
/>
</>
</div>
);
}