Log in with Discord #23

Closed
opened 2026-08-08 06:06:27 +07:00 by sulthan · 1 comment
Owner

Parent

#18

What to build

The owner signs in with Discord instead of a shared password, and the session becomes something that can actually be revoked. Guild membership is the gate. See ADR-0002.

Registration stays locked to the owner in this ticket — opening it is a later one.

Acceptance criteria

  • The owner completes a Discord authorization code grant with the identify and guilds.members.read scopes and lands logged in.
  • The Discord API base URL is configuration. Tests drive the entire flow through the real router against a local stub, so the real request construction is exercised — including the form-encoded token exchange, which Discord rejects if sent as JSON. Deliberately not an injected client interface.
  • state is generated, stored and validated. A missing or mismatched state is refused.
  • Membership of one configured guild is required. Use the endpoint answering membership in that single guild, not the one returning the Reader's entire server list.
  • A non-member is refused with a clear explanation that does not leak whether the guild exists or what its ID is, and no Reader is created as a side effect.
  • A required-role setting exists and is empty by default. When set, a member lacking the role is refused exactly as a non-member is.
  • Only the configured owner Discord ID may log in during this ticket.
  • Sessions are rows with opaque random ids. The cookie carries only the id and is looked up per request.
  • Cookies keep HttpOnly, SameSite, and Secure when behind HTTPS. Expiry is enforced.
  • Deleting a session causes the next request to be rejected.
  • The HMAC session signing, its derived key, and the web password are all removed. No replacement signing secret is introduced.
  • Login rate limiting is preserved or replaced with an equivalent.
  • No Discord-supplied string is ever rendered as markup. No secret reaches a log line or an error response.

Blocked by

#22

## Parent #18 ## What to build The owner signs in with Discord instead of a shared password, and the session becomes something that can actually be revoked. Guild membership is the gate. See ADR-0002. Registration stays locked to the owner in this ticket — opening it is a later one. ## Acceptance criteria - [x] The owner completes a Discord authorization code grant with the `identify` and `guilds.members.read` scopes and lands logged in. - [x] The Discord API base URL is configuration. Tests drive the entire flow through the real router against a local stub, so the real request construction is exercised — including the form-encoded token exchange, which Discord rejects if sent as JSON. Deliberately not an injected client interface. - [x] `state` is generated, stored and validated. A missing or mismatched state is refused. - [x] Membership of one configured guild is required. Use the endpoint answering membership in that single guild, not the one returning the Reader's entire server list. - [x] A non-member is refused with a clear explanation that does not leak whether the guild exists or what its ID is, and no Reader is created as a side effect. - [x] A required-role setting exists and is empty by default. When set, a member lacking the role is refused exactly as a non-member is. - [x] Only the configured owner Discord ID may log in during this ticket. - [x] Sessions are rows with opaque random ids. The cookie carries only the id and is looked up per request. - [x] Cookies keep HttpOnly, SameSite, and Secure when behind HTTPS. Expiry is enforced. - [x] Deleting a session causes the next request to be rejected. - [x] The HMAC session signing, its derived key, and the web password are all removed. No replacement signing secret is introduced. - [x] Login rate limiting is preserved or replaced with an equivalent. - [x] No Discord-supplied string is ever rendered as markup. No secret reaches a log line or an error response. ## Blocked by #22
sulthan added the ready-for-agent label 2026-08-08 06:06:27 +07:00
Author
Owner

Implemented in PR #31 (branch feat/discord-login).

  • Discord authorization code grant (identify + guilds.members.read), form-encoded token exchange
  • Guild membership gate via the single-guild endpoint; DISCORD_REQUIRED_ROLE optional, empty by default
  • Only OWNER_DISCORD_ID may sign in while registration is closed
  • Sessions are DB rows (migration 0005): opaque random id in the cookie, per-request lookup, expiry enforced, delete = revoke
  • HMAC session signing, derived key, and WEB_PASSWORD removed; no replacement signing secret (ADR-0002)
  • Login rate limiting preserved on the callback
  • DISCORD_API_BASE configurable; full flow tested through the real router against a local stub
  • Env: DISCORD_CLIENT_ID/_CLIENT_SECRET/_GUILD_ID/_REQUIRED_ROLE/_API_BASE/_REDIRECT_URI; compose + DEPLOY docs updated

go test ./... passes. Closing; merge tracked via PR #31.

Implemented in PR #31 (branch feat/discord-login). - Discord authorization code grant (identify + guilds.members.read), form-encoded token exchange - Guild membership gate via the single-guild endpoint; DISCORD_REQUIRED_ROLE optional, empty by default - Only OWNER_DISCORD_ID may sign in while registration is closed - Sessions are DB rows (migration 0005): opaque random id in the cookie, per-request lookup, expiry enforced, delete = revoke - HMAC session signing, derived key, and WEB_PASSWORD removed; no replacement signing secret (ADR-0002) - Login rate limiting preserved on the callback - DISCORD_API_BASE configurable; full flow tested through the real router against a local stub - Env: DISCORD_CLIENT_ID/_CLIENT_SECRET/_GUILD_ID/_REQUIRED_ROLE/_API_BASE/_REDIRECT_URI; compose + DEPLOY docs updated go test ./... passes. Closing; merge tracked via PR #31.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#23