Skip to content

Single-flight session restore so startup no longer clears the session - #717

Merged
gjwgit merged 4 commits into
devfrom
gjw/716_single_flight_session_restore
Sep 13, 2026
Merged

gjwgit merged 4 commits into
devfrom
gjw/716_single_flight_session_restore

Conversation

@gjwgit

@gjwgit gjwgit commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes the login page being shown on every start even while the session
was still valid.

Problem

Two callers reach AuthDataManager.loadAuthData() on the first frame —
solidui's auto-login (solid_login.dart:274) and its login-status
notifier (solid_login_status_notifier.dart:68, via isUserLoggedIn()
getAccessToken()). The slow path had no in-flight guard, and the
await secureStorage.read yields, so neither caller blocked the other:
both built their own SolidAuthManager and both POSTed a
refresh_token grant with the same stored token.

The server issues single-use refresh tokens, so the second grant came
back invalid_grant, and tryRestoreSession() responds to a failed
refresh by clearing the stored session — discarding the session the
first caller had just successfully refreshed. Deterministic rather than
intermittent, because both calls start in the same frame.

No solidpod code change caused this: lib/ is byte-identical between
1.0.19 and 1.0.20. The trigger was the dependency bump in 1.0.20, where
solid_auth 1.0.7 sets initMode: OidcInitMode.blockingValidate and so
makes init() reach the token endpoint during restore — turning a
redundant-but-harmless double restore into a double spend.

Fix

Concurrent callers now share one restore, and one refresh once a manager
is live, since that path double-spends the token the same way. The slow
path moved verbatim into _restoreFromStorage(); loadAuthData() is no
longer async so the ??= assignment happens before any await point,
which is what closes the window.

A completing future cannot null out a newer in-flight future, because a
new one is only created once the field is already null.

Testing

Verified with todopod on Linux desktop via a path override. Before, the
whole startup block appeared twice and ended in two Clearing stored session. After:

INFO  Attempting to restore previous session          ×1  (was ×2)
INFO  Session restored for: .../profile/card#me       ×1  (was ×0)
      Stored tokens not found                         ×0  (was ×2)
      Clearing stored session                         ×0  (was ×2)
      Starting Solid-OIDC login                       ×0  (was ×1, forced)

The app goes straight to its data instead of the login page, and the
session held for the whole run — no clearing or forced login afterwards.

Worth noting grant_type=refresh_token was ×0 on the successful run:
the cached access token was still valid, so no refresh was needed. That
is the behaviour originally lost, and it confirms the mechanism — the old
code's two racing restores each forced a token-endpoint round trip and
destroyed a session that never needed refreshing.

Not covered

Restore across an actually-expired access token, where a single refresh
does fire. That path is unexercised, so the rotating-token behaviour is
still unverified in practice.

Closes #716

@gjwgit

gjwgit commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Tested extensively with todopod, notepod, diarypod, innerpod, billipod, ++

@gjwgit
gjwgit merged commit 070e467 into dev Sep 13, 2026
20 checks passed
@gjwgit
gjwgit deleted the gjw/716_single_flight_session_restore branch September 13, 2026 19:53
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.

Session restore always fails: concurrent loadAuthData() calls double-spend the rotating refresh token

1 participant