Skip to content

fix(auth): recover and clear stale session cookies on page loads - #7832

Merged
bokelley merged 2 commits into
mainfrom
w-550/session-cookie-recovery
Oct 1, 2026
Merged

bokelley merged 2 commits into
mainfrom
w-550/session-cookie-recovery

Conversation

@bokelley

@bokelley bokelley commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Server-side follow-up to #7825 (the empty Agents registry for signed-in users).

Since #7462, optionalAuth returns 401 for a present-but-unusable wos-session cookie. The server-rendered page path (getUserFromRequest) had two gaps that could leave the browser holding such a cookie indefinitely:

  1. It refreshed sessions without sharing the rotation. WorkOS refresh tokens are single-use. The API middlewares store each rotation in session_refreshes so other copies of the cookie can adopt it; the page path didn't. Page navigations are often the first request after the access token expires, so the adcontextprotocol.org copy of the session (made by /auth/bridge) and in-flight API requests were left with a dead refresh token.
  2. It never cleared a dead cookie. The page rendered logged out, but the cookie stayed. Every optional-auth API call from the page answered 401, and the AdCP bridge never re-ran because a cookie was present.

Changes

  • resolvePageSession (server/src/middleware/auth.ts) replaces the page path's own WorkOS handling:
    • reuses sessionCache and awaits storeRefreshedSession after a refresh
    • adopts a session another request already rotated
    • clears the cookie only on a terminal WorkOS refresh result (retryable: false: invalid_grant, invalid_session_cookie)
    • provider outages, unknown errors, and failed session_refreshes lookups keep the cookie
    • WorkOS reports a lost refresh race with the same invalid_grant as a dead session, so before clearing, the page path retries the shared lookup at 200ms and again at 800ms
  • Re-bridge once on adcontextprotocol.org (server/src/http.ts): after clearing, the page redirects through /auth/bridge to pick up the current AgenticAdvertising.org session. The existing _bridge_checked=1 return marker prevents a loop when that session is dead too.
  • optionalAuth now uses the same shared-refresh retry as requireAuth (one retry at 200ms) through a shared findSharedRefreshedSession helper. It doesn't fall back to anonymous access, so fix(auth): hydrate primary authorization snapshots per request #7462's policy is unchanged.
  • getRefreshedSession takes { throwOnError } so the page path can tell "lookup failed" from "nothing stored". Existing callers keep their behavior.
  • Page cookies now use the same flags as setSessionCookie, which respects ALLOW_INSECURE_COOKIES.

Review notes

A security review found no auth grant, open redirect, or redirect loop. It raised one medium issue: a lost refresh race or a DB outage could clear a valid cookie. The second commit fixes it. Remaining low-severity notes, left as-is:

  • The page path doesn't consult deadSessionCache. A cookie requireAuth just marked dead still gets one WorkOS check on a page load (and then gets cleared or kept according to the rules above).
  • Small residual race: clearing a cookie after missing another machine's rotation for about 1s. When this happens, the user was already being logged out by requireAuth in that same window.

Test plan

  • server/tests/unit/page-session.test.ts (new): no cookie, valid session, shared refresh, adopting a rotation after one retry, clearing on invalid_grant/invalid_session_cookie, keeping the cookie on retryable/unknown/lookup failures
  • server/tests/unit/cross-domain-session-bridge.test.ts: re-bridge after clear, no loop with the return marker, no bridge for AAO or non-cleared sessions
  • All unit tests that import middleware/auth or http.ts (78 files, 1581 tests)
  • Full pre-commit unit suites

🤖 Generated with Claude Code


Open workspace in Conductor

bokelley and others added 2 commits September 30, 2026 21:53
Server-rendered pages refreshed WorkOS sessions without sharing the
rotation through session_refreshes, so other copies of the cookie (the
AdCP bridge copy, in-flight API requests) could no longer refresh. The
page then rendered logged out while keeping the dead cookie, and every
optional-auth API call from it answered 401.

- Resolve page sessions in auth middleware: reuse the session cache,
  store refreshes, and adopt a session another request rotated.
- Clear the cookie only when WorkOS terminally rejects the refresh;
  provider outages and unknown failures keep it.
- On adcontextprotocol.org, bridge once more after clearing so the page
  picks up the current AgenticAdvertising.org session.
- Give optionalAuth the same shared-refresh retry requireAuth has.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… outage

WorkOS answers a refresh token another request already used with the
same terminal invalid_grant as a dead session, and getRefreshedSession
reported database failures as "nothing stored". Either could clear a
valid cookie on a page load. Wait longer for a concurrent rotation to
be stored, and keep the cookie when the lookup itself fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — server-side auth fix (shared session refreshes on page-render path, terminal-rejection cookie clearing with re-bridge on adcontextprotocol.org).

Gate checks:

  • No no-auto-approve team match.
  • gated_paths: false — row 2 does not apply despite review_decision being REVIEW_REQUIRED.
  • high_risk: false; no deletions in sensitive paths.
  • App-only change (server/), correctly carries no changeset; no protocol surface (static/schemas/source/, docs/reference/, dist/) touched.

Reviewer confirmed cookie clearing is fail-closed (terminal-only, outages/errors preserve the cookie, lost-race retries), re-bridge loop protection holds via the _bridge_checked marker + bridge-checked cookie, and clear/set cookie flags are symmetric. New test coverage validates the fixed path.

No Critical/High/Medium findings. None of rows 1–8 fire; falls through to row 9 → approve.

@bokelley
bokelley merged commit 247a523 into main Oct 1, 2026
34 checks passed
@bokelley
bokelley deleted the w-550/session-cookie-recovery branch October 1, 2026 10:41
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