| 94 | |
| 95 | |
| 96 | async def signWithGoogle( |
| 97 | request: Request, |
| 98 | access_token: str, |
| 99 | email: str, |
| 100 | org_id: Optional[int] = None, |
| 101 | current_user=Depends(get_current_user), |
| 102 | db_session: AsyncSession = Depends(get_db_session), |
| 103 | ): |
| 104 | # Google |
| 105 | google_user = await get_google_user_info(access_token) |
| 106 | |
| 107 | # SECURITY: trust only the email Google returns *and* explicitly marks as |
| 108 | # verified. Previously this fell back to the body-supplied ``email`` when |
| 109 | # Google omitted it (which happens whenever the access token was minted |
| 110 | # without the ``email`` scope), letting an attacker with any valid Google |
| 111 | # token impersonate any LearnHouse user whose address they knew. The body |
| 112 | # ``email`` field is kept in the request schema for backward compatibility |
| 113 | # but is no longer used for identity resolution. |
| 114 | google_email = google_user.get("email") |
| 115 | google_email_verified = google_user.get("email_verified") |
| 116 | if not google_email or not google_email_verified: |
| 117 | raise HTTPException( |
| 118 | status_code=401, |
| 119 | detail="Google did not return a verified email for this account", |
| 120 | ) |
| 121 | # Normalise to lower-case to match the DB unique-ish invariant on email. |
| 122 | user_email = google_email.strip().lower() |
| 123 | |
| 124 | user = (await db_session.execute( |
| 125 | select(User).where(User.email == user_email) |
| 126 | )).scalars().first() |
| 127 | |
| 128 | if not user: |
| 129 | # Extract user data with safe defaults |
| 130 | given_name = google_user.get("given_name", "") |
| 131 | family_name = google_user.get("family_name", "") |
| 132 | picture = google_user.get("picture", "") |
| 133 | |
| 134 | # Generate username more robustly |
| 135 | username_parts = [] |
| 136 | if given_name: |
| 137 | username_parts.append(given_name) |
| 138 | if family_name: |
| 139 | username_parts.append(family_name) |
| 140 | |
| 141 | # If no name parts available, use part of email |
| 142 | if not username_parts and user_email and "@" in user_email: |
| 143 | email_prefix = user_email.split("@")[0] |
| 144 | if email_prefix: # Make sure it's not empty |
| 145 | username_parts.append(email_prefix) |
| 146 | |
| 147 | # If still no parts, use a default |
| 148 | if not username_parts: |
| 149 | username_parts.append("user") |
| 150 | |
| 151 | username = "".join(username_parts) + str(random.randint(10, 99)) |
| 152 | |
| 153 | user_object = UserCreate( |