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
Draft
feat(clerk-js,shared,ui): Add Protect SDK challenge support during sign-up and sign-in#8329zourzouvillys wants to merge 1 commit intomainfrom
zourzouvillys wants to merge 1 commit intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 0f726f7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 21 packages
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 |
24c3383 to
8529397
Compare
8529397 to
63aa1cd
Compare
63aa1cd to
a8d12d1
Compare
a8d12d1 to
2e82e39
Compare
2e82e39 to
ac07df4
Compare
ac07df4 to
b7e6942
Compare
…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
b7e6942 to
0f726f7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 exposes the challenge data, surfaces a UI to load and execute the challenge script, and resolves the gate via a dedicated endpoint.
protectCheckfield andsubmitProtectCheck()method on bothSignUpandSignInresources'needs_protect_check'value on theSignInStatusunionprotect-checkroute on the prebuilt<SignIn />and<SignUp />components that loads the challenge SDK, submits the proof token, and resumes the original flowBackground
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": { "reason": "device_new" } } }The client loads the SDK at
sdk_url, executes the challenge withtoken, and submits the resulting proof token toPATCH /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/ProtectCheckResourcewith fields{ status: 'pending', token, sdkUrl, expiresAt?, uiHints? }protect_check?: ProtectCheckJSON | nullonSignUpJSONandSignInJSON'protect_check'added toSignUpField(so it appears inmissing_fieldsfor sign-up)'needs_protect_check'added toSignInStatussubmitProtectCheckmethod onSignUpResource,SignUpFutureResource,SignInResource,SignInFutureResourceCore resources (
@clerk/clerk-js)SignUpandSignInnow exposeprotectCheckandsubmitProtectCheck({ proofToken })(PATCH .../protect_check)fromJSON/__internal_toSnapshotround-trip the fieldSignUpFuture/SignInFuturemirror the API for the experimental hooksSDK loader helper (
@clerk/shared/internal/clerk-js/protectCheck)Single shared helper used by both UI components:
sdkUrlmust parse, must behttps:, must not contain credentials. Rejectsdata:/blob:/javascript:and credentialed URLs at the helper boundary (fail-closed: the gate stays present, the user can't bypass it).(container, { token, uiHints, signal }), NOT the full sign-up/sign-in resource. This matches FAPI spec §5.2 and minimizes the trust surface granted to third-party Protect scripts.awaitso an uncooperative SDK can't return a token after abort.protect_check_invalid_sdk_url,protect_check_aborted,protect_check_script_load_failed(with CSP-aware guidance),protect_check_invalid_script,protect_check_execution_failed. Error messages do NOT includesdkUrl(avoids exposing an attacker-controlled URL in the auth UI).Flow orchestration
completeSignUpFlowadds a newprotectCheckPathparameter and routes to it whenprotectCheckis present (or'protect_check'is inmissing_fields)clerk._handleRedirectCallback(OAuth/SAML callback path) checks for the gate after the callback resolves (per spec §4.4)isSignInProtectGated()helper and route toprotect-check. Wired into 8 places: SignInStart (×2), SignInFactorOnePasswordCard, SignInFactorOneCodeForm, SignInFactorOneAlternativeChannelCodeForm, SignInFactorTwoBackupCodeCard, SignInFactorTwoCodeForm, ResetPassword, and the passkey handler inshared.ts. Web3 / OAuth / SAML / Solana sign-in paths useauthenticateWithRedirect(which redirects out of the SDK), so any post-redirect gate is caught by the centralized_handleRedirectCallbackcheck inclerk.ts.Prebuilt UI (
@clerk/ui)protect-checkroute on both<SignUp />and<SignIn />, gated bycanActivate: clerk => !!clerk.client.signUp.protectCheck(or signIn)SignUpProtectCheck/SignInProtectCheckcard components that:executeProtectCheckwith the protect_check object and a ref-attached container divsubmitProtectCheck({ proofToken })expiresAt: reload the resource so the server mints a fresh challenge (avoids infinite loop on stale local state)protectCheck): self-navigate to.to re-runprotect_check_already_resolved(HTTP 400): reload the resource, then route based on the refreshed statussetActive(sign-in) or finalize viacompleteSignUpFlow(sign-up)AbortController+ acancelledflag, gating every state update after everyawaitso unmount-mid-challenge doesn't trigger React-state-on-unmounted-component warnings or stray network callsSignInProtectChecknavigateNexthelper handles'needs_protect_check'explicitly (self-navigates) for forward compatibility with non-blocking checksLocalization (
@clerk/localizations,@clerk/shared)signUp.protectCheck.{title,subtitle,loading}andsignIn.protectCheck.{title,subtitle,loading}in the__internal_LocalizationResourceschema with English source values inen-US.tsunstable__errorsentries for the runtime error codes the helper can produce:action_blocked,protect_check_aborted(intentionally undefined — user-cancelled, not surfaced),protect_check_already_resolved(intentionally undefined — soft-success),protect_check_execution_failed,protect_check_invalid_script,protect_check_invalid_sdk_url,protect_check_script_load_failed. The error-code lookup is automatic viatranslateError()— other locales can opt in by adding their own translations, otherwise they fall back to the English source.Backwards compatibility
protect_checkfield on responses.'needs_protect_check'only when a feature gate matches, falling back to the underlying status otherwise.'needs_protect_check'addition toSignInStatusis type-additive but will surface as a new exhaustive-switch branch for downstream consumers using strict TypeScript.Risks
signIn.statusneed to handle'needs_protect_check'(or theprotectCheckfield) themselves. Without handling, the UI will appear stuck at the previous step. The new fields are documented on theSignInResourceinterface.(container, { token, uiHints, signal }) => Promise<string>. The container is a plain<div>mounted inside the Clerk card; the Protect SDK is responsible for rendering inside it (including any iframe sandboxing the SDK chooses to apply). Coordinate the contract with the Protect SDK team before deploying.script-src. The helper's load-failure error message explicitly calls this out._handleRedirectCallbackbefore the existing transfer logic. Behavioral parity verified against the existing transfer paths.authenticateWithRedirectand are caught after the redirect via_handleRedirectCallback.Test plan
SignUp.test.ts— 4 new tests for serialization, optional fields, snapshot round-trip,submitProtectCheckAPI callSignIn.test.ts— 5 new tests for the same surfaceprotectCheck.test.ts— 14 new tests covering URL validation (HTTPS, no credentials, nodata:/javascript:), script invocation contract (only spec-defined fields passed), cancellation (pre-load, mid-execution, uncooperative SDK), error wrapping (load / invalid-script / execution failure / no URL leakage)completeSignUpFlow.test.ts— 4 new tests for routing behavior (missing-field signal, field signal, priority over enterprise_sso, fallback when no path provided)SignUpProtectCheck.test.tsx— 7 tests (renders, runs SDK, expiry → reload,protect_check_already_resolved→ reload + continue, chained challenge self-navigation, abort on unmount, no-submit on SDK failure)SignInProtectCheck.test.tsx— 9 tests (same as sign-up plus status routing forneeds_first_factor,needs_second_factor,complete/finalize)@clerk/clerk-js,@clerk/shared,@clerk/localizations,@clerk/uiall build cleanOut of scope (follow-ups)
@clerk/backendresource model updates (backend SDK doesn't drive end-user flows)protectCheck !== nullbutstatusis the underlying value; the server doesn't emit it today and this PR treats every gate as blocking. Adding non-blocking support is additive when the server starts emitting it.