feat: implement redirect-based OAuth authorization for Google login

- Added a new endpoint `/auth/google/authorize-redirect` to handle OAuth authorization via server-side redirect, addressing CSRF cookie issues in Firefox/Safari.
- Updated the `GoogleLoginButton` component to use the new redirect endpoint instead of the previous JSON-based authorization method.
- Enhanced CSRF cookie handling by explicitly setting the cookie domain and ensuring compatibility with cross-origin requests.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-01-02 00:25:02 -08:00
parent a6b0400858
commit 9029f5c621
2 changed files with 78 additions and 25 deletions

View file

@ -1,6 +1,6 @@
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
@ -107,6 +107,8 @@ app.include_router(
)
if config.AUTH_TYPE == "GOOGLE":
from fastapi.responses import RedirectResponse
from app.users import google_oauth_client
# Determine if we're in a secure context (HTTPS) or local development (HTTP)
@ -119,6 +121,15 @@ if config.AUTH_TYPE == "GOOGLE":
# For same-origin or local development, use SameSite=Lax (default)
csrf_cookie_samesite = "none" if is_secure_context else "lax"
# Extract the domain from BACKEND_URL for cookie domain setting
# This helps with cross-site cookie issues in Firefox/Safari
csrf_cookie_domain = None
if config.BACKEND_URL:
from urllib.parse import urlparse
parsed_url = urlparse(config.BACKEND_URL)
csrf_cookie_domain = parsed_url.hostname
app.include_router(
fastapi_users.get_oauth_router(
google_oauth_client,
@ -127,6 +138,7 @@ if config.AUTH_TYPE == "GOOGLE":
is_verified_by_default=True,
csrf_token_cookie_secure=is_secure_context,
csrf_token_cookie_samesite=csrf_cookie_samesite,
csrf_token_cookie_httponly=False, # Required for cross-site OAuth in Firefox/Safari
)
if not config.BACKEND_URL
else fastapi_users.get_oauth_router(
@ -137,6 +149,8 @@ if config.AUTH_TYPE == "GOOGLE":
redirect_url=f"{config.BACKEND_URL}/auth/google/callback",
csrf_token_cookie_secure=is_secure_context,
csrf_token_cookie_samesite=csrf_cookie_samesite,
csrf_token_cookie_httponly=False, # Required for cross-site OAuth in Firefox/Safari
csrf_token_cookie_domain=csrf_cookie_domain, # Explicitly set cookie domain
),
prefix="/auth/google",
tags=["auth"],
@ -145,6 +159,62 @@ if config.AUTH_TYPE == "GOOGLE":
], # blocks OAuth registration when disabled
)
# Add a redirect-based authorize endpoint for Firefox/Safari compatibility
# This endpoint performs a server-side redirect instead of returning JSON
# which fixes cross-site cookie issues where browsers don't send cookies
# set via cross-origin fetch requests on subsequent redirects
@app.get("/auth/google/authorize-redirect", tags=["auth"])
async def google_authorize_redirect(
request: Request,
):
"""
Redirect-based OAuth authorization endpoint.
Unlike the standard /auth/google/authorize endpoint that returns JSON,
this endpoint directly redirects the browser to Google's OAuth page.
This fixes CSRF cookie issues in Firefox and Safari where cookies set
via cross-origin fetch requests are not sent on subsequent redirects.
"""
import secrets
from fastapi_users.router.oauth import generate_state_token
# Generate CSRF token
csrf_token = secrets.token_urlsafe(32)
# Build state token
state_data = {"csrftoken": csrf_token}
state = generate_state_token(state_data, SECRET, lifetime_seconds=3600)
# Get the callback URL
if config.BACKEND_URL:
redirect_url = f"{config.BACKEND_URL}/auth/google/callback"
else:
redirect_url = str(request.url_for("oauth:google.jwt.callback"))
# Get authorization URL from Google
authorization_url = await google_oauth_client.get_authorization_url(
redirect_url,
state,
scope=["openid", "email", "profile"],
)
# Create redirect response and set CSRF cookie
response = RedirectResponse(url=authorization_url, status_code=302)
response.set_cookie(
key="fastapiusersoauthcsrf",
value=csrf_token,
max_age=3600,
path="/",
domain=csrf_cookie_domain,
secure=is_secure_context,
httponly=False, # Required for cross-site OAuth in Firefox/Safari
samesite=csrf_cookie_samesite,
)
return response
app.include_router(crud_router, prefix="/api/v1", tags=["crud"])