You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
InMemoryCredentialStore saves a credential with a null non-nullable property (Token = null!) that every platform store refuses, and TryGet then returns it as found #208
#204 (fixing #180) made the platform stores refuse a credential whose non-nullable property is null: CredentialSerialization.Serialize/SerializeToString now throw ArgumentException because the options set RespectNullableAnnotations = true. CredentialCache.AddOrReplace documents that exception, but only for a store that serializes credentials.
InMemoryCredentialStore.Save never serializes, so it doesn't check this. It stores the credential as given:
// CredentialCache/Storage/InMemoryCredentialStore.cspublicvoidSave(PersonaGUIDpersona,Credentialcredential){...// Refuse what a platform store could not serialize, so code tested against this store// sees the same failure it would in production.CredentialSerialization.ThrowIfUnknownType(credential);_items[persona]=credential;}
The comment says the store should refuse what a platform store can't serialize. It checks for unknown types (#206) but not for null properties, so it misses the second case #204 added.
usingCredentialCachecache=new(newInMemoryCredentialStore());PersonaGUIDp=CredentialCache.CreatePersonaGUID();cache.AddOrReplace(p,newCredentialWithToken{Token=null!});// succeeds; a platform store throws ArgumentExceptionAssert.IsTrue(cache.TryGet(p,outCredential?c));// foundAssert.IsNull(((CredentialWithToken)c!).Token);// null Token handed back
This test passes. Against WindowsCredentialStore, MacOsCredentialStore or LinuxSecretServiceCredentialStore, the same AddOrReplace throws ArgumentException. NullPropertyTests.AddOrReplaceRejectsANullNonNullablePropertyAndLeavesTheStoreAndCacheUnchanged pins that, but only through a serializing stub store.
Why it matters
InMemoryCredentialStore is the store consumers use in tests (CredentialCache.ConfigureStore(new InMemoryCredentialStore())). Say a consumer builds a credential from a config value that can be missing, e.g. new CredentialWithToken { Token = config.Token! }:
Their test suite passes: the save succeeds and TryGet returns the credential.
#171 and #206 were about this same kind of gap (one store accepting what the others refuse), for unregistered subclasses.
Suggested fix
Have InMemoryCredentialStore.Save (or CredentialCache.AddOrReplace, next to ThrowIfUnknownType) run the same check the platform stores get. The simplest way is _ = CredentialSerialization.Serialize(credential); and then zero/discard the bytes. A cheaper validation helper on CredentialSerialization that reuses the same options would also work. Either way, every store then throws the same ArgumentException.
Acceptance criteria
new CredentialCache(new InMemoryCredentialStore()).AddOrReplace(p, new CredentialWithToken { Token = null! }) throws ArgumentException, and the store and cache are left unchanged.
The same holds for CredentialWithUsernamePassword with a null Username or Password, and through InMemoryCredentialStore.Save called directly.
The NullPropertyTests cases also run against InMemoryCredentialStore.
Suggested next step: put the validation in one place so every store fails the same way. The best spot is a CredentialSerialization.Validate(credential) helper that reuses the RespectNullableAnnotations options, called from AddOrReplace before any store sees the credential. Call it from InMemoryCredentialStore.Save as well, so direct calls are covered. Then parameterize NullPropertyTests over InMemoryCredentialStore and the serializing stub, as the acceptance criteria ask. Note that releases are currently blocked by #200, so the fix will not reach NuGet until that is resolved.
Activity
matt-edmondson commentedon Nov 11, 2026
Triage
ArgumentExceptionin production. It is the same class of gap as Saving a consumer-defined Credential subclass throws raw NotSupportedException on every platform store, but succeeds on InMemoryCredentialStore #171 and Saving an unregistered Credential subclass through a platform store still throws a raw NotSupportedException, while InMemoryCredentialStore accepts it (#171 fixed only CredentialCache.AddOrReplace) #206, which only covered unregistered subclasses.CredentialCache/Storage/InMemoryCredentialStore.Save, orCredentialCache.AddOrReplacenext toThrowIfUnknownType, along withCredentialSerialization. This follows directly from Refuse a null non-nullable credential property when reading or saving [patch] #204 (the fix for A stored "Token":null or "Password":null loads as a found credential with a null non-nullable property, and the caller's first read throws NullReferenceException #180), so whoever owns that change is the natural owner here.RegisterDerivedTypeaccepting types that cannot round-trip, and Saving a credential leaves its plaintext JSON in ArrayPool<byte>.Shared: zeroing the byte[] from SerializeToUtf8Bytes misses System.Text.Json's pooled copy #185 is about pooled plaintext buffers. Both touch serialization but neither covers null properties.InMemoryCredentialStore.Suggested next step: put the validation in one place so every store fails the same way. The best spot is a
CredentialSerialization.Validate(credential)helper that reuses theRespectNullableAnnotationsoptions, called fromAddOrReplacebefore any store sees the credential. Call it fromInMemoryCredentialStore.Saveas well, so direct calls are covered. Then parameterizeNullPropertyTestsoverInMemoryCredentialStoreand the serializing stub, as the acceptance criteria ask. Note that releases are currently blocked by #200, so the fix will not reach NuGet until that is resolved.Generated by Claude Code