Entering an email code submits it twice. The first attempt succeeds; the second replays the now-consumed code and comes back 401.
Harmless today only because the first one wins. It is a race, not a redundancy — and attempt_first_factor is not idempotent.
Steps to reproduce
- Sign in with
email_code.
- Enter the six-digit code into
MultiDigitCodeInput (typed or autofilled).
- Watch the network.
Expected results
One POST /v1/client/sign_ins/{id}/attempt_first_factor.
Actual results
Two, ~350 ms apart, same sign-in and same code:
15:55:06.562 POST /v1/client/sign_ins/sia_XXX/attempt_first_factor -> 200
15:55:06.916 POST /v1/client/sign_ins/sia_XXX/attempt_first_factor -> 401
Cause
MultiDigitCodeInput.updateEditingValue submits whenever the value reaches full length, with no guard against being called again while a submit is in flight:
@override
Future<void> updateEditingValue(TextEditingValue value) async {
setState(() => _editingValue = value);
if (value.text.length == widget.length) {
setState(() => loading = true);
final succeeded = await widget.onSubmit(value.text);
…
}
…
}
updateEditingValue is the TextInputClient callback, so the platform decides how often it fires. A second delivery carrying the same full-length text — which iOS produces routinely, and more so with autofillHints: [AutofillHints.oneTimeCode] set on this field — re-enters and submits again.
loading is set but never read before submitting, so it styles the widget rather than guarding it.
Suggested fix
Consult the in-flight state before submitting:
if (value.text.length == widget.length && !loading) {
or track submission separately from the display flag if loading is wanted for other reasons.
Why it matters beyond the wasted request
The second attempt increments attempts on the verification, so a flow that allows N tries silently gets N/2. And if the two ever land out of order — plausible under load, since they are concurrent — the 401 could be the one the UI observes, failing a sign-in that actually succeeded.
Code sample
Code sample
// No app code required — this is the SDK's own sign-in panel.
const ClerkAuthentication()
Logs
Logs
Captured with an HttpService decorator; identifiers redacted.
CLERKHTTP POST /v1/client/sign_ins/sia_XXX/attempt_first_factor -> 200
CLERKHTTP POST /v1/client/sign_ins/sia_XXX/attempt_first_factor -> 401
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.44.8, on macOS 26.6 25G72 darwin-arm64, locale en-US) [461ms]
• Flutter version 3.44.8 on channel stable at /opt/homebrew/share/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 058e0af2c2 (5 weeks ago), 2026-07-23 10:56:21 -0700
• Engine revision 0cd610717b
• Dart version 3.12.2
• DevTools version 2.57.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✗] Android toolchain - develop for Android devices [411ms]
✗ Unable to locate Android SDK.
Install Android Studio from: https://developer.android.com/studio/index.html
On first launch it will assist you in installing the Android SDK components.
(or visit https://flutter.dev/to/macos-android-setup for detailed instructions).
If the Android SDK has been installed to a custom location, please use
`flutter config --android-sdk` to update to that location.
[✓] Xcode - develop for iOS and macOS (Xcode 26.6) [952ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 17F113
• CocoaPods version 1.17.0
[✓] Chrome - develop for the web [4ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (4 available) [6.3s]
• My iPhone (mobile) • <redacted> • ios • iOS 26.6 23G71
• iPhone 17 Pro Max (mobile) • <redacted> • ios • com.apple.CoreSimulator.SimRuntime.iOS-26-5 (simulator)
• macOS (desktop) • macos • darwin-arm64 • macOS 26.6 25G72 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 151.0.7922.71
[✓] Network resources [403ms]
• All expected network resources are available.
! Doctor found issues in 1 category.
Entering an email code submits it twice. The first attempt succeeds; the second replays the now-consumed code and comes back
401.Harmless today only because the first one wins. It is a race, not a redundancy — and
attempt_first_factoris not idempotent.Steps to reproduce
email_code.MultiDigitCodeInput(typed or autofilled).Expected results
One
POST /v1/client/sign_ins/{id}/attempt_first_factor.Actual results
Two, ~350 ms apart, same sign-in and same code:
Cause
MultiDigitCodeInput.updateEditingValuesubmits whenever the value reaches full length, with no guard against being called again while a submit is in flight:updateEditingValueis theTextInputClientcallback, so the platform decides how often it fires. A second delivery carrying the same full-length text — which iOS produces routinely, and more so withautofillHints: [AutofillHints.oneTimeCode]set on this field — re-enters and submits again.loadingis set but never read before submitting, so it styles the widget rather than guarding it.Suggested fix
Consult the in-flight state before submitting:
or track submission separately from the display flag if
loadingis wanted for other reasons.Why it matters beyond the wasted request
The second attempt increments
attemptson the verification, so a flow that allows N tries silently gets N/2. And if the two ever land out of order — plausible under load, since they are concurrent — the401could be the one the UI observes, failing a sign-in that actually succeeded.Code sample
Code sample
Logs
Logs
Captured with an
HttpServicedecorator; identifiers redacted.Flutter Doctor output
Doctor output