Skip to content

Saving a consumer-defined Credential subclass throws raw NotSupportedException on every platform store, but succeeds on InMemoryCredentialStore #171

Description

@matt-edmondson

What's wrong

Credential is a public abstract class. The library also exposes ICredentialFactory<T> where T : Credential, RegisterCredentialFactory and TryCreate<T>, which suggests callers can bring their own credential types.

However, polymorphic serialization is closed. Credential.cs lists exactly three [JsonDerivedType] attributes, and the options in Storage/CredentialSerialization.cs are private, with no registration hook. The README's "Credential types" recipe (README.md L91-110) only works by editing Credential.cs in this repository. A NuGet consumer cannot do that.

The result:

  • Platform stores fail. Any subclass defined outside the library fails in CredentialSerialization.Serialize, which the Windows, macOS and Linux stores all use. On Linux it goes through NativeSecretBuffer.OfCredential.
  • In-memory hides the failure. InMemoryCredentialStore keeps the object reference and never serializes, so a consumer's tests pass while production fails.
  • The wrong exception escapes. AddOrReplace lets the raw NotSupportedException out, not the documented CredentialStoreException.

Failure scenario

public sealed class CredentialWithCertificate : Credential { public string Thumbprint { get; init; } = ""; }

CredentialCache.Instance.AddOrReplace(persona, new CredentialWithCertificate { Thumbprint = "ab" });

→ System.NotSupportedException: Runtime type '...CredentialWithCertificate' is not supported by polymorphic type 'ktsu.CredentialCache.Credential'. Path: $.

The same call on new CredentialCache(new InMemoryCredentialStore()) succeeds, and TryGet returns the credential.

This was reproduced with a temporary MSTest test in CredentialCache.Test. CredentialSerialization.Serialize and NativeSecretBuffer.OfCredential both throw, while the in-memory round trip succeeds.

Suggested fix / acceptance criteria

Either:

  • Open the type set. Add a public registration point, for example CredentialSerialization.RegisterDerivedType<T>(string typeDiscriminator) where T : Credential. Back it with a DefaultJsonTypeInfoResolver modifier that appends to JsonPolymorphismOptions.DerivedTypes for Credential. Update the README recipe to use it.
  • Or close it explicitly. Make the supported set explicit, e.g. no public way to subclass, and state in the README that the recipe is for contributors.

In both cases:

  • An unregistered subclass passed to AddOrReplace fails with a CredentialStoreException (or ArgumentException) that names the type. The cache is left unchanged.
  • If the type set is opened: a subclass defined in the test assembly and registered via the new API round-trips through Serialize/Deserialize and NativeSecretBuffer.OfCredential/ReadCredential.
  • Minor: the README calls Credential a "polymorphic record class", but it is a plain class.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions