Repository navigation
fix(auth): require Secret Service on Linux instead of the session keyring - #158
Conversation
doistbot
left a comment
There was a problem hiding this comment.
This PR pins Linux keyring storage to Secret Service in createSecureStore, so the silent session-scoped keyutils fallback (WSL2, headless boxes) now surfaces as SecureStoreUnavailableError and lets each CLI's credential-store policy handle it — system policies get the existing plaintext hint, fallback policies degrade to plaintext with a warning, and unavailable reads on AUTH_STORE_READ_FAILED now carry recovery hints. Tests cover the pinned constructor call, constructor-throw normalization, and the read-path hints, and the change reuses the existing error-wrapping and policy conventions cleanly. No inline issues were flagged during review; a minor optional follow-up would be extracting the shared plaintext-storage hint literal so the read and write paths stay wording-synced, but it's not blocking.
I also left one optional follow-up note in the details below.
Optional follow-up note (1)
src/auth/keyring/token-store.ts:100:
'Make the system credential manager available and retry.'already exists verbatim inrecord-write.ts'scredentialStoreUnavailableError, and this file already importsSECURE_STORE_DESCRIPTION(whose value is exactlysystem credential manager). Extract a shared hint constant/helper and compose it fromSECURE_STORE_DESCRIPTIONso the read and write paths can't drift.
…ring On Linux hosts with no Secret Service (WSL, headless boxes), @napi-rs/keyring silently fell back to the kernel keyutils keyring. That keyring is in-memory and scoped to the login session, so a write reported success while other terminals, cron jobs, and anything after a restart failed with AUTH_STORE_READ_FAILED. Pin AsyncEntry to the Secret Service store. Without one, the constructor throws, which surfaces as SecureStoreUnavailableError: the strict 'system' policy fails the write with the plaintext hint, and 'fallback' degrades to plaintext with a warning. Reads that hit an unavailable credential manager now carry hints pointing at plaintext storage, which covers credentials already stranded in a session keyring. Refs Doist/todoist-cli#577
4b10626 to
7469698
Compare
## [1.7.2](v1.7.1...v1.7.2) (2026-10-24) ### Bug Fixes * **auth:** keep plaintext credentials in plaintext across silent refresh ([#157](#157)) ([6001b63](6001b63)), closes [#155](#155) * **auth:** require Secret Service on Linux instead of the session keyring ([#158](#158)) ([f27add1](f27add1)), closes [Doist/todoist-cli#577](Doist/todoist-cli#577)
|
🎉 This PR is included in version 1.7.2 🎉 The release is available on: Your semantic-release bot 📦🚀 |
## Summary Fixes #577. Bumps `@doist/cli-core` from 1.7.0 to 1.7.2. On Linux with no Secret Service (WSL2 here), `td auth token` reported "Token stored securely" while the token actually went into the kernel keyring for the current login session, so a second terminal, a cron job or anything after a WSL restart failed with `AUTH_STORE_READ_FAILED`. cli-core now requires the Secret Service on Linux. Under `td`'s default `system` policy the write fails with the hint to use `--credential-store=plaintext`, the same guidance #424 added. Reading a token already stuck in a session keyring now reports the credential manager as unavailable, with hints, instead of claiming the entry was removed. The fix was tested end to end on Linux in Doist/cli-core#158 (container with no D-Bus, real `@napi-rs/keyring` native binary): published cli-core reproduced this issue's error from a new session, and 1.7.2 failed the write with the plaintext hint instead. 1.7.2 also keeps plaintext credentials in plaintext across silent token refresh (Doist/cli-core#155). `td` doesn't refresh tokens, so that has no effect here. 1.7.1 only bumped cli-core's internal chalk dependency. ## Test plan - [x] `npm run build`, `npm run type-check`, `npm run check`, `npm run check:skill-sync` - [x] `npm test` - [x] cli-core resolves its nested `@napi-rs/keyring` 2.1.2, which supports the Secret Service pin
## [5.8.1](v5.8.0...v5.8.1) (2026-10-24) ### Bug Fixes * **deps:** update dependency @doist/cli-core to v1.7.2 ([#618](#618)) ([7511a92](7511a92)), closes [#424](#424) [Doist/cli-core#158](Doist/cli-core#158) [Doist/cli-core#155](Doist/cli-core#155)
Summary
Refs Doist/todoist-cli#577. Stacked on #157.
On Linux with no Secret Service on the session bus (WSL2, headless boxes),
@napi-rs/keyringsilently falls back to the kernel keyutils keyring. That keyring is in-memory and scoped to the login session, sotd auth tokenreports "Token stored securely" while a second terminal, a cron job, or anything after a WSL restart fails withAUTH_STORE_READ_FAILED. The bug is in cli-core'screateSecureStore, so every CLI on cli-core has it.Changes
createSecureStorebuilds the entry with{ linux: { store: 'secret-service' } }(supported by@napi-rs/keyring2.1.2, ignored on macOS and Windows). With no Secret Service the constructor throws, and the existing wrapping turns that intoSecureStoreUnavailableError. Each credential-store policy then does what it already does:'system'(todoist-cli's default):AUTH_STORE_WRITE_FAILEDwith the existing plaintext hint, the same guidance #424 added.'fallback'(comms-cli's default): plaintext with a warning.AUTH_STORE_READ_FAILEDfor an unavailable credential manager now has hints (make it available, or log in again with plaintext storage). This covers users who upgrade with a credential already stuck in a session keyring: their reads now report the store as unavailable, where before they only worked in the session that wrote them.Why this is stacked on #157
The remedy here is to point users at plaintext storage. For CLIs that refresh tokens, that choice only lasts past the next rotation once #157 is merged.
Follow-up
Bump cli-core in todoist-cli, then close Doist/todoist-cli#577.
End-to-end testing
Tested end to end on Linux (
node:24arm64 container, no D-Bus,DBUS_SESSION_BUS_ADDRESSunset, seccomp unconfined so keyctl works as it does on WSL) with the real@napi-rs/keyring2.1.2 native binary. A small harness drovecreateKeyringTokenStorewith a JSON-fileUserRecordStore. A "new session" read ran in a fresh process that first joined a new kernel session keyring (KEYCTL_JOIN_SESSION_KEYRING), as in the issue's repro.Published cli-core 1.7.1 reproduced the bug:
'system': the write reportedsecure-storeand a same-session read worked, but a new-session read failed withAUTH_STORE_READ_FAILED: system credential manager returned no credential for the stored account.'fallback': same result. The write looked successful, so it never fell back to plaintext.With this branch:
'system': the write failed withAUTH_STORE_WRITE_FAILEDand the plaintext hint, and no record was written.'fallback': the write went to plaintext with the warning, and a new-session read returned the token.system credential manager unavailablewith the new hints, instead of reading as missing.The test ran below
td auth token, because that command needs a real API token forgetUser().Test plan
npm test,npm run type-check,npm run checksecure-storetests check the Secret Service pin and that a throwing pinned constructor surfaces asSecureStoreUnavailableErroron get, set and deletetoken-storetest checks the hints on an unavailable read