Skip to content

Handle stale OAuth client reset recovery - #169

Merged
kentcdodds merged 7 commits into
mainfrom
cursor/graceful-client-reset-5962
Apr 14, 2026
Merged

kentcdodds merged 7 commits into
mainfrom
cursor/graceful-client-reset-5962

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Apr 14, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • verify the latest AI review comments against the current code and keep only the valid fixes
  • scope the signed OAuth reset-verification cookie to /oauth so browsers send it to both /oauth/authorize and /oauth/authorize-info, allowing cleanup logic to actually run
  • remove the now-unused invalidRedirectUriMessage export after confirming it no longer has any callers
  • keep the previously hardened invalid-client reset verification flow and Remix route-state reset behavior intact

Walkthrough

oauth-review-followup-demo.mp4
Successful stale-client reset with form hidden
Fresh authorize query resets route state

Testing

  • npx vitest run --project workers-unit packages/worker/src/oauth-handlers.workers.test.ts
  • npm run typecheck
  • npm run build
  • browser-like curl cookie-jar verification proving the signed reset cookie is issued with Path=/oauth and sent back to /oauth/authorize-info
  • previous live worker/browser verification for the hardened invalid-client reset flow and query-change route reset behavior
Open in WebΒ Open in CursorΒ 

Summary by CodeRabbit

  • New Features

    • Improved OAuth error handling to detect client ID mismatches and offer a verified "Reset stored connection" flow; authorization form and follow-up messaging update after reset.
  • Tests

    • Added coverage for the client-mismatch reset path and updated expectations for non-resettable rejection messaging.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Apr 14, 2026 •

Copy link
Copy Markdown
πŸ“ Walkthrough

Walkthrough

Adds a client-ID-mismatch reset flow: new message constant, cookie-based verification for reset eligibility, client UI gating for reset and post-reset behavior, server handlers to set/validate the verification cookie and perform guarded reset, and tests covering the new path.

Changes

Cohort / File(s) Summary
Shared OAuth messages
packages/shared/src/oauth-messages.ts
Replaced invalidRedirectUriMessage with invalidClientIdMismatchMessage (export change).
Client authorize UI
packages/worker/client/routes/oauth-authorize.tsx
Use request-scoped activeInfoRequestId, track allowClientReset and resetCompleted; show reset card only when allowed; stop rendering authorize form after reset; read query error into UI state.
Server OAuth handlers
packages/worker/src/oauth-handlers.ts
Add signed short-lived verification cookie for client-id-mismatch, resolveAuthorizeInfoResetState(...), cookie create/read/destroy helpers, centralized Set-Cookie header handling, JSON response header enforcement, and broadened reset authorization and error messaging.
Tests for handlers
packages/worker/src/oauth-handlers.workers.test.ts
New tests: authorize-info returns allowClientReset + cookie on client-id-mismatch; reset-client flow using verification cookie; negative test when cookie absent; updated assertions and imports.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant Client as OAuth Authorize Route (client)
  participant Server as OAuth Worker (server)
  participant Store as Grants/Client Store

  Browser->>Client: GET /oauth/authorize (may include error_description)
  Client->>Client: readQueryError(), increment activeInfoRequestId
  Client->>Server: GET /oauth/authorize-info
  Server->>Server: parse request, detect invalidClientIdMismatchMessage
  alt mismatch detected
    Server->>Browser: 400 { ok:false, error, allowClientReset:true } + Set-Cookie (verification)
    Client->>Browser: show "Reset stored connection" UI
    Browser->>Server: POST /oauth/reset-client?decision=reset-client (with auth cookie + verification cookie)
    Server->>Server: validate verification cookie vs client_id
    Server->>Store: list grants, revoke grants, delete stored client
    Server-->>Browser: 200 { ok:true, message }
  else not mismatch
    Server->>Browser: normal authorize-info response
    Client->>Browser: show authorize form or error
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A client ID hopped out of line,
I planted a cookie to mark the sign.
If IDs mismatch and things reset,
I nibble grants until they're set.
A tidy OAuth burrowβ€”soft and fine.

