Skip to content

Add clarity surrounding user enumeration protection#3365

Open
alexisintech wants to merge 5 commits into
mainfrom
aa/DOCS-11693
Open

Add clarity surrounding user enumeration protection#3365
alexisintech wants to merge 5 commits into
mainfrom
aa/DOCS-11693

Conversation

@alexisintech
Copy link
Copy Markdown
Member

🔎 Previews:

What does this solve? What changed?

secure/user-enumeration-protection: tested bulk versus strict user enumeration flows when signing up/signing in and updated the docs accordingly.

development/custom-flows/authentication/sign-in-or-up: updated the copy to more clearly explain how the two flow options relate to user enumeration protection settings.

Deadline

No rush

Other resources

https://linear.app/clerk/issue/DOCS-11693/clarify-sign-in-or-up-behavior-with-strict-user-enumeration-protection

@alexisintech alexisintech requested a review from a team as a code owner May 13, 2026 00:07
@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

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

Project Deployment Actions Updated (UTC)
clerk-docs Ready Ready Preview May 15, 2026 0:45am

Request Review

@manovotny manovotny self-assigned this May 13, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@manovotny
Copy link
Copy Markdown
Contributor

manovotny commented May 13, 2026

Pushed a few small cleanups directly in 7c3e04a.

  • Moved the Standard sign-in-or-up flow TIP up to sit directly under its H2 so it mirrors the signUpIfMissing TIP's placement (the TIP describes the flow as a whole, not just the build step).
  • Changed flowsbehavior in both TIPs to match the intro sub-bullets and fix the singular-subject/plural-object mismatch.
  • Tightened the strict email-code line in user-enumeration-protection.mdx to drop the doubled "there was": … explaining that there was a sign-in attempt, but there was no account found.… explaining that someone attempted to sign in to a non-existent account.

Left a couple of pending review comments on broader questions I didn't have answers to.

Let me know what you think.

- Sign in: When the account exists, sign-in proceeds normally. When it doesn't, Clerk's behavior depends on the strategy:
- **Password or Web3 wallet**: The attempt is rejected without revealing whether the account exists.
- **Email code or email link**: Clerk sends a notification to the address explaining that someone tried to sign in, and suggesting they sign up instead.
- **Email code or email link**: Clerk shows the verification screen and sends a notification to the address explaining that someone attempted to sign in to a non-existent account.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This says strict's email-code path sends a notification only (no real code, so the user is stuck on the verification screen), but the signUpIfMissing flow doc says it sends a real code that succeeds and transfers to sign-up.

If prebuilt components use signUpIfMissing under the hood when strict is on and public sign-up is allowed, this line might be describing the restricted/waitlist case specifically.

Should we narrow this to the scenario it actually covers, or update the signUpIfMissing flow doc to match?

Copy link
Copy Markdown
Member Author

@alexisintech alexisintech May 13, 2026

Choose a reason for hiding this comment

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

oh you know what, it's because I was testing with the fresh nextjs-quickstart app and I assumed the <SignIn /> component was a combined flow (both sign in and up) but I see you have to opt-in with a prop or env var

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

okay just tested with the combined flow, and this is still the case. its sending no account found emails, it never sends a code to sign up with... maybe I'm doing this incorrectly?

I have a fresh app, I have withSignUp={true} on the <SignIn /> component so that I am opted in to the combined flow, and this does reflect in the rendered component. however, I try to enter an email for an account that doesn't exist, and I get a "Sign in attempted, no account found" email - I never a get a code to sign-up with.

Image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

so this might be an API issue?

Copy link
Copy Markdown
Contributor

@manovotny manovotny May 13, 2026

Choose a reason for hiding this comment

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

After pulling up packages/ui/src/components/SignIn/SignInStart.tsx in clerk/javascript and api/shared/sign_in/service.go in clerk_go —you're not doing anything wrong, and your test matches what line 38 now describes. I was wrong and didn't dig deep enough.

The prebuilt <SignIn withSignUp={true} /> combined flow calls signIn.create(...) without signUpIfMissing and only transfers to sign-up when FAPI returns FORM_IDENTIFIER_NOT_FOUND (SignInStart.tsx lines 444–467). When strict is on, FAPI suppresses that error and serves an enumeration-protection response (fake verification screen + notification email), so the transfer branch is unreachable. That's the dead end you're seeing.

signUpIfMissing is a different code path on the server. In service.go around line 1597, FAPI does:

enumerationProtectionEnabled := userSettings.EnumerationProtectionEnabled() || signIn.SignUpIfMissing

So when the client opts in, FAPI behaves as if strict were on regardless of the setting, but it also enables the CreateTransferForSignUpIfMissing path that turns a missing user into a real sign_up_if_missing_transfer after a successful verification. The prebuilt components never opt into this, so under strict they can't reach the transfer.

The cleaner framing:

  • Standard custom flow ≈ prebuilt <SignIn withSignUp /> = behavior under bulk / no protection ✅ — the "mimics bulk" line works.
  • signUpIfMissing ≠ prebuilt under strict. It's more capable: under strict, prebuilt gets stuck; signUpIfMissing completes the transfer.

TL;DR: We could:

  1. Leave this alone/as-is.
  2. We could drop the "mimics strict" line on the signUpIfMissing flow (both the TIP and the intro sub-bullet) and reframe it as the recommended custom flow when strict is enabled (line 1239 already half-says that).

Totally your call.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ooo this is great info. so we should also document that the sign-in-or-up flow with prebuilt components/account portal will not work (sign-in's will not transfer to sign-up's) if strict user enumeration is turned on.

and then as for fixing this issue at hand, let's drop the mimics stuff. i will update it now

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

actually, i readded the tips but changed the word 'mimic' to 'similar to'
because the whole idea behind adding that information is that if someone coming to build a sign-in-or-up custom flow, and they want to know how to recreate a clerk flow, they should know which option to pick in relation to which settings they have enabled in the clerk dashboard.
does that make sense? totally open to reworking it the execution of it, but that's the idea

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The "similar to" swap works for the bulk pairing — the Standard flow really does behave like the prebuilt components under bulk, so that one's fine as-is.

The strict one still doesn't quite hold, though. Since the prebuilt components / Account Portal can't complete a sign-in-or-up under strict (the sign-in never transfers to sign-up), signUpIfMissing isn't similar to what they do under strict — it does the thing they can't. And it'd sit right next to the caveat you mentioned adding about prebuilt not working under strict, so the two would read as a contradiction.

Your goal still works, I think it just needs a different framing for the strict case. Instead of "similar to prebuilt behavior," maybe anchor it as "use this flow when strict is enabled, because prebuilt can't." Something like:

Use this flow if your application has strict user enumeration protection enabled. The Account Portal and prebuilt components can't complete a sign-in-or-up under strict — a sign-in for a non-existent account won't transfer to sign-up — so signUpIfMissing is how you offer sign-in-or-up with strict turned on.

Same idea for the strict intro sub-bullet. That folds your "prebuilt doesn't work under strict" note in as the reason, rather than a separate caveat.

Does that make sense, or am I not understanding it correctly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've updated the copy again to make it more clear - what do you think 👀

Comment thread docs/guides/development/custom-flows/authentication/sign-in-or-up.mdx Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants