Skip to content

feat(clerk-js): Add sign_up_if_missing for SignIn.create#7749

Open
dmoerner wants to merge 4 commits intomainfrom
daniel/user-4596-sign_up_if_missing-support-custom-flow-with-captcha-in-sdk
Open

feat(clerk-js): Add sign_up_if_missing for SignIn.create#7749
dmoerner wants to merge 4 commits intomainfrom
daniel/user-4596-sign_up_if_missing-support-custom-flow-with-captcha-in-sdk

Conversation

@dmoerner
Copy link
Contributor

@dmoerner dmoerner commented Feb 3, 2026

Description

We are building a new sign-in-or-sign-up flow compatible with strict enumeration protection. It is meant to complement our existing sign-in-or-sign-up flow.

Our current sign-in-or-sign-up flow is managed by the SDKs: We start with a sign in attempt, and on a 422 (when an account does not exist), we redirect to a sign up. The flow is thus: Sign In (422) -> Sign Up (200) -> Attempt Verifications for VerifiedAtSignUp identifiers (200). This is vulnerable to user enumeration attacks because the attacker sees the sign in to sign up redirect before they prove their identity by completing a verification.

When sign_up_if_missing is passed as a param when POSTing a sign in, instead we do the following: Sign In (200) -> Attempt Verification for Identifier (200) -> Create User and Session. (In future work this third step will be modified to support adding additional information to the user, either via AccountTransfer or Session Tasks). This is enumeration safe, because you only see if an account already existed or was created after you verify your identity.

This PR is the first step in SDK support for this new flow. We add support for the optional sign_up_if_missing param on SignIn. We also add captcha support for SignIn. This is all optional and currently in testing with custom components. Support in Clerk components will be in future PRs.

Fixes USER-4596

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • [ ]

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • New Features

    • Sign-in now supports sign_up_if_missing with automated CAPTCHA handling and browser locale injection; CAPTCHA options include "signin".
  • Improvements

    • Transfer sign-in parameters extended to carry locale and CAPTCHA metadata for more robust external-account flows.
  • Tests

    • Expanded test coverage for CAPTCHA and sign_up_if_missing across sign-in flows.
  • Chores

    • Release metadata updated and bundle size threshold slightly adjusted.

We are building a new sign-in-or-sign-up flow compatible with strict
enumeration protection. It is meant to complement our existing
sign-in-or-sign-up flow.

Our current sign-in-or-sign-up flow is managed by the SDKs: We start
with a sign in attempt, and on a 422 (when an account does not exist),
we redirect to a sign up. The flow is thus: Sign In (422) -> Sign Up
(200) -> Attempt Verifications for VerifiedAtSignUp identifiers (200).
This is vulnerable to user enumeration attacks because the attacker sees
the sign in to sign up redirect before they prove their identity by
completing a verification.

When `sign_up_if_missing` is passed as a param when POSTing a sign in,
instead we do the following: Sign In (200) -> Attempt Verification for
Identifier (200) -> Create User and Session. (In future work this third
step will be modified to support adding additional information to the
user, either via AccountTransfer or Session Tasks). This is enumeration
safe, because you only see if an account already existed or was created
after you verify your identity.

This PR is the first step in SDK support for this new flow. We add
support for the optional `sign_up_if_missing` param on `SignIn`. We also
add captcha support for `SignIn`. This is all optional and currently in
testing with custom components. Support in Clerk components will be in
future PRs.
@changeset-bot
Copy link

changeset-bot bot commented Feb 3, 2026

🦋 Changeset detected

Latest commit: 968af9e

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

This PR includes changesets to release 20 packages
Name Type
@clerk/clerk-js Minor
@clerk/shared 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/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/ui 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

@vercel
Copy link

vercel bot commented Feb 3, 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 Feb 3, 2026 9:26pm

Request Review

finalParams = { ...finalParams, ...captchaParams };
}

if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check guards against an attacker who tries to make a fake transfer and then changes strategy at the same time, which would be an attempt to get around WAF rules and Captcha rules that exclude transfers. We already guard against this on the backend on both sign in and sign up, but in the SDK we were only checking it on sign up. So I took this opportunity to also add the check on sign in.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

Adds a changeset and implements sign_up_if_missing support in SignIn.create. SignIn.create is now async, injects browser locale, and includes a runtime CAPTCHA flow with shouldRequireCaptcha, shouldBypassCaptchaForAttempt, and getCaptchaToken plus transfer-bypass handling; finalParams are merged into the POST body. SignInFuture gained a private _create mirroring CAPTCHA/locale behavior. Types updated: SignInCreateParams and SignInFutureCreateParams include locale, sign_up_if_missing, captcha_token, captcha_error, captcha_widget_type. CaptchaOptions.action now includes signin. Tests extended for CAPTCHA scenarios.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature addition: support for sign_up_if_missing parameter on SignIn.create, which is the primary objective of this PR.
Linked Issues check ✅ Passed All coding objectives from USER-4596 are met: sign_up_if_missing parameter added to SignIn.create and captcha support implemented for SignIn to enable the enumeration-safe flow.
Out of Scope Changes check ✅ Passed All changes are within scope. The PR adds sign_up_if_missing support, captcha integration, type updates, test coverage, and a minor bundle size bump—all directly supporting the USER-4596 objective for custom flows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 3, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7749

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7749

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7749

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7749

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7749

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7749

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@7749

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7749

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7749

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7749

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7749

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7749

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7749

@clerk/react

npm i https://pkg.pr.new/@clerk/react@7749

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7749

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7749

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7749

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7749

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@7749

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7749

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7749

