Skip to content

Sign-in "Continue" does nothing: client-setter timestamp guard drops every sign-in progression response #436

Description

@MarkOSullivan94

Summary

Every progressive sign-in flow (password, email code, phone code, email link, client trust) freezes on its first screen: tapping Continue appears to do nothing. The API call succeeds and the server creates/progresses the sign-in, but the response is silently discarded client-side.

Introduced in cd4b95b (“test(patrol): sign in with google (#410)”, sub-commit fix(clerk_flutter): sso race condition).

Root cause

That commit added an override of the client setter in ClerkAuthState (packages/clerk_flutter/lib/src/clerk_auth_state.dart):

@override
set client(clerk.Client newClient) {
  final newTs = newClient.updatedAt.microsecondsSinceEpoch;
  final currentTs = client.updatedAt.microsecondsSinceEpoch;
  if (newTs > 0 && currentTs > 0 && newTs <= currentTs) return;
  super.client = newClient;
}

It rejects any incoming client whose updated_at is not strictly newer than the current one — resting on the assumption that any meaningful change to the client bumps client.updated_at.

That assumption is false for sign-in progression: the Frontend API does not advance client.updated_at when a sign_in is created or moves through its statuses. The SDK’s own recorded fixtures show this — in packages/clerk_auth/test/_responses/clerk_auth/sign_in_test/, client.updated_at carries the identical timestamp on every response of every flow, even as sign_in.status progresses to complete.

Failure sequence

  1. App startup fetches the client — updated_at = T. Accepted; becomes the high-water mark.
  2. User enters an identifier and taps ContinueattemptSignInPOST /v1/client/sign_ins200 with sign_in.status: needs_first_factor and updated_at: T (unchanged).
  3. The setter guard sees newTs <= currentTs and returns without assigning. The new sign_in never lands in state.
  4. notifyListeners() fires, the panel rebuilds, signIn is still null → identifier screen again. Every tap repeats the cycle.

The same drop occurs at every later stage (first → second factor, code verification), so no progressive flow can complete.

Why nothing caught it

  • The fixture-backed sign-in tests drive Auth (clerk_auth), which has no guard — the override lives in ClerkAuthState (clerk_flutter).
  • Widget-test fixtures build each client with updatedAt: DateTime.now(), so successive test clients always have increasing timestamps and pass the guard — exactly the behavior the real API doesn’t have.
  • The patrol test added in the same commit covers Google SSO, where completion creates a session and the client genuinely changes — masking the progressive flows.

Reproduction

Deterministic reproduction (no live network): drive ClerkAuthState through a mocked sign-in where consecutive clients share one updated_at

  • state-level: attemptSignIn(strategy: password, identifier: …)signIn stays null;
  • widget-level: pump ClerkSignInPanel, enter an email, tap Continue → flow never reaches needs_first_factor.

Or manually: run the example app against any instance with password/email-code sign-in and tap Continue on the identifier screen.

Fix

The guard cannot simply be deleted — the refreshClient() race-condition tests depend on it screening stale/equal-timestamp refresh responses. Scope it instead: apply the timestamp check only while refreshClient() is resolving, so foreground auth responses (_housekeeping writes) are never screened and background refreshes keep exactly the protection they had.

A regression test should drive ClerkAuthState (not just Auth) with constant updated_at across responses and assert signIn.status progresses.

Related

🤖 Generated with Claude Code

Metadata

Metadata

Labels

bugSomething isn't workingclerk_flutterpackage: clerk_flutter

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions