Skip to content

fix(auth): remove cookie auth; bound CORS preflight and domain-check caching - #56

Merged
GianniCarlo merged 4 commits into
mainfrom
cors-preflight-max-age
Oct 8, 2026
Merged

GianniCarlo merged 4 commits into
mainfrom
cors-preflight-max-age

Conversation

@GianniCarlo

@GianniCarlo GianniCarlo commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

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: 7200

Every web-app call carries Authorization and accept-version, so browsers send a preflight first. The API sent no Access-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 from apple_clients keep 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

checkVersion cached a registered domain in Redis (domain_app_version_<origin>) with no TTL, so removing or deactivating a row in apple_clients did 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=None session cookie for every registered browser origin except the watch, and auth.ts accepted it as a token. Together with cors({ credentials: true }) and origin: true, the apple_clients list 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:

  • iOS and the watch app send the bearer token. The watch sends origin: watch.bookplayer.app and never got a cookie.
  • Android's only cookie jar is for Audiobookshelf OIDC.
  • The new web app keeps its token in localStorage and doesn't send credentials.
  • The old web app that used cookies is dead.

Changes:

  • auth.ts reads only the Authorization header.
  • Login no longer sets the cookie, and GET /user/logout no longer clears it. The route stays and still answers { logout: true }.
  • CORS drops credentials: true.
  • Removes the cookie / @types/cookie dependencies and the hand-written src/types/cookie.d.ts.
  • CLAUDE.md no longer documents the cookie path.

After deploy

  • Removing a row from apple_clients blocks 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_NAME is no longer read; it can be dropped from the task's secrets.

Verified

  • tsc --noEmit passes, and yarn install --frozen-lockfile passes with the updated lockfile.
  • ESLint on the touched files reports no new errors; the remaining ones were already there.
  • The first commit's cors() config was checked with a local Express app: the preflight returned 204 with the expected access-control-max-age.
  • The test suite runs in CI.

🤖 Generated with Claude Code

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).
Comment thread src/server.ts Outdated
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

✅ Claude PR Review — PASS

This PR does three things. It removes the cookie session: the auth middleware now reads only the Authorization header, login no longer sets a cookie, logout no longer clears one, and CORS drops credentials: true. It sets CORS maxAge: 7200. It moves the checkVersion domain cache to a v2 key with a 1h TTL.

Auth: less surface than before. A JWT is now accepted only from the bearer header, and with credentials gone, origin: true no longer allows other sites to make cookie-authenticated requests. No code still reads cookie or SESSION_COOKIE_NAME, and no tests depend on the cookie. Login still uses client_id to check the origin and to verify the Apple token.

Caching: RedisService.setObject takes ttlSeconds and sets EX, so the domain cache now expires after an hour. The earlier maxAge concern is fixed: it is now 7200, with the revocation delay written in a comment.

Nothing here touches IAP, passkeys, or per-user authorization scoping.

Findings: no findings

Previously raised

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.
@GianniCarlo GianniCarlo changed the title perf(cors): let browsers reuse CORS preflights fix(auth): remove cookie auth; bound CORS preflight and domain-check caching Oct 8, 2026
@GianniCarlo
GianniCarlo merged commit f1c38cb into main Oct 8, 2026
4 checks passed
@GianniCarlo
GianniCarlo deleted the cors-preflight-max-age branch October 8, 2026 04:38

This branch was successfully deployed

1 active deployment
reviewer — 7eedcc0a Deployed Oct 8, 2026 by GianniCarlo via review #112
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant