accounts: sign in with Google, on a shared provider-agnostic OAuth engine #122
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Jinemi/thermograph#122
Loading…
Reference in a new issue
No description provided.
Delete branch "worktree-google-oauth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds Google as a second identity provider — sign in with it, and connect it to an existing account.
Why this is a refactor and not an addition
The obvious move is a second copy of the Discord flow. That would duplicate the signed state, the code exchange, and — critically — the account-resolution logic, which is the security-sensitive part. A parallel hand-rolled copy is exactly where a subtle divergence becomes a takeover bug.
So the flow moves to
accounts/oauth.pyand both providers become configurations of it. A third provider is now aPROVIDERSentry and nothing else.notifications/discord_link.pydrops from 342 lines to a thin alias layer plus the DM toggle.The tests are parametrised over every provider, so Discord and Google are held to the same rules rather than one being tested and the other assumed.
Schema
Identity moves to an
oauth_accounttable keyed(provider, subject). Resolution is on the provider's own stable id, never email — an address can be changed or reassigned upstream, a subject can't.user.discord_iddeliberately stays. It is the DM delivery addressnotify.py:166reads, not merely an identity, so the Discord link keeps writing it. That asymmetry is documented on both the model and the link helper.discord_only→oauth_only, and the lockout guard is now about the last credential rather than about Discord: with two providers linked to a password-less account, either may be unlinked; the survivor can't be.Migration 0005
Renames the flag and backfills an
oauth_accountrow for every existingdiscord_id. Without that backfill an already-linked user would stop being recognised at login and would silently get a second account on their next sign-in.It also drops a vestigial
discord_only: revision0003re-adds that column on a database whose0001already builtoauth_onlyfrom current model metadata, which would otherwise leave fresh and upgraded databases with different schemas. Handled in0005rather than by editing0003, which is already applied on dev and beta.Exercised on SQLite across all three paths:
oauth_onlypresent, no vestigial column0004, livediscord_onlyvalues, one Discord-linked user) → values preserved, exactly one row backfilled, none for the unlinked userNOT EXISTSguard)Compatibility
FE and BE deploy independently, so a newer backend must keep serving an older frontend:
/discord/link/callbackkeeps answering — that URL is registered in Discord's developer portal, so moving it would break every Discord login until an operator edited the portal./discord/*routes stay because the deployed frontend calls them.?discord=alongside the new?oauth=&provider=.UserReadstill carriesdiscord_onlyas a computed alias ofoauth_only.tests/notifications/test_discord_link.pywas rewritten to pin exactly these promises rather than to re-test the flow.Security properties (all tested, per provider)
Deployment
No changes to existing secrets. Google needs new ones:
THERMOGRAPH_GOOGLE_CLIENT_ID/THERMOGRAPH_GOOGLE_CLIENT_SECREThttps://<host>/api/v2/oauth/google/callbackregistered on the OAuth client. Google matches redirect URIs exactly, so each environment needs its own entry.openid+email, both non-sensitive — no Google verification review.Unset, Google reports
enabled: false, shows no UI, and its routes bounce tounavailable— same fail-closed behaviour Discord has. Both providers therefore ship dark on dev and beta, since only prod's vault holds the Discord pair and no vault yet holds Google's.Verification
tests/accounts/test_oauth.py(parametrised over both providers) replacestest_discord_login.py.gofmt -lclean,go vet+go test ./...green;account.jsparses as an ES module.response_type=code,scope=openid email,access_type=online, matchingredirect_uri.Not covered:
account.jsstill has no test runner in this repo, so the new provider UI is unexercised by CI.c17a4c3dd7to81135c1be5