Skip to content

fix(auth): require Secret Service on Linux instead of the session keyring - #158

Merged
scottlovegrove merged 2 commits into
fix/plaintext-survives-refreshfrom
fix/linux-require-secret-service
Oct 24, 2026
Merged

scottlovegrove merged 2 commits into
fix/plaintext-survives-refreshfrom
fix/linux-require-secret-service

Conversation

@scottlovegrove

@scottlovegrove scottlovegrove commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Refs Doist/todoist-cli#577. Stacked on #157.

On Linux with no Secret Service on the session bus (WSL2, headless boxes), @napi-rs/keyring silently falls back to the kernel keyutils keyring. That keyring is in-memory and scoped to the login session, so td auth token reports "Token stored securely" while a second terminal, a cron job, or anything after a WSL restart fails with AUTH_STORE_READ_FAILED. The bug is in cli-core's createSecureStore, so every CLI on cli-core has it.

Changes

  • createSecureStore builds the entry with { linux: { store: 'secret-service' } } (supported by @napi-rs/keyring 2.1.2, ignored on macOS and Windows). With no Secret Service the constructor throws, and the existing wrapping turns that into SecureStoreUnavailableError. Each credential-store policy then does what it already does:
    • 'system' (todoist-cli's default): AUTH_STORE_WRITE_FAILED with the existing plaintext hint, the same guidance #424 added.
    • 'fallback' (comms-cli's default): plaintext with a warning.
  • AUTH_STORE_READ_FAILED for 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.
  • README notes the Linux pinning.

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:24 arm64 container, no D-Bus, DBUS_SESSION_BUS_ADDRESS unset, seccomp unconfined so keyctl works as it does on WSL) with the real @napi-rs/keyring 2.1.2 native binary. A small harness drove createKeyringTokenStore with a JSON-file UserRecordStore. 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 reported secure-store and a same-session read worked, but a new-session read failed with AUTH_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 with AUTH_STORE_WRITE_FAILED and 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.
  • Upgrade path: a credential written by 1.7.1 into the session keyring, read after upgrading to this branch, now fails as system credential manager unavailable with the new hints, instead of reading as missing.

The test ran below td auth token, because that command needs a real API token for getUser().

Test plan

  • npm test, npm run type-check, npm run check
  • secure-store tests check the Secret Service pin and that a throwing pinned constructor surfaces as SecureStoreUnavailableError on get, set and delete
  • token-store test checks the hints on an unavailable read

@scottlovegrove scottlovegrove self-assigned this Oct 24, 2026
@scottlovegrove
scottlovegrove added this pull request to stack #159 October 24, 2026 11:17

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
  • P3 src/auth/keyring/token-store.ts:100: 'Make the system credential manager available and retry.' already exists verbatim in record-write.ts's credentialStoreUnavailableError, and this file already imports SECURE_STORE_DESCRIPTION (whose value is exactly system credential manager). Extract a shared hint constant/helper and compose it from SECURE_STORE_DESCRIPTION so the read and write paths can't drift.

Share Feedback • Review Logs

…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
@scottlovegrove
scottlovegrove force-pushed the fix/linux-require-secret-service branch from 4b10626 to 7469698 Compare October 24, 2026 11:35
@scottlovegrove scottlovegrove added the 🚢 Ship PR requires passing checks before merge, but no independent human review. label Oct 24, 2026
@scottlovegrove
scottlovegrove merged commit f27add1 into main Oct 24, 2026
6 checks passed
@scottlovegrove
scottlovegrove deleted the fix/linux-require-secret-service branch October 24, 2026 11:50
doist-release-bot Bot added a commit that referenced this pull request Oct 24, 2026
## [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)
@doist-release-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.7.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

scottlovegrove added a commit to Doist/todoist-cli that referenced this pull request Oct 24, 2026
## 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
doist-release-bot Bot added a commit to Doist/todoist-cli that referenced this pull request Oct 24, 2026
## [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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released 🚢 Ship PR requires passing checks before merge, but no independent human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Without Secret Service, the "system" store lands in a per-session kernel keyring

2 participants