accounts: sign in with Discord, sharing the existing link callback #115
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#115
Loading…
Reference in a new issue
No description provided.
Delete branch "worktree-discord-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?
Discord was already linkable from a signed-in session, but it could not authenticate one. This adds sign in with Discord and connects the resulting identity to an account.
What's new
GET /api/v2/discord/login/start— unauthenticated by design; this is how you get a session. Someone already signed in who lands there is sent down the link flow instead, so it never silently switches accounts.Both flows return to the existing
/discord/link/callback. Discord only honours redirect URIs registered in the developer portal, so a second callback path would have blocked this behind an operator change in the portal. Instead the signedstatenow carries a purpose; because that is inside the HMAC, a state can only verify under the flow it was minted for. Scopes are requested per-call, not registered, so the addedemailscope needs no portal change either.Account resolution, in order:
discord_id— the durable key, needs no email at all.Security properties worth reviewing
link/start, whose state is bound to a specific user id, may link. Covered bytest_a_login_state_cannot_link_the_signed_in_account.verifiedflag, tested from the attacker's side intest_login_refuses_an_unverified_discord_email.mismatch).user.discord_onlyrecords this and unlink returns 409; the UI drops the button and explains.Migration
0003_user_discord_onlyadds the column conditionally. Revision0001builds the schema withBase.metadata.create_alloff live model metadata, so a database created after this column reached the ORM already has it and an unconditionaladd_columnwould fail there. Verified both ways against SQLite: a fresh DB no-ops and stamps; a DB rewound to0002gains the column with existing rows backfilled to false.Drive-by fixes to the existing link flow
Three things that were already broken before this change:
{BASE}/subscriptions, which no route serves — the frontend serves that page at/alerts. A completed link landed on a dead path.?discord=status the callback has always sent, so linking gave no feedback at all despite the code comment claiming the page "surfaces the link state". Every outcome is now a toast.discord_idUNIQUE constraint and 500'd; it now returns atakenstatus and explains itself.Frontend
"Continue with Discord" in the sign-in modal, shown only when the server reports Discord configured — the
/discord/configprobe now runs regardless of sign-in state, since the modal needs the answer precisely when nobody is signed in.Deployment note
No new secrets.
THERMOGRAPH_DISCORD_APP_IDandTHERMOGRAPH_DISCORD_CLIENT_SECRETalready gate linking and now gate both flows; the redirect URI is already registered. Only prod's vault has that pair, so this ships dark on dev and beta —enabled()is false there, the UI hides itself, and the endpoints bounce tounavailable. Adding the two keys to those vaults (plus each host's redirect URI in the portal) is what would turn it on.Verification
tests/notifications/test_discord_login.py.gofmt -lclean,go vet ./...andgo test ./...green;account.jsparses as an ES module.Not covered:
account.jshas no test runner in this repo, so the new UI is unexercised by CI — same as the auth UI it sits next to.