Yahoo Login & Identity Resolution¶
How /auth/yahoo/callback turns a Yahoo OAuth login into a stable users row, why it
broke (2026-06-27), and the guards that must not be removed.
How identity is resolved (in order)¶
The callback (web_dashboard.py: auth_yahoo_callback) resolves the user's guid โ
the key the users table is indexed on โ from these sources, first valid wins:
token_data['xoauth_yahoo_guid']โ the OAuth token's guid. Not reliably present.- Fantasy API (
fantasysports.yahooapis.com/.../users;use_login=1) โusers.0.user[0].guid. - OIDC userinfo (
https://api.login.yahoo.com/openid/v1/userinfo) โ thesubclaim. This is the authoritative, non-masked source (it returns the authenticated user's own id) and also yieldsemail+name.
Every candidate is passed through _valid_yahoo_guid() before it's accepted. If no
valid guid resolves, the login is refused (it never invents/stores a placeholder).
The DB upsert then: SELECT โฆ WHERE yahoo_guid = <guid>; if found โ update; else link
by email (an existing row with the same email is updated to this guid, carrying
is_admin/approval forward) before falling back to inserting a new row.
The 2026-06-27 outage (what broke, and why this design)¶
The owner's Yahoo login bounced to the logged-out view. Trace showed three simultaneous failures:
| Source | What it returned | Why |
|---|---|---|
Token xoauth_yahoo_guid |
absent | Yahoo dropped it from the token response |
| Fantasy API guid | --hidden-- |
the user's Yahoo privacy settings mask the guid |
Social Directory API social.yahooapis.com/v1/me |
404 | Yahoo retired that endpoint |
With all three dead/masked, the old code (which only checked if not yahoo_guid) had
been storing the literal string --hidden-- as a guid. Because users.yahoo_guid
is UNIQUE-indexed, that collapsed every privacy-masked user onto one shared row โ
a cross-account identity collision โ and minted non-admin twin rows that stranded admin
grants. The fix added validation + switched to OIDC userinfo, whose sub is the real,
non-masked guid (verified: sub == 2LTAGYAJ5LMMK35PIIXU3VBZUE, the owner's real id).
โ Regression guards โ do not remove¶
- The authorize request MUST include
scope=openid email profile(auth_yahoo). Withoutopenid, the token carries noid_token, the userinfo endpoint is unauthorized, and identity resolution falls back to the masked/dead sources โ logins refuse. Symptom of regression:token_keyshas noid_token; users bounce to the logged-out view. Changing the scope requires the Yahoo app to have OpenID Connect permissions enabled in the Yahoo developer console, and users will re-consent once. - Use the OIDC userinfo endpoint, never
social.yahooapis.comโ the latter is gone (404)._YAHOO_USERINFO_URL = https://api.login.yahoo.com/openid/v1/userinfo. - Keep
_valid_yahoo_guid()(rejects empty,--hidden--, and anything not^[A-Z0-9]{20,}$). This is what prevents the cross-account collision. Never accept a raw API value as a guid without it. - Keep the email-link fallback in the upsert โ if a returning user's guid ever changes (Yahoo API generation/privacy state), they re-link to their existing row by email instead of duplicating.
fantasy-football-secrets/FLASK_SECRET_KEYmust be set (it is) so sessions are valid across both backend replicas; an unset/inconsistent key logs users out on the round-robin hop.
Diagnosing a future break¶
The OAuth round-trip itself working (you see Yahoo's consent screen) but landing logged-out almost always means guid resolution failed. To trace, temporarily add at the three resolution points:
logger.warning(f"AUTHDIAG token_keys={sorted(token_data.keys())} ...")
logger.warning(f"AUTHDIAG userinfo keys={sorted(me.keys())} sub={me.get('sub')!r} email={...!r}")
logger.warning(f"AUTHDIAG final yahoo_guid={yahoo_guid!r} valid={_valid_yahoo_guid(yahoo_guid)}")
kubectl logs <backend pod> -n fantasyfootball | grep AUTHDIAG after one login.
Remove the logging once resolved (it prints sub/email).
Fallback access while debugging: the local admin account always works at /login
(username/password from DASHBOARD_USERNAME/DASHBOARD_PASSWORD), independent of Yahoo.