Skip to content

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

Open

Description

@matt-edmondson

What's wrong

#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.cs
public void Save(PersonaGUID persona, Credential credential)
{
    ...
    // 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.

Reproduction (verified at 92cb161 on net10.0)

using CredentialCache cache = new(new InMemoryCredentialStore());
PersonaGUID p = CredentialCache.CreatePersonaGUID();

cache.AddOrReplace(p, new CredentialWithToken { Token = null! }); // succeeds; a platform store throws ArgumentException
Assert.IsTrue(cache.TryGet(p, out Credential? c));                // found
Assert.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! }:

#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.

Activity

  1. matt-edmondson commented on Nov 11, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions