Skip to content

chore: Add no-store Cache-Control to auth response paths#147

Open
ulascanzorer wants to merge 3 commits into
mainfrom
chore/pyz-198-add-no-store-cache-control
Open

chore: Add no-store Cache-Control to auth response paths#147
ulascanzorer wants to merge 3 commits into
mainfrom
chore/pyz-198-add-no-store-cache-control

Conversation

@ulascanzorer

@ulascanzorer ulascanzorer commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Task Reference: [PYZ-198]

Auth pages (/sign-in, /sign-up, /consent) and Better Auth handler routes under /api/auth/* relied on Next.js defaults for Cache-Control, which can let browsers or intermediate caches store session-bearing HTML and Set-Cookie responses. This PR pins Cache-Control: no-store at every Piyaz-owned auth surface so a shared cache (CDN edge, corporate proxy, browser bfcache) cannot store and replay a session-bearing response to a different user, closing the gap between the existing per-route precedent (app/api/oauth/consent-meta/route.ts:81) and the auth pages / BA handler routes.

What changed:

  • lib/security/headers.ts: three exact-source rules in headerRules() returning Cache-Control: no-store for /sign-in, /sign-up, /consent, independent of isProd (dev sign-in HTML is the same fixation risk).
  • app/api/auth/[...all]/route.ts: the catch-all handler backfills cache-control: no-store on every allowlisted response when absent, guarded by headers.has('cache-control') so BA-owned directives pass through unchanged — notably the well-known discovery docs, which BA tags public, max-age=15 before the wrapper runs.
  • app/api/auth/oauth2/token/route.ts: a module-local withNoStore helper backfills no-store on both return paths (the non-form passthrough and the rewritten-form path, after logTokenGrant). BA already sets no-store here (@better-auth/oauth-provider/dist/index.mjs:600) so this is a no-op-on-write; the guard makes the contract project-owned against a future BA upgrade.
  • tests/auth/cache-control.test.ts (new): pins both the static headerRules shape and the live response header on BA-core (/sign-in/email, /sign-up/email, /sign-out, /get-session), sensitive OAuth (/oauth2/userinfo, /oauth2/introspect, /oauth2/revoke), the token endpoint (via the dedicated route POST), plus a negative assertion that the well-known doc keeps its public cache hint. Modeled on tests/auth/cookie-attributes.test.ts, isolated on loopback IPs 127.0.0.40+.
  • tests/security/headers.test.ts: updated two rule-count assertions to admit the new auth-page rules (intent — HSTS absent in dev, host-scoped in prod — unchanged; assertions now match on rule content rather than absolute array length).

The well-known discovery routes (/.well-known/oauth-authorization-server, /.well-known/openid-configuration) are explicitly NOT downgraded to no-store per RFC 8414 §3; the guard preserves BA's public, max-age=15 hint.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation

Testing

  • Tested locally with bun run dev
  • Linting passes (bun run lint)
  • Typecheck passes (bun run typecheck)

bun test — 912 pass, 0 fail. tsc --noEmit — clean. eslint . — clean. New file runnable via bun test tests/auth/cache-control.test.ts (10 pass).

Notes for reviewer

The plan's token-route snippet assumed the route returned auth.handler(...) directly, but the live code already wraps it with logTokenGrant(...) (#108 grant logging). I layered withNoStore on top of logTokenGrant rather than replacing the body, preserving the grant-outcome logging untouched. Surgical: only header injection added, the MCP resource-defaulting body logic is unchanged.

Cache-Control: no-store disqualifies auth-page responses from browser bfcache (Chrome since 2022, Firefox since 2023) — hitting Back after sign-in will not show a stale signed-out page. This is the intended behavior, consistent with how major SaaS treats auth pages, not a regression.

Docs impact

  • none

Pin Cache-Control: no-store on the Mymir-owned auth surfaces so a shared
cache (CDN, corporate proxy, browser bfcache) cannot store and replay a
session-bearing response to a different user.

- lib/security/headers.ts: three exact-source rules for /sign-in,
  /sign-up, /consent (rendered auth pages), independent of isProd.
- app/api/auth/[...all]/route.ts: backfill no-store on every allowlisted
  response when absent, guarded by headers.has so BA's well-known
  public, max-age=15 hint passes through unchanged.
- app/api/auth/oauth2/token/route.ts: module-local withNoStore helper on
  both return paths, after logTokenGrant on the form path.
- tests/auth/cache-control.test.ts: pins the static headerRules shape and
  the live response header on the BA-core, OAuth, token, and well-known
  surfaces, isolated on loopback IPs 127.0.0.40+.
- tests/security/headers.test.ts: updated rule-count assertions to admit
  the new auth-page rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ulascanzorer and others added 2 commits June 20, 2026 16:13
biome format:check rejected the one-per-line rules.push entries in
lib/security/headers.ts; expand them to multi-line object form so the
CI Quality Check job's format:check step passes and the full
typecheck/lint/test suite runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…est docstring [PYZ-198]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ulascanzorer ulascanzorer self-assigned this Jun 20, 2026
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