Skip to content

fix: persist a credential before caching it in AddOrReplace [patch] - #146

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/nice-davinci-mkr7my
Sep 14, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/nice-davinci-mkr7my

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #145

Problem

CredentialCache.AddOrReplace wrote the credential into _credentials before calling Store.Save:

_credentials[persona] = credential;
Store.Save(persona, credential);

Every native store in this repo throws on failure — WindowsCredentialStore.Save raises CredentialStoreException when the serialized blob exceeds CRED_MAX_CREDENTIAL_BLOB_SIZE or CredWriteW fails, and MacOsCredentialStore / LinuxSecretServiceCredentialStore do the same on native API failure. When that happened the caller saw the exception and reasonably concluded nothing was saved, but a later TryGet still returned the credential from the in-memory dictionary, because TryGet short-circuits on _credentials and never consults the store once an entry exists. The process then behaved as though the credential were durably stored, and only a restart — or a second CredentialCache over the same store, the scenario the singleton/ConfigureStore API exists to support — revealed that it had never been written.

Fix

Persist first, update the cache only on success. A failed save now leaves the cache holding exactly what the store holds: nothing for a new persona, or the previously persisted credential when a replacement fails.

Tests

Two regression tests in CredentialCacheTests, backed by a new ThrowingCredentialStore fake (an InMemoryCredentialStore wrapper with a ThrowOnSave switch):

  • AddOrReplaceDoesNotCacheCredentialWhenStoreSaveThrows — after a failed save of a new persona, TryGet returns false.
  • AddOrReplaceKeepsThePersistedCredentialWhenReplacementFailsToSave — after a failed replacement, TryGet returns the credential the store actually holds, not the one whose save failed.

Both were verified to fail against the old ordering and pass with the fix. Full suite on Linux: 24 total, 19 passed, 5 skipped (the pre-existing native-store tests, which are inconclusive off a platform with a native secret service).

🤖 Generated with Claude Code

https://claude.ai/code/session_016dQa3DK7HjcyQkRaYBxumh


Generated by Claude Code

AddOrReplace wrote the credential into the in-memory dictionary before
calling Store.Save. When a native store rejected the write (an oversized
blob on Windows Credential Manager, a transient keychain or libsecret
failure), the caller saw the exception but a later TryGet still returned
the credential from memory, so the process behaved as though it had been
durably stored until a restart or a second cache instance revealed it was
never written.

Save first, then update the cache, so a failed save leaves the cache
holding exactly what the store holds.

Fixes #145

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dQa3DK7HjcyQkRaYBxumh
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 484c925 into main Sep 14, 2026
16 checks passed
@matt-edmondson
matt-edmondson deleted the claude/nice-davinci-mkr7my branch September 14, 2026 21:21
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.

AddOrReplace can leave CredentialCache's in-memory cache holding a credential that was never persisted

1 participant