Skip to content

Persist Snapmaker cloud login across restarts (supersedes #479) - #644

Draft
respinoza wants to merge 4 commits into
Snapmaker:mainfrom
respinoza:pr/login-persistence
Draft

Persist Snapmaker cloud login across restarts (supersedes #479)#644
respinoza wants to merge 4 commits into
Snapmaker:mainfrom
respinoza:pr/login-persistence

Conversation

@respinoza

@respinoza respinoza commented Jul 29, 2026

Copy link
Copy Markdown

Fixes #116, fixes #226. Supersedes #479, whose two commits are included unchanged with @PGMacDesign's authorship. Built through the flatpak CI and verified on a real U1 setup (CachyOS, KDE Plasma 6).

Scope

The desktop login flow returns exactly one credential: id.snapmaker.com/?from=orca ends at account.snapmaker.com/?token=…, whose only parameter is a JWT with iat/exp 24 hours apart, scope openid profile mqtt, no offline_access, no refresh token. So restarts within 24h no longer cost a login (today every launch does); after that the user signs in again. Lifting that ceiling needs a refresh token or silent re-auth via the webview session — your call, not in this PR.

Why #479 alone didn't work

  • accounts/current reports auth failures as HTTP 200 with {"code":110002,"msg":"Missing authorization"} (verified by replaying an expired token; the web UI's table also lists 110003, 110004). Persist Snapmaker cloud login across restarts (secure token store + startup race fix) #479 accepted any 2xx, so a dead token yielded a "logged in" state with an empty profile and the UI still showed Login/Register. Restore now validates the body envelope via sm_parse_account_response, shared with the login dialog, tolerating code/id as int or numeric string (only the id.snapmaker.com envelope could be observed live).
  • Clearing only on definitive rejection — those codes or HTTP 401/403. Transient failures keep the token, so a flaky startup connection no longer signs the user out.
  • Duplicate keyring items — storing under varying attributes forked the token across items and restore could load a stale one. Now exactly one item: Windows/macOS delete-before-save under a fixed attribute set; Linux relies on libsecret replacing the item matching the same schema/attributes.
  • Races — a login epoch guards the async restore against clobbering a manual login or logout, and rides through the async keyring write so a store landing after a logout neither recreates the marker nor leaves the item behind. The revalidation request has a 30s timeout and a 64 KiB response cap; on exit it is cancelled and its callbacks are disarmed through a weak liveness token, since Http::cancel() alone cannot suppress a completion that has already begun on the detached worker thread.

Linux: Secret portal instead of wxSecretStore

In the Flatpak build, libsecret selects its file backend behind org.freedesktop.portal.Secret: the token lives in an encrypted per-app keyring and the manifest needs no org.freedesktop.secrets permission, so the sandbox cannot read other apps' secrets. Unsandboxed builds (AppImage, distro packages) get no such isolation — same code, normal Secret Service, token in the user's login keyring. Windows/macOS keep wxSecretStore. No portal and no keyring means signing in each launch.

Only the token is persisted; profile fields are re-fetched on restore. A non-secret [sm_login]/has_session marker means startup never touches the keyring (or triggers an unlock prompt) for users who never signed in.

Also from #479: the subscribe-time push in sw_SubscribeUserLoginState, so a session restored before the web UI subscribes isn't lost. Gated on m_event_id != "" to leave plain subscribe calls' response shape unchanged.

Verification

Log in → quit → relaunch restores in ~2s, repeatedly, over a day of slicing until the token hit its natural expiry — at which point it was cleared and a fresh login requested, as intended. Token only in the per-app keyring (host KDE wallet has no Snapmaker entries); a simulated server error leaves it intact; device connect and sending prints work against current main including #468.

Caveats, out of scope

  • Flagging, not changing: Http disables TLS certificate and hostname verification globally (CURLOPT_SSL_VERIFYPEER/VERIFYHOST = 0 in Http::priv::priv, present since the original BambuStudio import). Nothing in this PR touches it and the feature does not depend on it, but persisting a token does mean that credential is now replayed at each startup over unverified TLS rather than being sent once per interactive login. We don't know the original reason for the setting — if it's incidental rather than deliberate, it may deserve its own issue.
  • libsecret's file backend keeps a .keyring~ backup, so a cleared token's ciphertext can linger inside the sandbox. Deleting another library's private on-disk format seemed worse than documenting it.
  • No "keep me signed in" toggle; persistence is unconditional after login. Happy to add one.

Snapmaker cloud login was not persisted on Linux/AppImage (and elsewhere):
every restart forced a fresh OAuth login. Persist the session so it survives
restarts, revalidating the stored token on startup.

- Store only the bearer token, and only in the OS secret store via wxSecretStore
  (libsecret / Credential Manager / Keychain) -- never in plaintext config. If no
  secret service is available the token is not persisted (falls back to the prior
  re-login behaviour) rather than written insecurely. Guarded with
  wxUSE_SECRETSTORE so toolchains without it (e.g. MinGW) still compile.
- On startup, revalidate the token against the accounts/current endpoint and
  re-fetch the profile from the server, restoring the session; clear the stored
  token on failure or explicit logout. No user data is written to disk.

Addresses Snapmaker#116, Snapmaker#226. Builds on the approach proposed in Snapmaker#266, hardened to keep
the token out of plaintext on disk.
The persisted session is revalidated asynchronously at startup. That can
complete before the Flutter home page subscribes to login-state updates, in
which case the one-shot user_login_notify() push reaches an empty subscriber
list and is lost -- leaving the UI showing 'logged out' despite a valid,
restored token.

Make sw_SubscribeUserLoginState push the current login state immediately when
the user is already logged in at subscribe time. This closes the race in both
orderings: restore-before-subscribe is covered by the immediate push here, and
restore-after-subscribe by the existing notify() path.
@superhans65

Copy link
Copy Markdown

I hope this works. It's the reason why I rarely print with my U1, choosing my X1C instead because the login every single time drives me crazy. I have to verify with my phone as well.

@respinoza
respinoza force-pushed the pr/login-persistence branch from 412fe53 to 9f99ab1 Compare August 2, 2026 08:39
…ggle

Building on Snapmaker#479, this makes the persisted session survive the ways it
previously did not:

* accounts/current reports auth failures as HTTP 200 with a non-200 body
  code (110002 authorization_missing, 110003 token_expired, 110004
  authentication_failed, per the bundled web UI's error table), so a 2xx
  status alone proves nothing. Slic3r::sm_parse_account_response reports
  what a reply contains - success envelope, token refusal, profile fields
  - and each caller applies its own policy: a restore needs a success
  envelope with a usable account id, while a fresh login proceeds unless
  the token was refused outright. Previously an invalid token produced a
  "signed in" state with an empty profile while the UI still showed
  Login/Register. The parser and the region-to-host mapping live in
  slic3r/Utils/SnapmakerAccount, free of any GUI dependency, so they can
  be covered by tests/slic3rutils.
* The stored token is cleared only on those auth codes or HTTP 401/403.
  Transient failures keep it for the next launch, and an unrecognized
  response is logged rather than passing silently.
* A login epoch guards every asynchronous completion, so a slow restore
  cannot clobber a manual login or logout that happened meanwhile, and a
  keyring write that lands late cannot resurrect a session the user
  ended. Failing to store degrades to "nothing persisted" rather than
  leaving a previous account's token behind.
* Exactly one keyring item exists per platform: Windows and macOS delete
  before saving under a fixed attribute set.
* [sm_login]/has_session records that a token really reached the store,
  so startup skips the keyring - and any unlock prompt - for users who
  never signed in. It is a hint only: a reader that finds it disagreeing
  with the store corrects it, and a marker that outlives its item merely
  costs the keyring probe that having no marker would cost anyway.
* The revalidation request has a 30s timeout and a 64 KiB response cap,
  and its callbacks are disarmed on exit through a liveness token.
* Persistence is opt-out through a "Stay signed in" preference (default
  on); turning it off also drops whatever is already stored.
* The login-state payload sent to the web UI is deduplicated into
  GUI_App::sm_login_state_json, and the subscribe-time push added by
  Snapmaker#479 is gated on m_event_id so plain subscribe calls keep their shape.
Use libsecret's asynchronous password API instead of wxSecretStore on
Linux. Inside a Flatpak this lets libsecret select its file backend
behind the Secret portal (org.freedesktop.portal.Secret): the token
lives in an encrypted per-application keyring keyed by a per-app master
secret, so the sandbox needs no org.freedesktop.secrets permission at
all and cannot read other applications' secrets. The manifest drops
that permission accordingly. Unsandboxed builds talk to the ordinary
Secret Service, and hosts with neither a portal nor a keyring degrade
to signing in each launch.

The asynchronous API avoids blocking the UI thread on a D-Bus round
trip; Keychain and Credential Manager on the wxSecretStore path are
local and stay synchronous, and both present the same callback-shaped
interface so the restore path needs no platform branching. In-flight
operations are cancelled on exit through a GCancellable.
@respinoza
respinoza force-pushed the pr/login-persistence branch from 9f99ab1 to c651048 Compare August 3, 2026 01:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants