fix(auth): give every OAuth callback failure a way back - #70
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 6 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
Comment |
eeb03e9 to
61b992a
Compare
|
Rebased onto A comment in this PR became false. It said the admin pages couldn't tell a dead session from a non-admin, so they kept bare redirects. #67 made exactly that distinction. Comment corrected to point at A second commit fixes a defect #67 introduced ( The union now has a third member, Re-verified against the new base, all output read:
|
Six failure paths across the two callbacks returned text/plain with a status code and nothing else, and both callbacks had unwrapped throws. app/error.tsx does not cover route handlers, so an ESI or Discord blip during sign-in reached the member as a bare 500 with no navigation. Every failure now redirects to a page that names what happened: expired-or-forged state, provider unreachable, and session-gone are distinguished, and the catch logs err.message (not the error object, which for a Postgres failure can carry query parameters from rows that hold refresh tokens). /account also distinguishes a dead session cookie from no cookie, so a first-time visitor is never told a session they never had has ended. The admin pages keep bare redirects: getAdminContext() returns null for a signed-in non-admin too, so they cannot tell the two apart.
covering two things: a visitor with no cookie at all, and one whose cookie no longer resolves. Both redirected to /login?error=session_expired, so anyone who typed /admin without ever signing in was told a session they never had had ended. resolveAdmin now returns session-expired only for a cookie that fails to resolve; an absent cookie gets the plain login page. This is the same line account/page.tsx draws, and the first time denyAdmin's exhaustive switch has actually caught an added union member.
61b992a to
725f990
Compare
Follow-up to #64. The critique credited
src/app/error.tsxwith covering the callback failure paths; it doesn't.error.tsxis a React error boundary for page and layout rendering and never wraps a route handler, so every throw in either OAuth callback reached the member as a bare Next 500 with no navigation back.Alongside it, six paths returned
text/plainand a status code:Three per callback. Leave a login tab open for eleven minutes and you got
invalid or expired stateon a white page.What changed
Both callbacks redirect on every failure now.
missing params400/login?error=oauth_failed/account?error=discord_failedinvalid or expired state400/login?error=oauth_expired/account?error=discord_expired/account?error=link_expired, or/login?error=session_expiredwith no session/account?error=discord_expired, or/login?error=session_expired/account?error=link_failedor/login?error=oauth_failed/account?error=discord_failed/accountalso distinguishes a dead session cookie from no cookie, so a first-time visitor is never told a session they never had has ended. The four admin pages keep their bare redirects on purpose:getAdminContext()returns null for a signed-in non-admin too (src/lib/admin-guard.ts:10-37), so they can't tell expiry from insufficient privilege and shouldn't guess.Where to look
Replay safety. The transaction is consumed before the session-mismatch branch, so redirecting there can't hand anyone a replayable state.
tests/auth-routes.test.tsnow asserts the replay lands on the same destination as any unusable state and issues no second session cookie.The catch logs
.message, noterr. Half this repo logs the error object (worker/dispatcher.ts:129), half logs.message(services/health.ts:11)..messageis right here:EveSsoErroris clean, but a Postgres error from the surrounding transaction can carry the failing query and its parameters on sibling properties, and those rows hold refresh tokens. Losing the stack is the deliberate cost.Monitoring. Turning 4xx into 302 would blind a status-code dashboard — there isn't one, and these two
console.errorlines are the first observability these paths have ever had.docs/ops.mdnow records which codes log and which are normal background.Verification
npm run typecheck— cleannpm test— 52 files, 431 tests passed (430 before; the new ESI-failure test is the addition)npm run test:e2e— 60 passed, including 4 new/loginspecsnpm run format:check— cleannpm run lint— 0 errors, 5 pre-existingno-img-elementwarningsNot done
No stylelint — CSS is still unlinted, which is how the deprecated
word-breakslipped through in #64. Worth a separate PR. The dated plan doc underdocs/superpowers/plans/still shows the old 400s and was left as the historical record it is.