Repository navigation
fix(auth): remove cookie auth; bound CORS preflight and domain-check caching - #56
Conversation
Without Access-Control-Max-Age, browsers cache a preflight for 5 seconds, so the web app pays an OPTIONS round trip before most API calls. 86400 lets each browser keep it up to its own cap (Chrome 2h).
✅ Claude PR Review —
|
| Finding | Status |
|---|---|
src/server.ts:49 (info) ⚠︎ moved |
✅ verified fixed in 7eedcc0 |
Converged: nothing new this round, and every earlier finding is settled.
Model claude-opus-5-5 · run log · 0 new · 0 carried over · 1 verified closed · 0 resolved · advisory (a human should still review). Findings are de-duplicated across pushes; an earlier finding closes only when the verification pass judges it against the current code — fixed, no longer applicable, accepted by a maintainer, or a duplicate of a finding reported on this push.
86400 only mattered to Firefox (Chrome caps at 7200) and let a domain removed from apple_clients keep working in a browser for up to a day.
The registered-domain lookup was cached in Redis with no TTL, so removing a domain from apple_clients did nothing until its key was deleted by hand. New key name, so the old never-expiring entries are no longer read.
Every client sends the token in the Authorization header: iOS, the watch app, Android and the new web app. Only the dead web app used the cookie login set for registered browser origins, and with CORS credentials on, apple_clients was all that kept another site from making cookie-authenticated calls. Login no longer sets the cookie, logout no longer clears it, auth reads only the header, and CORS stops allowing credentials.
Tightens how the API handles browser clients now that the web app (web.bookplayer.app) is back. Three changes, one commit each:
1. Let browsers reuse CORS preflights:
maxAge: 7200Every web-app call carries
Authorizationandaccept-version, so browsers send a preflight first. The API sent noAccess-Control-Max-Age, so a preflight was only reused for 5 seconds and most calls cost two round trips. 7200 (2h) is Chrome's cap. A first version used 86400; the review pointed out that would let a domain removed fromapple_clientskeep working for up to a day in Firefox, so it's 2h everywhere. Browsers cache preflights per exact URL, so this helps the fixed-URL calls (POST /library,/library/move,PATCH /user/preferences, …) the most.2. Expire the cached domain check after an hour
checkVersioncached a registered domain in Redis (domain_app_version_<origin>) with no TTL, so removing or deactivating a row inapple_clientsdid nothing for CORS until someone deleted the key by hand. It now uses a new key name (domain_app_version_v2_<origin>) with a 1h TTL. The old entries are no longer read and can be deleted at leisure.3. Remove the cookie session
Login set an httpOnly
SameSite=Nonesession cookie for every registered browser origin except the watch, andauth.tsaccepted it as a token. Together withcors({ credentials: true })andorigin: true, theapple_clientslist was the only thing keeping another site from making cookie-authenticated calls. That list had stale entries, including a 2023 dev domain that is no longer registered and that anyone could buy (being removed separately).No client uses the cookie:
origin: watch.bookplayer.appand never got a cookie.localStorageand doesn't send credentials.Changes:
auth.tsreads only theAuthorizationheader.GET /user/logoutno longer clears it. The route stays and still answers{ logout: true }.credentials: true.cookie/@types/cookiedependencies and the hand-writtensrc/types/cookie.d.ts.CLAUDE.mdno longer documents the cookie path.After deploy
apple_clientsblocks Apple login from that origin immediately (login reads the table directly), and blocks browsers within 1h (Redis) plus up to 2h (cached preflights).SESSION_COOKIE_NAMEis no longer read; it can be dropped from the task's secrets.Verified
tsc --noEmitpasses, andyarn install --frozen-lockfilepasses with the updated lockfile.cors()config was checked with a local Express app: the preflight returned204with the expectedaccess-control-max-age.🤖 Generated with Claude Code