1+ import logging
2+ import secrets
3+
4+ import httpx
15from sqlmodel import Session
26
3- from app .core .security import verify_password
7+ from app .core .config import settings
8+ from app .core .security import get_password_hash , verify_password
49from app .models import User
510from app .repositories import user_repository
611
712# Dummy hash to use for timing attack prevention when user is not found.
813# This is an Argon2 hash of a random password.
914DUMMY_HASH = "$argon2id$v=19$m=65536,t=3,p=4$MjQyZWE1MzBjYjJlZTI0Yw$YTU4NGM5ZTZmYjE2NzZlZjY0ZWY3ZGRkY2U2OWFjNjk"
1015
16+ logger = logging .getLogger (__name__ )
17+
18+
19+ class GoogleAuthError (RuntimeError ):
20+ def __init__ (self , detail : str , status_code : int = 400 ) -> None :
21+ super ().__init__ (detail )
22+ self .detail = detail
23+ self .status_code = status_code
24+
25+
26+ def _verify_google_id_token (* , id_token : str ) -> dict [str , str ]:
27+ client_id = settings .GOOGLE_OAUTH_CLIENT_ID
28+ if not client_id or not client_id .strip ():
29+ raise GoogleAuthError ("Google login is not configured" , status_code = 503 )
30+
31+ try :
32+ response = httpx .get (
33+ "https://oauth2.googleapis.com/tokeninfo" ,
34+ params = {"id_token" : id_token },
35+ timeout = settings .GOOGLE_AUTH_TIMEOUT_SECONDS ,
36+ )
37+ except httpx .HTTPError as exc :
38+ logger .warning ("Google token verification HTTP error: %s" , exc )
39+ raise GoogleAuthError (
40+ "Google token verification failed" , status_code = 502
41+ ) from exc
42+
43+ if response .status_code >= 400 :
44+ logger .warning (
45+ "Google token verification rejected (status=%s): %s" ,
46+ response .status_code ,
47+ response .text [:500 ],
48+ )
49+ raise GoogleAuthError ("Invalid Google login token" , status_code = 401 )
50+
51+ payload = response .json ()
52+ if not isinstance (payload , dict ):
53+ raise GoogleAuthError ("Invalid Google token response" , status_code = 502 )
54+
55+ audience = str (payload .get ("aud" , "" )).strip ()
56+ if audience != client_id :
57+ logger .warning (
58+ "Google token audience mismatch (expected=%s got=%s)" ,
59+ client_id ,
60+ audience ,
61+ )
62+ raise GoogleAuthError ("Invalid Google token audience" , status_code = 401 )
63+
64+ return {str (k ): str (v ) for k , v in payload .items ()}
65+
1166
1267def authenticate (* , session : Session , email : str , password : str ) -> User | None :
1368 user = user_repository .get_by_email (session = session , email = email )
@@ -23,3 +78,37 @@ def authenticate(*, session: Session, email: str, password: str) -> User | None:
2378 user .hashed_password = updated_password_hash
2479 user_repository .save (session = session , user = user )
2580 return user
81+
82+
83+ def authenticate_google_id_token (* , session : Session , id_token : str ) -> User :
84+ payload = _verify_google_id_token (id_token = id_token )
85+
86+ email = payload .get ("email" , "" ).strip ().lower ()
87+ email_verified = payload .get ("email_verified" , "" ).strip ().lower () == "true"
88+ full_name = payload .get ("name" , "" ).strip () or None
89+
90+ if not email :
91+ raise GoogleAuthError ("Google account email is missing" , status_code = 401 )
92+ if not email_verified :
93+ raise GoogleAuthError ("Google account email is not verified" , status_code = 401 )
94+
95+ user = user_repository .get_by_email (session = session , email = email )
96+ if user is None :
97+ random_password = secrets .token_urlsafe (24 )
98+ logger .info ("Creating local user from Google login (email=%s)" , email )
99+ user = user_repository .create (
100+ session = session ,
101+ user = User (
102+ email = email ,
103+ full_name = full_name ,
104+ hashed_password = get_password_hash (random_password ),
105+ ),
106+ )
107+ elif not user .is_active :
108+ raise GoogleAuthError ("Inactive user" , status_code = 400 )
109+ elif not user .full_name and full_name :
110+ user .full_name = full_name
111+ user = user_repository .save (session = session , user = user )
112+
113+ logger .info ("Google login successful (email=%s user_id=%s)" , user .email , user .id )
114+ return user
0 commit comments