🚀 The Developer's Survival Guide to Google OAuth

🚀 The Developer's Survival Guide to Google OAuth

Blog

Or: How I Learned to Stop Worrying and Love the Scope

Table of Contents

  1. Introduction: The OAuth Dance
  2. The Three Horsemen of OAuth Errors
  3. Best Practices: OAuth Wisdom
  4. The Ultimate Troubleshooting Checklist
  5. Bonus: Quick Reference Card

Introduction: The OAuth Dance

Picture this: You're building a sleek email client. Everything's going great until you hit the authentication wall. "How hard can OAuth be?" you think. Famous last words.

Google OAuth is like a dance - one wrong step and you're stepping on toes (or in our case, getting cryptic error messages). This guide chronicles real errors encountered while building FluffyMail and how to dodge them like a pro.

The OAuth Flow (The Choreography)

User: "I want to sign in!"
    ↓
Your App → Google: "Hey, can this user sign in?"
    ↓
Google → User: "Do you trust this app?"
    ↓
User → Google: "Yes!" 
    ↓
Google → Your App: "Here's a code"
    ↓
Your App → Google: "Here's the code, give me tokens!"
    ↓
Google → Your App: "Here are your tokens"
    ↓
Your App: "Sweet! Let me get user info..."
    ↓
[ERROR OCCURS HERE] 🔥

The Three Horsemen of OAuth Errors

1. The Case of the Missing Import

The Crime Scene:

ImportError: cannot import name 'TokenRequest' from 'gmail_backend.models'

What Happened: You defined a beautiful TokenRequest class in models/auth.py, but forgot to tell Python it exists. It's like inviting someone to a party but forgetting to give them the address.

The Fix:

# models/__init__.py
from .auth import AuthRequest, AuthResponse, TokenRequest, TokenResponse  # Don't forget me!

__all__ = [
    "AuthRequest", "AuthResponse", "TokenRequest", "TokenResponse",
    # ... other exports
]

Life Lesson: Always check your __init__.py files. They're like the guest list at a club - if you're not on it, you're not getting in.

2. The Mystery of the Vanishing ID

The Crime Scene:

Authentication failed: 'id'

The Investigation: You confidently write:

user_id = user_info["id"]  # BOOM! 💥

But Google's API returns:

{
  "sub": "1234567890",  // <-- The ID is here!
  "email": "user@gmail.com",
  "name": "John Doe"
  // Notice: no "id" field!
}

The Fix:

# Be flexible - Google might use 'id' or 'sub'
user_id = user_info.get("id") or user_info.get("sub")
if not user_id:
    raise ValueError(f"No user ID found in response: {user_info}")

Pro Tip: Always print API responses during development:

print(f"User info response: {user_info}")  # Your debugging best friend

3. The Scope That Wouldn't Stay Put

The Crime Scene:

Scope has changed from "X" to "X + openid"

The Plot Twist: You request these scopes:

SCOPES = [
    'https://www.googleapis.com/auth/userinfo.email',
    'https://www.googleapis.com/auth/gmail.modify'
]

But Google says: "Hey, I'm adding 'openid' whether you like it or not!"

The Fix:

SCOPES = [
    'openid',  # Just embrace it!
    'https://www.googleapis.com/auth/userinfo.email',
    'https://www.googleapis.com/auth/userinfo.profile',
    'https://www.googleapis.com/auth/gmail.modify',
    'https://www.googleapis.com/auth/gmail.compose',
    'https://www.googleapis.com/auth/gmail.send'
]

The Secret Sauce: Change this:

prompt='consent'  # Shows consent screen EVERY TIME

To this:

prompt='select_account'  # Only shows account selection

Best Practices: OAuth Wisdom

1. The Golden Rules of Scopes

# ❌ Don't do this:
SCOPES = ['https://www.googleapis.com/auth/gmail']  # Too broad!

# ✅ Do this:
SCOPES = [
    'openid',  # Always include this first
    'https://www.googleapis.com/auth/userinfo.email',  # Get email
    'https://www.googleapis.com/auth/userinfo.profile',  # Get name/picture
    'https://www.googleapis.com/auth/gmail.modify',  # Read/write emails
    'https://www.googleapis.com/auth/gmail.compose',  # Create drafts
    'https://www.googleapis.com/auth/gmail.send'  # Send emails
]

2. Error Handling Like a Boss

