feat(server): login page + session cookie auth #13

Merged
w33ble merged 6 commits from feat/login-cookie-auth into main 2026-09-22 14:02:32 -06:00
Collaborator

Summary

Replace the HTTP Basic dialog on the web UI with a login page + session cookie:

  • GET/POST /login (inline form, ?e=1 error, open-redirect-guarded ?next=) and GET/POST /logout (internal/server/login.go)
  • Stateless HMAC-SHA256 session cookie (mediacast_session): 30-day TTL, HttpOnly, SameSite=Lax, Secure on TLS, sliding refresh under 15 days remaining (internal/server/session.go)
  • Key derived from MEDIACAST_PASSWORD, so changing the password invalidates every outstanding session — no new MEDIACAST_* env var
  • Auth gate rework in ServeHTTP: /login, /logout, /style.css allowlisted; web surface redirects 303 to the login page (no WWW-Authenticate challenge); feed/audio/artwork catcher unchanged (401 + Basic when MEDIACAST_FEED_AUTH=auth) so podcast clients and user:pass@host/feed.xml subscriptions keep working
  • HTTP Basic auth stays accepted everywhere as a fallback for curl/scripts
  • Login page styled by the real stylesheet (no duplicated palette); background glow anchored to the viewport so it renders identically on short and tall pages
  • Log out link in the index header
  • README auth wording updated (no config/compose changes needed)

Why

Basic auth's browser dialog can't be styled, has no logout, and phone password managers interact with it poorly. A cookie session keeps the user logged in ~30 days with a proper password-manager-friendly form, using the same static credentials.

Tests

New login_test.go: form render, bad/good creds, cookie-only access, tampered/expired cookie rejection, sliding refresh, next= guard, logout. Existing *RequiresAuth tests updated 401 → 303-to-login for web surfaces; Basic-fallback and feed-catcher (TestFeedAuthFlag) tests unchanged and passing.

mise run check + mise run test green.

## Summary Replace the HTTP Basic dialog on the web UI with a login page + session cookie: - `GET/POST /login` (inline form, `?e=1` error, open-redirect-guarded `?next=`) and `GET/POST /logout` (`internal/server/login.go`) - Stateless HMAC-SHA256 session cookie (`mediacast_session`): 30-day TTL, HttpOnly, SameSite=Lax, Secure on TLS, sliding refresh under 15 days remaining (`internal/server/session.go`) - Key derived from `MEDIACAST_PASSWORD`, so changing the password invalidates every outstanding session — no new `MEDIACAST_*` env var - Auth gate rework in `ServeHTTP`: `/login`, `/logout`, `/style.css` allowlisted; web surface redirects 303 to the login page (no `WWW-Authenticate` challenge); feed/audio/artwork catcher unchanged (401 + Basic when `MEDIACAST_FEED_AUTH=auth`) so podcast clients and `user:pass@host/feed.xml` subscriptions keep working - HTTP Basic auth stays accepted everywhere as a fallback for curl/scripts - Login page styled by the real stylesheet (no duplicated palette); background glow anchored to the viewport so it renders identically on short and tall pages - Log out link in the index header - README auth wording updated (no config/compose changes needed) ## Why Basic auth's browser dialog can't be styled, has no logout, and phone password managers interact with it poorly. A cookie session keeps the user logged in ~30 days with a proper password-manager-friendly form, using the same static credentials. ## Tests New `login_test.go`: form render, bad/good creds, cookie-only access, tampered/expired cookie rejection, sliding refresh, `next=` guard, logout. Existing `*RequiresAuth` tests updated 401 → 303-to-login for web surfaces; Basic-fallback and feed-catcher (`TestFeedAuthFlag`) tests unchanged and passing. `mise run check` + `mise run test` green.
The login page rendered unstyled because /style.css sat behind the
auth gate and the inline <style> fallback was missing body background
and box-sizing — inputs with width:100% + padding + border overflowed
the card.

Option A: allowlist /style.css through the auth gate (it's static
theme CSS, no data) and strip the duplicated palette rules from the
login template's inline <style>. The login page now loads the real
stylesheet like every other page — no drift, no duplication.

- Rename isLoginPath → isPublicPath, add /style.css
- Login inline styles now use CSS variables from the real stylesheet
- Update TestStyleSheetRequiresAuth → TestStyleSheetPublicWithoutAuth
  (asserts 200 + palette anchor without auth)
fix(server): anchor background glow to viewport, not body height
Some checks failed
ci / check (pull_request) Failing after 3m25s
acab19bcfc
The login page showed a prominent purple glow band at the top because
the radial-gradient y-position used a percentage (-10%), which is
relative to the element's height. On the short login page body
(~800px), the glow center sat at y=-80px with 600px vertical size,
bleeding ~520px into the viewport. On the taller index page
(~1600px+), the center was at y=-160px, bleeding only ~440px — the
glow read as a subtle accent, not a band.

Fix: switch to fixed pixel offsets (-200px) and smaller glow size
(800x400), plus background-attachment: fixed so the gradient anchors
to the viewport regardless of body height. The glow now renders
identically on both pages — a faint purple tint at the top, not a
dominant band.
w33ble approved these changes 2026-09-22 13:37:35 -06:00
test(server): make cookie tamper deterministic
All checks were successful
ci / check (pull_request) Successful in 3m30s
f486b53aac
Flipping bit 0x1 of the last base64 char sometimes lands on an unused
padding bit, leaving the decoded token valid — the test failed ~40% of
runs (130/300 locally, and CI run #34). The first char's bits are all
significant, so flipping there always changes the decoded bytes.
w33ble merged commit 01f45c8c89 into main 2026-09-22 14:02:32 -06:00
w33ble deleted branch feat/login-cookie-auth 2026-09-22 14:02:32 -06:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
w33ble/mediacast!13
No description provided.