commit: 968af9e

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/clerk-js/src/core/resources/SignIn.ts`:
- Around line 185-192: The code accesses nested properties on
SignIn.clerk.client without guarding signUp/verifications/externalAccount;
change the retrieval of strategy in the SignIn transfer branch to use safe
checks (e.g. optional chaining or explicit null checks) so you do const strategy
= SignIn.clerk.client?.signUp?.verifications?.externalAccount?.strategy and only
assign (finalParams as TransferParams).strategy = strategy as
TransferParams['strategy'] if strategy is not undefined/null; update the logic
inside the transfer block (related to shouldBypassCaptchaForAttempt and
finalParams assignment) to avoid any potential TypeError from missing
intermediate objects.
- Around line 621-629: The transfer branch accesses
SignIn.clerk.client!.signUp.verifications.externalAccount.strategy directly
which can throw if signUp/verifications/externalAccount is undefined; update the
condition inside the captchaOauthBypass check to use optional chaining (e.g.,
SignIn.clerk.client?.signUp?.verifications?.externalAccount?.strategy) and
compare safely (guarding null/undefined) so the predicate returns false when the
nested property is missing, ensuring the transfer path doesn't cause a runtime
error.

Comment on lines +185 to +192
if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) {
const strategy = SignIn.clerk.client?.signUp.verifications.externalAccount.strategy;
if (strategy) {
// When transfer is true, we're in the OAuth/Enterprise SSO transfer case
type TransferParams = Extract<SignInCreateParams, { transfer: true }>;
(finalParams as TransferParams).strategy = strategy as TransferParams['strategy'];
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential null reference when accessing signUp properties during transfer.

Line 186 uses optional chaining on client? but not on the subsequent property chain. If client exists but signUp, verifications, or externalAccount is null/undefined, this will throw a TypeError.

Proposed fix
     if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) {
-      const strategy = SignIn.clerk.client?.signUp.verifications.externalAccount.strategy;
+      const strategy = SignIn.clerk.client?.signUp?.verifications?.externalAccount?.strategy;
       if (strategy) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) {
const strategy = SignIn.clerk.client?.signUp.verifications.externalAccount.strategy;
if (strategy) {
// When transfer is true, we're in the OAuth/Enterprise SSO transfer case
type TransferParams = Extract<SignInCreateParams, { transfer: true }>;
(finalParams as TransferParams).strategy = strategy as TransferParams['strategy'];
}
}
if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) {
const strategy = SignIn.clerk.client?.signUp?.verifications?.externalAccount?.strategy;
if (strategy) {
// When transfer is true, we're in the OAuth/Enterprise SSO transfer case
type TransferParams = Extract<SignInCreateParams, { transfer: true }>;
(finalParams as TransferParams).strategy = strategy as TransferParams['strategy'];
}
}
🤖 Prompt for AI Agents
In `@packages/clerk-js/src/core/resources/SignIn.ts` around lines 185 - 192, The
code accesses nested properties on SignIn.clerk.client without guarding
signUp/verifications/externalAccount; change the retrieval of strategy in the
SignIn transfer branch to use safe checks (e.g. optional chaining or explicit
null checks) so you do const strategy =
SignIn.clerk.client?.signUp?.verifications?.externalAccount?.strategy and only
assign (finalParams as TransferParams).strategy = strategy as
TransferParams['strategy'] if strategy is not undefined/null; update the logic
inside the transfer block (related to shouldBypassCaptchaForAttempt and
finalParams assignment) to avoid any potential TypeError from missing
intermediate objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a sign in and a transfer and there's no signup or verification or externalaccount, that would be an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmoerner, thank you for the clarification. You're right—in this transfer flow context, if those nested properties are missing, that would indicate a legitimate error state that should surface rather than be silently handled. The current implementation with optional chaining only on client is appropriate here.

Comment on lines +621 to +629
if (
params.transfer &&
captchaOauthBypass.some(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
strategy => strategy === SignIn.clerk.client!.signUp.verifications.externalAccount.strategy,
)
) {
return true;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same null reference risk in the transfer branch.

Line 625 accesses signUp.verifications.externalAccount.strategy without optional chaining. This could throw if the signUp state is incomplete during a transfer scenario.

Proposed fix
     if (
       params.transfer &&
       captchaOauthBypass.some(
         // eslint-disable-next-line `@typescript-eslint/no-non-null-assertion`
-        strategy => strategy === SignIn.clerk.client!.signUp.verifications.externalAccount.strategy,
+        strategy => strategy === SignIn.clerk.client?.signUp?.verifications?.externalAccount?.strategy,
       )
     ) {
🤖 Prompt for AI Agents
In `@packages/clerk-js/src/core/resources/SignIn.ts` around lines 621 - 629, The
transfer branch accesses
SignIn.clerk.client!.signUp.verifications.externalAccount.strategy directly
which can throw if signUp/verifications/externalAccount is undefined; update the
condition inside the captchaOauthBypass check to use optional chaining (e.g.,
SignIn.clerk.client?.signUp?.verifications?.externalAccount?.strategy) and
compare safely (guarding null/undefined) so the predicate returns false when the
nested property is missing, ensuring the transfer path doesn't cause a runtime
error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in the other case, a transfer with a sign up that's incomplete in the sense of missing an external account is an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmoerner, understood. If the external account is expected to exist during transfer scenarios, the assertion is appropriate to catch that error early. Thanks for the clarification.

captcha_token?: string;
captcha_error?: unknown;
captcha_widget_type?: string | null;
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention with these types was for SignInCreateParams to match what we send to FAPI, which is why I added to the discriminated union. Another approach would be to reserve it for what is passed into the SDK method, and use implicit typing or just Record<string,unknown> for what we sent to FAPI. The latter approach actually fits better what we already do on sign up but it felt haphazard to me.

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