The Google OAuth Bug That Had Me Chasing Shadows — Until I Noticed This One Line in FastAPI
Isaac Amankwaah Anane
“It worked in Postman… but failed miserably in production.”
That’s how it started. What followed was eight days of debugging, confusion, and finally, the humbling realization that I was the bug.
The Mission
I was working on integrating Google OAuth in my FastAPI backend to authenticate users in a full-stack application. Simple enough:
FastAPI backend
Frontend app in JavaScript (React)
Google Cloud OAuth credentials set up
redirect_uriconfigured properlyLocal dev setup with
http://localhost:1856for frontend andhttp://localhost:8000for backend
Everything was flowing perfectly in Postman. I was getting the authorization code, exchanging it for tokens, and setting a secure cookie.
But from the browser? It just wouldn’t work.
The Flow I Designed
User clicks “Login with Google” on the frontend
Frontend calls
/auth/login/googleon the backendBackend builds the Google OAuth URL and returns it
Frontend redirects the user to that URL
Google does its thing and redirects back to my backend callback
/auth/callback/google
Google Auth Flow with Fast API
Simple, right?
The Bug
Here’s what I actually did on step 3:
# Step 3: Backend receives login request
redirect_response = RedirectResponse(google_oauth_url)
return {"url": redirect_response.headers["location"]} # 👈 This right hereInstead of returning the RedirectResponse object directly (which would cause FastAPI to issue a 307 redirect automatically), I was extracting the location header manually and sending just the URL to the frontend.
So the frontend was like:
const response = await axios.get("/auth/login/google");
window.location.href = response.data.url; // 👈 Redirect manuallySeems fine, right?
Wrong.
That little change broke the entire OAuth flow.
The Symptoms
Callback never triggered properly
Sometimes the
codequery param was missing entirelyCookies wouldn’t set
Tokens wouldn’t exchange
Session got lost
Mismatching OAuth State
In short: everything that made the OAuth dance beautiful was out of sync.
The Realization
The OAuth redirect flow is very sensitive to:
Redirect origins
Browser context
SameSite cookie behavior
Referer headers
And most importantly: who is performing the redirect
By manually extracting the redirect URL and doing window.location.href = ... on the frontend, I broke the browser context and interfered with how cookies, referers, and tokens were expected to behave.
Google expected the server to issue the redirect as a response to a direct browser request. But instead, I had a middleman (frontend JS) breaking the chain.
✅ The Fix
Here’s what I changed:
❌ Before:
return {"url": redirect_response.headers["location"]}✅ After:
return redirect_response # Let FastAPI handle the redirect!Now the frontend just hits:
window.location.href = "/auth/login/google";No need to manually handle any OAuth URL yourself.
Lesson Learned
When using
RedirectResponse, let it redirect.
Don’t unwrap it. Don’t dissect it. Just return it.
OAuth providers like Google rely heavily on browser behavior, not just your code. By over-engineering the redirect, I broke the very thing I was trying to simplify.
Bonus Tips
Return
RedirectResponsedirectly — don’t try to be clever (Like me 😩).In development, turn off
secure=Truefor cookies unless you're using HTTPS.Use tools like OAuth Playground and browser dev tools to trace flows.
Watch out for SameSite settings; cross-origin cookies require care.
Final Code
from authlib.integrations.starlette_client import OAuth
from authlib.integrations.starlette_client import OAuthError
from fastapi import FastAPI
from fastapi import Request
from starlette.config import Config
from starlette.middleware.sessions import SessionMiddleware
from apps.jwt import create_refresh_token
from apps.jwt import CREDENTIALS_EXCEPTION
from apps.jwt import valid_email_from_db
from starlette.responses import RedirectResponse
# Create the auth app
auth_app = FastAPI()
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID'
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET'
config_data = {'GOOGLE_CLIENT_ID': GOOGLE_CLIENT_ID, 'GOOGLE_CLIENT_SECRET': GOOGLE_CLIENT_SECRET}
starlette_config = Config(environ=config_data)
oauth = OAuth(starlette_config)
oauth.register(
name='google',
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'},
)
# Set up the middleware to read the request session
SECRET_KEY = 'SECRET_KEY'
auth_app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY)
FRONTEND_URL = 'http://127.0.0.1:1856/token'
@auth_app.route('/auth/login/google')
async def login(request: Request) -> RedirectResponse:
redirect_uri = FRONTEND_URL # This creates the url for our /auth endpoint
return await oauth.google.authorize_redirect(request, redirect_uri)
@auth_app.route('/auth/callback/google')
async def auth(request: Request) -> RedirectResponse:
redirect_url = f"{FRONTEND_URL}/home"
try:
access_token = await oauth.google.authorize_access_token(request)
except OAuthError:
raise CREDENTIALS_EXCEPTION
user_data = await oauth.google.parse_id_token(request, access_token)
if valid_email_from_db(user_data['email']):
response = RedirectResponse(redirect_url)
response.set_cookie(key="refresh_token", value=create_refresh_token(user_data['email']))
return response
return RedirectResponse(redirect_url)