try:
    # OAuth magic here
    tokens = gmail_auth.exchange_code_for_tokens(code, state)
    
    # Get user info
    response = await client.get(
        "https://www.googleapis.com/oauth2/v2/userinfo",
        headers={"Authorization": f"Bearer {tokens['access_token']}"}
    )
    response.raise_for_status()  # Don't forget this!
    user_info = response.json()
    
except httpx.HTTPError as e:
    # Network/API errors
    logger.error(f"API call failed: {e}")
    return {"error": "Google API is having a bad day"}
    
except KeyError as e:
    # Missing fields
    logger.error(f"Missing field in response: {e}")
    return {"error": f"Google forgot to send us: {e}"}
    
except Exception as e:
    # Everything else
    logger.error(f"OAuth failed spectacularly: {e}")
    return {"error": "Something went wrong. It's not you, it's us."}

3. Frontend-Backend Harmony

Backend redirect:

# Always encode error messages!
return RedirectResponse(
    url=f"{redirect_uri}?error={urllib.parse.quote(str(e))}"
)

Frontend handling:

// Check for errors FIRST
const error = searchParams.get('error');
if (error) {
    console.error('Auth failed:', decodeURIComponent(error));
    showUserFriendlyError(error);
    return;
}

// Then handle success
const token = searchParams.get('token');
if (token) {
    localStorage.setItem('auth_token', token);
    router.push('/dashboard');
}

4. The Art of Token Storage

# Redis with expiration - tokens don't live forever!
self.redis_client.setex(
    f"gmail:tokens:{user_id}",
    timedelta(days=30),  # Refresh before this!
    json.dumps(tokens)
)

The Ultimate Troubleshooting Checklist

When OAuth fails, go through this list:

🔍 Pre-Flight Checks

  • [ ] Are your client ID and secret correct? (Not "your-client-id-here")
  • [ ] Is your redirect URI registered in Google Console?
  • [ ] Does your redirect URI match EXACTLY? (http vs https matters!)
  • [ ] Are you on the correct port? (localhost:3000 vs localhost:3001)

🐛 Debug Mode Activated

# Add these temporarily
print(f"Auth URL: {auth_url}")
print(f"Redirect URI: {self.redirect_uri}")
print(f"Scopes: {self.SCOPES}")
print(f"User info response: {user_info}")
print(f"Token response: {tokens}")

🚨 Common Gotchas

  1. "Invalid redirect URI" → Check trailing slashes!
  2. "Access blocked" → App might be in test mode
  3. "Scope has changed" → Add 'openid' scope
  4. "Invalid grant" → Token expired or already used
  5. "'id' key error" → Use .get() not []

🛠️ The Nuclear Option

When all else fails:

  1. Go to https://myaccount.google.com/permissions
  2. Revoke access to your app
  3. Clear browser cookies
  4. Try again with fingers crossed

Quick Reference Card

Essential URLs

# OAuth endpoints
AUTH_URL = "https://accounts.google.com/o/oauth2/auth"
TOKEN_URL = "https://oauth2.googleapis.com/token"
USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo"

# Your app
BACKEND_CALLBACK = "http://localhost:8000/auth/callback"
FRONTEND_SUCCESS = "http://localhost:3001/auth/success"

The Minimal Working Setup

# 1. Scopes (in order!)
SCOPES = [
    'openid',
    'https://www.googleapis.com/auth/userinfo.email',
    'https://www.googleapis.com/auth/userinfo.profile',
    # Add your app-specific scopes here
]

# 2. Flow configuration
flow = Flow.from_client_config(
    client_config,
    scopes=SCOPES,
    redirect_uri=redirect_uri
)

# 3. Authorization URL
auth_url, state = flow.authorization_url(
    access_type='offline',  # Get refresh token
    include_granted_scopes='true',  # Incremental auth
    prompt='select_account'  # Not 'consent'!
)

# 4. Token exchange
flow.fetch_token(code=code)

# 5. Get user info (with error handling!)
user_id = user_info.get("id") or user_info.get("sub")

Emergency Contacts


Final Words of Wisdom

OAuth is like cooking - follow the recipe exactly the first time, then experiment once you know what you're doing. And remember:

  1. Always handle errors gracefully - Your users don't need to see stack traces
  2. Log everything during development - Future you will thank present you
  3. Test with multiple Google accounts - Your personal account might have special permissions
  4. Keep your secrets secret - Never commit credentials to Git

Happy authenticating! May your tokens always be fresh and your scopes never change unexpectedly. 🎉


P.S. If you found this guide helpful, you've probably spent too much time debugging OAuth. Welcome to the club! ☕