Merge pull request #659 from MODSetter/dev

try: oauth_errors
This commit is contained in:
Rohan Verma 2026-01-02 00:26:20 -08:00 committed by GitHub
commit ae1c3f954e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 78 additions and 25 deletions

View file

@ -1,6 +1,6 @@
from contextlib import asynccontextmanager 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 fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
@ -107,6 +107,8 @@ app.include_router(
) )
if config.AUTH_TYPE == "GOOGLE": if config.AUTH_TYPE == "GOOGLE":
from fastapi.responses import RedirectResponse
from app.users import google_oauth_client from app.users import google_oauth_client
# Determine if we're in a secure context (HTTPS) or local development (HTTP) # 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) # For same-origin or local development, use SameSite=Lax (default)
csrf_cookie_samesite = "none" if is_secure_context else "lax" 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( app.include_router(
fastapi_users.get_oauth_router( fastapi_users.get_oauth_router(
google_oauth_client, google_oauth_client,
@ -127,6 +138,7 @@ if config.AUTH_TYPE == "GOOGLE":
is_verified_by_default=True, is_verified_by_default=True,
csrf_token_cookie_secure=is_secure_context, csrf_token_cookie_secure=is_secure_context,
csrf_token_cookie_samesite=csrf_cookie_samesite, 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 if not config.BACKEND_URL
else fastapi_users.get_oauth_router( else fastapi_users.get_oauth_router(
@ -137,6 +149,8 @@ if config.AUTH_TYPE == "GOOGLE":
redirect_url=f"{config.BACKEND_URL}/auth/google/callback", redirect_url=f"{config.BACKEND_URL}/auth/google/callback",
csrf_token_cookie_secure=is_secure_context, csrf_token_cookie_secure=is_secure_context,
csrf_token_cookie_samesite=csrf_cookie_samesite, 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", prefix="/auth/google",
tags=["auth"], tags=["auth"],
@ -145,6 +159,62 @@ if config.AUTH_TYPE == "GOOGLE":
], # blocks OAuth registration when disabled ], # 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"]) app.include_router(crud_router, prefix="/api/v1", tags=["crud"])

View file

@ -3,7 +3,7 @@ import { IconBrandGoogleFilled } from "@tabler/icons-react";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Logo } from "@/components/Logo"; import { Logo } from "@/components/Logo";
import { trackLoginAttempt, trackLoginFailure } from "@/lib/posthog/events"; import { trackLoginAttempt } from "@/lib/posthog/events";
import { AmbientBackground } from "./AmbientBackground"; import { AmbientBackground } from "./AmbientBackground";
export function GoogleLoginButton() { export function GoogleLoginButton() {
@ -13,29 +13,12 @@ export function GoogleLoginButton() {
// Track Google login attempt // Track Google login attempt
trackLoginAttempt("google"); trackLoginAttempt("google");
// Redirect to Google OAuth authorization URL // IMPORTANT: Use the redirect-based authorize endpoint for cross-origin OAuth
// credentials: 'include' is required to accept the CSRF cookie from cross-origin response // This fixes CSRF cookie issues in Firefox/Safari where cookies set via
fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/google/authorize`, { // cross-origin fetch requests may not be sent on subsequent redirects.
credentials: "include", // The authorize-redirect endpoint does a server-side redirect to Google
}) // and sets the CSRF cookie properly for same-site context.
.then((response) => { window.location.href = `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/google/authorize-redirect`;
if (!response.ok) {
throw new Error("Failed to get authorization URL");
}
return response.json();
})
.then((data) => {
if (data.authorization_url) {
window.location.href = data.authorization_url;
} else {
trackLoginFailure("google", "No authorization URL received");
console.error("No authorization URL received");
}
})
.catch((error) => {
trackLoginFailure("google", error?.message || "Unknown error");
console.error("Error during Google login:", error);
});
}; };
return ( return (
<div className="relative w-full overflow-hidden"> <div className="relative w-full overflow-hidden">