mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-04 22:02:16 +02:00
feat(oauth): migrate Google OAuth account IDs to use 'sub' and enhance user resolution logic
This commit is contained in:
parent
eb76c02d43
commit
e5aded5a65
3 changed files with 298 additions and 78 deletions
|
|
@ -7,6 +7,7 @@ from urllib.parse import urlparse
|
|||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||
from fastapi_users import exceptions as fastapi_users_exceptions
|
||||
from google.auth.transport import requests as google_requests
|
||||
from google.oauth2 import id_token as google_id_token
|
||||
from sqlalchemy import select
|
||||
|
|
@ -58,6 +59,68 @@ async def _load_user(user_id) -> User | None:
|
|||
return result.scalars().first()
|
||||
|
||||
|
||||
async def resolve_google_user(
|
||||
*,
|
||||
user_manager: UserManager,
|
||||
request: Request,
|
||||
google_access_token: str,
|
||||
claims: dict,
|
||||
expires_at: int | None = None,
|
||||
google_refresh_token: str | None = None,
|
||||
) -> User:
|
||||
"""Resolve a Google identity with one policy for web and desktop OAuth.
|
||||
|
||||
Email-based account linking is only allowed when Google asserts that the
|
||||
email is verified. Existing OAuth accounts continue to resolve by provider
|
||||
account id regardless of the current email claim.
|
||||
"""
|
||||
if not claims.get("sub") or not claims.get("email"):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Google identity token")
|
||||
|
||||
sub = claims["sub"]
|
||||
email_verified = bool(claims.get("email_verified"))
|
||||
|
||||
canonical_user = await user_manager.user_db.get_by_oauth_account("google", sub)
|
||||
if canonical_user is None:
|
||||
legacy_account_id = f"people/{sub}"
|
||||
legacy_user = await user_manager.user_db.get_by_oauth_account(
|
||||
"google", legacy_account_id
|
||||
)
|
||||
if legacy_user is not None:
|
||||
# Fallback for pre-sub Google OAuth rows created by the old web flow.
|
||||
# TODO: Remove after oauth_account is fully backfilled to bare Google
|
||||
# sub and production has zero google rows with account_id LIKE 'people/%'.
|
||||
for oauth_account in legacy_user.oauth_accounts:
|
||||
if (
|
||||
oauth_account.oauth_name == "google"
|
||||
and oauth_account.account_id == legacy_account_id
|
||||
):
|
||||
await user_manager.user_db.update_oauth_account(
|
||||
legacy_user,
|
||||
oauth_account,
|
||||
{"account_id": sub},
|
||||
)
|
||||
break
|
||||
|
||||
try:
|
||||
return await user_manager.oauth_callback(
|
||||
"google",
|
||||
google_access_token,
|
||||
sub,
|
||||
claims["email"],
|
||||
expires_at=expires_at,
|
||||
refresh_token=google_refresh_token,
|
||||
request=request,
|
||||
associate_by_email=email_verified,
|
||||
is_verified_by_default=email_verified,
|
||||
)
|
||||
except fastapi_users_exceptions.UserAlreadyExists as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="OAUTH_USER_ALREADY_EXISTS",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=None)
|
||||
@limiter.limit("30/minute")
|
||||
async def refresh_access_token(
|
||||
|
|
@ -276,24 +339,17 @@ async def create_desktop_session(
|
|||
detail="Invalid Google identity token",
|
||||
) from exc
|
||||
|
||||
if not claims.get("sub") or not claims.get("email"):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Google identity token")
|
||||
|
||||
email_verified = bool(claims.get("email_verified"))
|
||||
user = await user_manager.oauth_callback(
|
||||
"google",
|
||||
access_token,
|
||||
claims["sub"],
|
||||
claims["email"],
|
||||
user = await resolve_google_user(
|
||||
user_manager=user_manager,
|
||||
request=request,
|
||||
google_access_token=access_token,
|
||||
claims=claims,
|
||||
expires_at=(
|
||||
int(datetime.now(UTC).timestamp()) + int(token_data["expires_in"])
|
||||
if token_data.get("expires_in")
|
||||
else None
|
||||
),
|
||||
refresh_token=token_data.get("refresh_token"),
|
||||
request=request,
|
||||
associate_by_email=email_verified,
|
||||
is_verified_by_default=email_verified,
|
||||
google_refresh_token=token_data.get("refresh_token"),
|
||||
)
|
||||
app_access_token = await get_jwt_strategy().write_token(user)
|
||||
app_refresh_token = await create_refresh_token(user.id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue