fix(backend): Resolve Safari ITP handshake loop
Description
Looks like Safari ITP sometimes blocks the Strict cookies on cross-origin redirects ( __clerk_uat ) in development so if you are redirecting directly to a protected route, the middleware will keep trying to handshake in order to acquire that particular cookie leading to an infinite redirect loop.
In the scenario where we detect a handshake redirect loop (attempt 2+), we just validate the token and return signed in instead of forcing a handshake.
Checklist
- [ ]
pnpm testruns as expected. - [ ]
pnpm buildruns as expected. - [ ] (If applicable) JSDoc comments have been added or updated for any package exports
- [ ] (If applicable) Documentation has been updated
Type of change
- [ ] 🐛 Bug fix
- [ ] 🌟 New feature
- [ ] 🔨 Breaking change
- [ ] 📖 Refactoring / dependency upgrade / documentation
- [ ] other:
Summary by CodeRabbit
-
Bug Fixes
- Prevents infinite handshake redirect loops in browsers that block cross-origin cookies (e.g., Safari ITP) by verifying existing session tokens so valid sessions remain signed in.
-
Tests
- Added tests covering redirect-loop scenarios to ensure correct handling of valid and invalid session tokens.
-
Chores
- Added a patch changeset documenting the redirect-loop fix for the backend package.
✏️ Tip: You can customize this high-level summary in your review settings.
🦋 Changeset detected
Latest commit: fe817e4f7ca0c091e12d2f3513e1f925bb947a0d
The changes in this PR will be included in the next version bump.
This PR includes changesets to release 11 packages
| Name | Type |
|---|---|
| @clerk/backend | Patch |
| @clerk/agent-toolkit | Patch |
| @clerk/astro | Patch |
| @clerk/express | Patch |
| @clerk/fastify | Patch |
| @clerk/nextjs | Patch |
| @clerk/nuxt | Patch |
| @clerk/react-router | Patch |
| @clerk/remix | Patch |
| @clerk/tanstack-react-start | Patch |
| @clerk/testing | Patch |
Not sure what this means? Click here to learn what changesets are.
Click here if you're a maintainer who wants to add another changeset to this PR
The latest updates on your projects. Learn more about Vercel for GitHub.
| Project | Deployment | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| clerk-js-sandbox | Preview | Comment | Dec 4, 2025 2:33pm |
Walkthrough
Adds redirect-loop detection to authenticateRequest: when no clientUat exists but a session token and a redirect-count cookie are present, it tries to verify the session token and returns a signed-in state on success; on failure it falls back to the existing handshake flow.
Changes
| Cohort / File(s) | Summary |
|---|---|
Redirect-loop handling & tests packages/backend/src/tokens/request.ts, packages/backend/src/tokens/__tests__/request.test.ts |
Adds a conditional branch in authenticateRequest to detect redirect loops via __clerk_redirect_count; if no clientUat but a session token exists it attempts cookie-based session verification and returns a signed-in auth state on success, otherwise falls back to handshake (SessionTokenWithoutClientUAT). Adds two tests covering successful verification and handshake fallback. |
Changeset / Manifest .changeset/true-carpets-happen.md, package.json |
Adds a patch changeset describing the redirect-loop fix for development browsers that block cross-origin Strict cookies and updates package metadata as part of the release changeset. |
Sequence Diagram
sequenceDiagram
participant Client
participant AuthRequest as authenticateRequest()
participant CookieStore
participant SessionVerifier
Client->>AuthRequest: HTTP request (cookies include __clerk_redirect_count)
AuthRequest->>CookieStore: Read session token & redirect count
alt No clientUat && session token present && redirect_count > 0
AuthRequest->>SessionVerifier: Verify session token from cookie
alt Verification succeeds
SessionVerifier-->>AuthRequest: Valid session token & claims
AuthRequest-->>Client: Return signed-in auth state (session token)
else Verification fails
SessionVerifier-->>AuthRequest: Invalid or expired token
AuthRequest-->>Client: Continue to handshake (SessionTokenWithoutClientUAT / null auth)
end
else Normal flow
AuthRequest-->>Client: Proceed with existing auth/handshake logic
end
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~20 minutes
- Inspect
packages/backend/src/tokens/request.tsfor correctness of the new conditional, cookie parsing, and error/edge-case handling. - Verify test accuracy and JWKS mocking in
packages/backend/src/tokens/__tests__/request.test.ts. - Check
.changeset/true-carpets-happen.mdandpackage.jsonfor correct changeset metadata and versioning intent.
Poem
🐰
A loop that chased us round and round,
I sniffed the cookie, then found the sound.
If token sings, I'll open the gate—
If silent, handshake decides our fate.
Hops saved, code tidy, rabbit elate!
Pre-merge checks and finishing touches
❌ 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%. | You can run @coderabbitai generate docstrings to improve docstring coverage. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | The title clearly and specifically describes the main change: resolving an infinite handshake redirect loop caused by Safari ITP blocking cross-origin redirect cookies. |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
✨ Finishing touches
- [ ] 📝 Generate docstrings
🧪 Generate unit tests (beta)
- [ ] Create PR with unit tests
- [ ] Post copyable unit tests in a comment
- [ ] Commit unit tests in branch
fix/safari-itp-handshake-loop
Comment @coderabbitai help to get the list of available commands and usage tips.
@clerk/agent-toolkit
npm i https://pkg.pr.new/@clerk/agent-toolkit@7335
@clerk/astro
npm i https://pkg.pr.new/@clerk/astro@7335
@clerk/backend
npm i https://pkg.pr.new/@clerk/backend@7335
@clerk/chrome-extension
npm i https://pkg.pr.new/@clerk/chrome-extension@7335
@clerk/clerk-js
npm i https://pkg.pr.new/@clerk/clerk-js@7335
@clerk/dev-cli
npm i https://pkg.pr.new/@clerk/dev-cli@7335
@clerk/elements
npm i https://pkg.pr.new/@clerk/elements@7335
@clerk/clerk-expo
npm i https://pkg.pr.new/@clerk/clerk-expo@7335
@clerk/expo-passkeys
npm i https://pkg.pr.new/@clerk/expo-passkeys@7335
@clerk/express
npm i https://pkg.pr.new/@clerk/express@7335
@clerk/fastify
npm i https://pkg.pr.new/@clerk/fastify@7335
@clerk/localizations
npm i https://pkg.pr.new/@clerk/localizations@7335
@clerk/nextjs
npm i https://pkg.pr.new/@clerk/nextjs@7335
@clerk/nuxt
npm i https://pkg.pr.new/@clerk/nuxt@7335
@clerk/clerk-react
npm i https://pkg.pr.new/@clerk/clerk-react@7335
@clerk/react-router
npm i https://pkg.pr.new/@clerk/react-router@7335
@clerk/remix
npm i https://pkg.pr.new/@clerk/remix@7335
@clerk/shared
npm i https://pkg.pr.new/@clerk/shared@7335
@clerk/tanstack-react-start
npm i https://pkg.pr.new/@clerk/tanstack-react-start@7335
@clerk/testing
npm i https://pkg.pr.new/@clerk/testing@7335
@clerk/themes
npm i https://pkg.pr.new/@clerk/themes@7335
@clerk/types
npm i https://pkg.pr.new/@clerk/types@7335
@clerk/upgrade
npm i https://pkg.pr.new/@clerk/upgrade@7335
@clerk/vue
npm i https://pkg.pr.new/@clerk/vue@7335
commit: fe817e4