Add clarity surrounding user enumeration protection#3365
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed a few small cleanups directly in 7c3e04a.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
so this might be an API issue?
There was a problem hiding this comment.
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;signUpIfMissingcompletes the transfer.
TL;DR: We could:
- Leave this alone/as-is.
- We could drop the "mimics strict" line on the
signUpIfMissingflow (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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
signUpIfMissingis 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?
There was a problem hiding this comment.
I've updated the copy again to make it more clear - what do you think 👀
🔎 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