mirror of https://github.com/kortix-ai/suna.git
Merge pull request #1044 from escapade-mckv/fix-google-oauth
attempt to fix google oauth
This commit is contained in:
commit
9ec1a4e5ae
|
@ -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
|
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue