feat(backend): Discord OAuth login with DB-backed sessions (#23)
The browser UI signs in with a Discord authorization code grant (identify + guilds.members.read) instead of a shared password. Guild membership is the gate; the owner's Discord ID is the only identity allowed in while registration is closed. Sessions become rows in a sessions table with opaque random ids — the cookie carries only the id, looked up and expiry-checked per request — so deleting a row revokes a session. HMAC cookie signing, its derived key, and WEB_PASSWORD are gone, and no replacement signing secret is introduced (ADR-0002). Discord's API base is configurable (DISCORD_API_BASE); the full flow is tested through the real router against a local stub, including the form-encoded token exchange Discord rejects if sent as JSON.
This commit is contained in:
@@ -83,10 +83,10 @@ Go backend:
|
||||
- `html/template` only for anything a browser parses, never `text/template`. Never wrap stored or fetched strings in `template.HTML`/`JS`/`URL`; that switches off the escaping every template depends on.
|
||||
- Any outbound fetch of a client-supplied URL passes `fetchableSeriesURL` (site + `https` + host check) first. `series_url` arrives in a PUT body, so without the gate the poller will probe arbitrary hosts from the server's own network position. New fetch path reuses the gate rather than re-deriving one.
|
||||
- Cap every remote body with `io.LimitReader` (`maxBodyBytes`). An unbounded read is an OOM handed to whatever is on the other end.
|
||||
- Compare secrets with `hmac.Equal` / `subtle.ConstantTimeCompare`, never `==`. Covers API token, web password, session MAC.
|
||||
- Errors: generic text to the client (`http.Error(w, "internal error", 500)`), detail to `log.Printf`. Never log `API_TOKEN`, `WEB_PASSWORD`, a session cookie value, or a whole `Authorization` header.
|
||||
- Compare secrets with `hmac.Equal` / `subtle.ConstantTimeCompare`, never `==`. Covers the API token.
|
||||
- Errors: generic text to the client (`http.Error(w, "internal error", 500)`), detail to `log.Printf`. Never log `API_TOKEN`, `DISCORD_CLIENT_SECRET`, a session id, or a whole `Authorization` header.
|
||||
- Proxy headers are trusted only where they already are: `X-Forwarded-Proto` for the Secure cookie flag, **rightmost** `X-Forwarded-For` for client IP (leftmost is attacker-supplied). Don't read either anywhere else.
|
||||
- Session cookies keep `HttpOnly`, `SameSite`, `Secure`-when-HTTPS, and expiry checked before signature.
|
||||
- Session cookies keep `HttpOnly`, `SameSite`, `Secure`-when-HTTPS; expiry is enforced by the `sessions` table lookup, not a signature.
|
||||
- Stdlib crypto only. No hand-rolled hashing, no MD5/SHA-1 anywhere security-bearing.
|
||||
- Validate at the handler boundary before storing: body capped by `http.MaxBytesReader` (64 KB), empty `key` and unknown `status`/`kind` rejected with `400`. A bad value that reaches the store becomes every later reader's problem.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user