πŸš₯ Pre-merge checks | βœ… 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… Passed checks (2 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title accurately captures the main technical objective: hardening the OAuth client reset flow with server-verified cookie-based verification to handle stale client states.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
πŸ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/graceful-client-reset-5962

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds
kentcdodds marked this pull request as ready for review April 14, 2026 14:38
@github-actions

github-actions Bot commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

πŸ”Ž Preview deployed: https://kody-pr-169.kentcdodds.workers.dev

Worker: kody-pr-169
D1: kody-pr-169-db
KV: kody-pr-169-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/worker/client/routes/oauth-authorize.tsx (1)

203-207: ⚠️ Potential issue | 🟠 Major

Reset resetCompleted when the authorize query changes.

This flag is set after a successful reset, but Lines 203-207 keep the same route instance alive across window.location.search changes. A later authorize attempt in the same tab will keep showAuthorizeForm false at Line 227 and hide the approve/deny actions until the page is hard-refreshed.

πŸ’‘ Minimal fix
 		if (currentSearch !== lastSearch) {
 			lastSearch = currentSearch
+			resetCompleted = false
 			void loadInfo()
 		}

Also applies to: 223-227

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/client/routes/oauth-authorize.tsx` around lines 203 - 207,
The component keeps the same instance across window.location.search changes
(tracked by lastSearch) but doesn't clear the resetCompleted flag, causing
showAuthorizeForm to remain false on subsequent authorize queries; update the
block that detects search changes (the lastSearch / loadInfo area) to also set
resetCompleted = false whenever currentSearch !== lastSearch (and likewise where
similar search-change handling exists around lines 223-227), so a new authorize
flow will display the authorize form and actions again.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/oauth-handlers.ts`:
- Around line 193-196: The current logic uses the raw query error_description
(via readAuthorizeErrorDescription) to decide canResetStoredClient (through
canResetStoredClientForMessage), which is untrusted; replace this by checking
only trusted server-side signals (e.g., redirectUriMismatch OR a validated
provider-reported flag stored in server session/state or a provider-signed
assertion) and stop using queryErrorDescription for reset gating. Concretely,
remove or ignore canResetStoredClientForMessage(queryErrorDescription) when
computing canResetStoredClient and instead consult a trusted boolean (for
example providerReportedStaleClient or session.providerMismatchVerified)
populated after verifying provider responses or server-side callbacks in the
OAuth flow before allowing stored-client reset in the resetStoredClient/reset
path referenced by the reset logic.

---

Outside diff comments:
In `@packages/worker/client/routes/oauth-authorize.tsx`:
- Around line 203-207: The component keeps the same instance across
window.location.search changes (tracked by lastSearch) but doesn't clear the
resetCompleted flag, causing showAuthorizeForm to remain false on subsequent
authorize queries; update the block that detects search changes (the lastSearch
/ loadInfo area) to also set resetCompleted = false whenever currentSearch !==
lastSearch (and likewise where similar search-change handling exists around
lines 223-227), so a new authorize flow will display the authorize form and
actions again.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c3b62bcd-a856-4c4e-9c38-0168e5cf6492

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between cab119c and 8e94907.

πŸ“’ Files selected for processing (4)
  • packages/shared/src/oauth-messages.ts
  • packages/worker/client/routes/oauth-authorize.tsx
  • packages/worker/src/oauth-handlers.ts
  • packages/worker/src/oauth-handlers.workers.test.ts

Comment thread packages/worker/src/oauth-handlers.ts Outdated
Comment thread packages/worker/src/oauth-handlers.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/client/routes/oauth-authorize.tsx Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • βœ… Fixed: Server trusts client-controlled query param for authorization
    • Resetting stored clients now requires a server-confirmed redirect URI mismatch instead of trusting the query error description.

You can send follow-ups to the cloud agent here.

Comment thread packages/worker/src/oauth-handlers.ts Outdated
cursoragent and others added 2 commits April 14, 2026 15:11
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit b3cb57d. Configure here.

Comment thread packages/worker/src/oauth-handlers.ts Outdated
Comment thread packages/shared/src/oauth-messages.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/worker/src/oauth-handlers.ts (1)

116-122: Potential false positive from substring match.

Using includes() to check for the cookie could match unintended cookie names. For example, a cookie named other_kody_oauth_client_reset= would trigger a false positive.

Consider using a more precise check:

πŸ”§ Suggested fix with regex boundary
 function requestHasOAuthClientResetVerificationCookie(request: Request) {
 	const cookieHeader = request.headers.get('Cookie')
+	if (!cookieHeader) return false
+	const pattern = new RegExp(
+		`(?:^|;\\s*)${oauthClientResetVerificationCookieName}=`,
+	)
-	return (
-		cookieHeader?.includes(`${oauthClientResetVerificationCookieName}=`) ??
-		false
-	)
+	return pattern.test(cookieHeader)
 }
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/oauth-handlers.ts` around lines 116 - 122, The current
requestHasOAuthClientResetVerificationCookie uses a substring includes() check
which can false-positive match cookie names that contain the target as a
substring; update the function to parse the Cookie header properly and check
cookie name equality instead of substring matching β€” e.g., split
request.headers.get('Cookie') on ';', trim each pair, and verify any pair starts
with `${oauthClientResetVerificationCookieName}=` (or use a strict regex with
cookie name boundaries) so only an exact cookie name match (reference:
requestHasOAuthClientResetVerificationCookie and
oauthClientResetVerificationCookieName) triggers true.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/worker/src/oauth-handlers.ts`:
- Around line 116-122: The current requestHasOAuthClientResetVerificationCookie
uses a substring includes() check which can false-positive match cookie names
that contain the target as a substring; update the function to parse the Cookie
header properly and check cookie name equality instead of substring matching β€”
e.g., split request.headers.get('Cookie') on ';', trim each pair, and verify any
pair starts with `${oauthClientResetVerificationCookieName}=` (or use a strict
regex with cookie name boundaries) so only an exact cookie name match
(reference: requestHasOAuthClientResetVerificationCookie and
oauthClientResetVerificationCookieName) triggers true.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c7cb903-388f-42c8-b463-0d50fa672f00

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 9ccbbb0 and e0aa33d.

πŸ“’ Files selected for processing (4)
  • packages/shared/src/oauth-messages.ts
  • packages/worker/client/routes/oauth-authorize.tsx
  • packages/worker/src/oauth-handlers.ts
  • packages/worker/src/oauth-handlers.workers.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/shared/src/oauth-messages.ts
  • packages/worker/src/oauth-handlers.workers.test.ts

@kentcdodds
kentcdodds merged commit 577475b into main Apr 14, 2026
9 checks passed
@kentcdodds
kentcdodds deleted the cursor/graceful-client-reset-5962 branch April 14, 2026 16:12
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.

2 participants