feat(clerk-js,shared,ui): Add Protect SDK challenge support during sign-up and sign-in#8329
feat(clerk-js,shared,ui): Add Protect SDK challenge support during sign-up and sign-in#8329zourzouvillys wants to merge 3 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: f013b20 The changes in this PR will be included in the next version bump. This PR includes changesets to release 20 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
| return useCallback(async (...args: Parameters<typeof authenticateWithPasskey>) => { | ||
| try { | ||
| const res = await authenticateWithPasskey(...args); | ||
| // Per spec §2.3 / §4: protect_check can fire on attempt_first_factor (which is what |
There was a problem hiding this comment.
[minor] is this a reference to an LLM spec document?
| <Route path='create'> | ||
| <Route |
There was a problem hiding this comment.
I believe this route segment also needs a protect-check path. Looks like we have navigations to create/protect-check in the combined flow
| <Route path='create'> | |
| <Route | |
| path='protect-check' | |
| canActivate={clerk => !!clerk.client.signUp.protectCheck} | |
| > | |
| <LazySignUpProtectCheck /> | |
| </Route> | |
| <Route |
| }); | ||
| } | ||
|
|
||
| // Per Protect spec §4.4: OAuth/SAML callbacks can result in a protect_check gate that |
There was a problem hiding this comment.
What spec is this referring to?
|
@zourzouvillys The core stuff looks good. I think the biggest gap is the routing logic integration. Feels like it this is targeting the standalone / , but the combined flows are not hooked up properly. |
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.