Fix: capture Google avatar on returning sign-in (+ userinfo fallback)

find_or_create_user returned early when the identity already existed, so a
returning Google sign-in never refreshed the profile picture (the name had been
set earlier, at link time — which is why name worked but avatar stayed null).
Now profile bits refresh on every sign-in. Also fall back to the OIDC userinfo
endpoint for the picture if the ID token omits it. 119 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-03 14:57:44 +00:00
parent 15728c3bcb
commit a2765af3fc
4 changed files with 42 additions and 15 deletions
+14
View File
@@ -19,6 +19,7 @@ from datetime import datetime, timezone
AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
TOKEN_URL = "https://oauth2.googleapis.com/token"
USERINFO_URL = "https://openidconnect.googleapis.com/v1/userinfo"
_VALID_ISS = {"accounts.google.com", "https://accounts.google.com"}
@@ -75,6 +76,19 @@ def exchange_code(code: str, redirect_uri: str, code_verifier: str) -> dict:
return json.loads(response.read())
def fetch_userinfo(access_token: str) -> dict:
"""Call the OIDC userinfo endpoint — a reliable source for the profile picture
if the ID token happens to omit it. Best-effort; returns {} on any error."""
request = urllib.request.Request(
USERINFO_URL, headers={"Authorization": f"Bearer {access_token}", "Accept": "application/json"}
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
return json.loads(response.read())
except Exception:
return {}
def _decode_jwt_payload(token: str) -> dict:
parts = token.split(".")
if len(parts) != 3: