clerk / clerk/javascript

getCookiesFromHandshake swallows nonce handshake-payload fetch failures — resolves as generic signed-out

Open
#9,114 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Stale
Dominant language
TypeScript
Stars
1.8k
Forks
472
Avg merge
2d 11h
Merged PRs (30d)
189

Description

Note for triage: filed at the request of Clerk support (Ryan) from an existing support ticket, where the root cause and evidence below were confirmed. This is an SDK hardening/observability request backed by a production incident, not a help request — hence no separate minimal-repro repo; deterministic repro steps are inline below.

Package: @clerk/backend (observed on 3.8.3; code is current on main)

Summary

When a nonce-format handshake's payload fetch fails, HandshakeService.getCookiesFromHandshake() (packages/backend/src/tokens/handshake.ts) catches the error, logs it with console.error, and resolves with zero cookie directives. resolveHandshake() then finds no session cookie and returns a plain signedOut(reason: AuthErrorReason.SessionTokenMissing, message: "").

The result: a server-side infrastructure failure (Backend API unreachable, misconfigured/missing secret key, transient 5xx) is laundered into an ordinary "signed out" auth state. The handshake "succeeds" empty, auth never converges, and nothing distinguishable surfaces to the adapter, middleware, or requestState consumer — the only trace is a console line in server logs.

// getCookiesFromHandshake()
if (this.authenticateContext.handshakeNonce) {
  try {
    const handshakePayload = await this.authenticateContext.apiClient?.clients.getHandshakePayload({
      nonce: this.authenticateContext.handshakeNonce
    });
    if (handshakePayload) {
      cookiesToSet.push(...handshakePayload.directives);
    }
  } catch (error) {
    console.error('Clerk: HandshakeService: error getting handshake payload:', error);
  }
}

Note the apiClient?. optional chaining has the same property: a missing apiClient silently yields no directives with no log at all.

Real-world impact

This made a production outage take days to diagnose. svelte-clerk constructed its Backend API client with an undefined secret key on Vercel (module-scope env read racing SvelteKit's Server.init() — fixed in wobsoriano/svelte-clerk#442). Every nonce handshake threw Missing Clerk Secret Key locally, before any request reached Clerk's API:

  • Flows depending on server-side handshake convergence (Dashboard impersonation) dead-looped to the sign-in page with zero surfaced errors.
  • Clerk support searched API logs for our handshake nonces and found nothing — because the SDK threw before the network call. That "zero requests" observation was only reconcilable once we found the swallowed console.error in Vercel function logs.
  • From the app's perspective the symptom was "auth randomly won't converge," which points at cookies, tokens, and clock skew — everywhere except the actual failure.

Context confirmed with Clerk support (Ryan) in an existing ticket; happy to link it internally.

Reproduction (deterministic, no impersonation needed)

  1. Any SvelteKit/Next.js/etc. app using @clerk/backend's authenticateRequest on a production Clerk instance, deployed so that the Backend API call can fail — simplest forcing function: an invalid CLERK_SECRET_KEY at the point createClerkClient runs (that is exactly what the svelte-clerk bug produced).
  2. Send any request with a handshake nonce cookie: curl https://your-app.example/ -H 'Cookie: __clerk_handshake_nonce=anything'.
  3. Observed: request resolves as generic signed-out (reason: session-token-missing, empty message); the only evidence of the real failure is Clerk: HandshakeService: error getting handshake payload: … on the server console.
  4. Expected: the returned requestState carries a distinguishable reason (and/or the SDK attempts a safe fallback) so the adapter/app can tell "infrastructure failure during handshake resolution" apart from "user is signed out."

Suggested hardening (either or both)

  1. Surface the failure distinctly. Return/propagate a dedicated auth error reason (e.g. AuthErrorReason.HandshakePayloadFetchFailed) instead of the generic session-token-missing, so requestState consumers and framework adapters can log or act on it. Even keeping the signed-out outcome, a distinguishable reason/message would have turned a multi-day investigation into a five-minute log read.
  2. Fall back where safe. When the nonce payload fetch fails, fall back to a token-format handshake if the conditions allow, so a transient BAPI failure doesn't cost the user their session convergence.

Swallowing may be a deliberate availability choice (don't hard-fail requests on BAPI hiccups) — that's compatible with suggestion 1: keep resolving, but make the failure observable in the returned state, not only stderr.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/backend/src/tokens/handshake.ts, reading HandshakeService.getCookiesFromHandshake() and resolveHandshake(), then reproduce the failure with an invalid CLERK_SECRET_KEY and a handshake nonce cookie. Trace how the result reaches requestState and define a distinguishable handshake failure or safe fallback; done means consumers can tell infrastructure failure apart from an ordinary signed-out state.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication, backend-api-design
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.