Skip to content

feat(clerk-js,shared,ui): Add Protect SDK challenge support during sign-up and sign-in#8329

Draft
zourzouvillys wants to merge 1 commit intomainfrom
theo/protect-check-sdk-support
Draft

feat(clerk-js,shared,ui): Add Protect SDK challenge support during sign-up and sign-in#8329
zourzouvillys wants to merge 1 commit intomainfrom
theo/protect-check-sdk-support

Conversation

@zourzouvillys
Copy link
Copy Markdown
Contributor

Summary

Adds client-side support for Clerk Protect mid-flow SDK challenges during both sign-up and sign-in. When the antifraud service issues a challenge, the SDK now exposes the challenge data, surfaces a UI to load and execute the challenge script, and resolves the gate via a dedicated endpoint.

  • Adds a top-level protectCheck field and submitProtectCheck() method to both SignUp and SignIn resources
  • Adds a new 'needs_protect_check' value to the SignInStatus union
  • Adds a protect-check route to the prebuilt <SignIn /> and <SignUp /> components that loads the challenge SDK, submits the proof token, and resumes the original flow

Background

Previously, anti-fraud blocks could only happen at sign-in/sign-up create time. The new mechanism allows the service to gate at any step (e.g., between identifier and first-factor, before sending an SMS code, before finalizing). When gated, the response carries:

{
  "protect_check": {
    "status": "pending",
    "token": "<challenge token>",
    "sdk_url": "https://.../sdk.js",
    "expires_at": 1700000000000,
    "ui_hints": { ... }
  }
}

The client loads the SDK at sdk_url, executes the challenge with token, and submits the resulting proof token to PATCH /v1/client/sign_{ins,ups}/{id}/protect_check. The response either clears the gate, issues a chained challenge, or completes the flow.

A previous attempt at SDK support (#7894) targeted an earlier server API and was closed. This PR targets the current backend contract.

Implementation

Type additions (@clerk/shared)

  • ProtectCheckJSON / ProtectCheckResource with fields { status: 'pending', token, sdkUrl, expiresAt?, uiHints? }
  • protect_check?: ProtectCheckJSON | null on SignUpJSON and SignInJSON
  • 'protect_check' added to SignUpField (so it appears in missing_fields for sign-up)
  • 'needs_protect_check' added to SignInStatus
  • New submitProtectCheck method on SignUpResource, SignUpFutureResource, SignInResource, SignInFutureResource

Core resources (@clerk/clerk-js)

  • SignUp and SignIn now expose protectCheck and submitProtectCheck({ proofToken }) (PATCH .../protect_check)
  • fromJSON / __internal_toSnapshot round-trip the field
  • SignUpFuture / SignInFuture mirror the API for the experimental hooks

Flow orchestration

  • completeSignUpFlow adds a new protectCheckPath parameter and routes to it when protectCheck is present (or 'protect_check' is in missing_fields)
  • clerk._handleRedirectCallback (OAuth/SAML callback path) checks for the gate after the callback resolves
  • All sign-in operation switches (start, password, code, alternative channel, MFA backup code, MFA code form, reset password) detect the gate via a small isSignInProtectGated() helper and route to protect-check

Prebuilt UI (@clerk/ui)

  • New protect-check route on both <SignUp /> and <SignIn />
  • New SignUpProtectCheck / SignInProtectCheck card components that:
    • Dynamically import() the challenge SDK from sdkUrl
    • Invoke its default export with a container <div> and the resource
    • Submit the resulting proof token
    • Skip expired challenges (re-routes to issue a fresh one)
    • Handle chained challenges by self-navigating
    • Treat protect_check_already_resolved as a soft success and continue the flow
  • New ProtectCheckElement (mirrors CaptchaElement) — DOM container with a MutationObserver so external scripts can resize the host element

Backwards compatibility

  • All new fields are optional. Old SDK consumers ignore the new protect_check field on responses. Older clients hitting newer servers continue to work — the server emits 'needs_protect_check' only when a feature gate matches, falling back to the underlying status otherwise.
  • The 'needs_protect_check' addition to SignInStatus is type-additive but will surface as a new exhaustive-switch branch for downstream consumers using strict TypeScript.
  • No existing API surface is removed.

Risks

  • Custom flows (non-prebuilt UI) that switch on signIn.status need to handle 'needs_protect_check' (or the protectCheck field) themselves. Without handling, the UI will appear stuck at the previous step. The new fields are documented on the SignInResource interface.
  • Challenge SDK contract — the dynamically imported script must export a default function (container, resource) => Promise<string>. If a different SDK is loaded with a different signature, the challenge will fail and the user will see an error in the protect-check card.
  • Dynamic import URL — uses /* webpackIgnore: true */ so bundlers don't try to inline. The URL comes from the server response; it is always HTTPS but applications with strict CSP may need to allow the relevant script origin.
  • OAuth/SAML callbacks — the protect_check check now happens in _handleRedirectCallback before the existing transfer logic. Behavioral parity verified for all four res.status cases of the existing transfer path.
  • The 7 sign-in dispatch points are wired explicitly. Less-common cards (passkey, web3) are not yet wired and would still navigate to their default next step if a gate fires there — not unsafe (the gate persists across reloads), but the UX would be worse than for the wired paths.

Test plan

  • Unit: SignUp.test.ts — 4 new tests for serialization, optional fields, snapshot round-trip, submitProtectCheck API call
  • Unit: SignIn.test.ts — 5 new tests for the same surface
  • Unit: completeSignUpFlow.test.ts — 4 new tests for routing behavior (missing-field signal, field signal, priority over enterprise_sso, fallback when no path provided)
  • Build: @clerk/clerk-js, @clerk/shared, @clerk/ui all build clean
  • Lint: clean (no new warnings)
  • Manual: drive through a sign-up/sign-in with a Protect-enabled instance and confirm the challenge UI renders and resolves
  • Manual: verify chained challenge handling (server issues a second challenge after the first proof)
  • Manual: verify expired-challenge auto-recovery (set a short expiry, wait, retry)
  • Manual: verify the OAuth/SAML callback path with a gating instance

Out of scope (follow-ups)

  • Localization keys for the protect-check card titles (currently hardcoded English placeholders)
  • Component-level tests for SignInProtectCheck / SignUpProtectCheck (would need to mock dynamic import())
  • @clerk/backend resource model updates (backend SDK doesn't drive end-user flows)

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Apr 16, 2026 7:17am

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 16, 2026

🦋 Changeset detected

Latest commit: 8529397

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@clerk/clerk-js Minor
@clerk/shared Minor
@clerk/ui Minor
@clerk/chrome-extension Patch
@clerk/expo Patch
@clerk/agent-toolkit Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/hono Patch
@clerk/localizations Patch
@clerk/msw Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/react Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/vue 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

…gn-up and sign-in

Adds client-side support for mid-flow SDK challenges issued by the antifraud
service during sign-up and sign-in.

- New `protectCheck` field and `submitProtectCheck()` method on SignUp and SignIn resources
- New `'needs_protect_check'` value on the SignInStatus union
- New `protect-check` route on the prebuilt `<SignIn />` and `<SignUp />` components
  that loads the challenge SDK, submits the proof token, and resumes the flow
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant