From 3c0578848a0183b61fe7aa788eab33259cb5627e Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Mon, 18 May 2026 16:26:36 +0100 Subject: [PATCH 1/6] Add IConfidentialClientApplicationProvider and CachePartitionKey support IConfidentialClientApplicationProvider: new public interface that exposes the managed CCA instance for a given authentication scheme. Enables extensions to call MSAL directly with custom parameters (e.g. cache partition keys) while reusing IdWeb's CCA lifecycle and configuration. TokenAcquisitionOptions.CachePartitionKey: new optional field that threads through to MSAL's WithCachePartitionKey on AcquireTokenSilent calls. Enables partition-aware silent token lookup for downstream API calls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../IConfidentialClientApplicationProvider.cs | 27 ++++++++++ .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 12 +++++ .../PublicAPI/net462/PublicAPI.Unshipped.txt | 12 +++++ .../PublicAPI/net472/PublicAPI.Unshipped.txt | 12 +++++ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 12 +++++ .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 12 +++++ .../netstandard2.0/PublicAPI.Unshipped.txt | 12 +++++ .../ServiceCollectionExtensions.cs | 2 + .../TokenAcquisition.cs | 21 +++++++- .../TokenAcquisitionOptions.cs | 13 ++++- .../ServiceCollectionExtensionsTests.cs | 50 +++++++++++++++++-- .../TokenAcquisitionTests.cs | 35 +++++++++++++ 12 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/IConfidentialClientApplicationProvider.cs create mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt create mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt create mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt create mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt create mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt create mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/IConfidentialClientApplicationProvider.cs b/src/Microsoft.Identity.Web.TokenAcquisition/IConfidentialClientApplicationProvider.cs new file mode 100644 index 000000000..1eb695aee --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/IConfidentialClientApplicationProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Identity.Client; + +namespace Microsoft.Identity.Web +{ + /// + /// Provides access to the managed by + /// Microsoft.Identity.Web for a given authentication scheme. Use this when you need + /// to call MSAL directly (e.g., for custom token acquisition flows) while reusing + /// the same application instance, credentials, and token cache that IdWeb manages. + /// + public interface IConfidentialClientApplicationProvider + { + /// + /// Gets the for the specified authentication scheme. + /// + /// + /// The authentication scheme name. If null, the effective default scheme is used. + /// + /// The confidential client application instance. + Task GetConfidentialClientApplicationAsync( + string? authenticationScheme = null); + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..4a38603b5 --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -0,0 +1,12 @@ +#nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void +Microsoft.Identity.Web.IConfidentialClientApplicationProvider +Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..4a38603b5 --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -0,0 +1,12 @@ +#nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void +Microsoft.Identity.Web.IConfidentialClientApplicationProvider +Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..4a38603b5 --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -0,0 +1,12 @@ +#nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void +Microsoft.Identity.Web.IConfidentialClientApplicationProvider +Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..4a38603b5 --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,12 @@ +#nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void +Microsoft.Identity.Web.IConfidentialClientApplicationProvider +Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..4a38603b5 --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -0,0 +1,12 @@ +#nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void +Microsoft.Identity.Web.IConfidentialClientApplicationProvider +Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..4a38603b5 --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,12 @@ +#nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void +Microsoft.Identity.Web.IConfidentialClientApplicationProvider +Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs index 79c22d8bf..1e6c36b02 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs @@ -137,6 +137,7 @@ public static IServiceCollection AddTokenAcquisition( } #endif services.AddSingleton(s => (ITokenAcquisitionInternal)s.GetRequiredService()); + services.AddSingleton(s => (IConfidentialClientApplicationProvider)s.GetRequiredService()); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); @@ -171,6 +172,7 @@ public static IServiceCollection AddTokenAcquisition( } #endif services.AddScoped(s => (ITokenAcquisitionInternal)s.GetRequiredService()); + services.AddScoped(s => (IConfidentialClientApplicationProvider)s.GetRequiredService()); services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(); diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index b4fdda8c4..70c882652 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -38,9 +38,9 @@ namespace Microsoft.Identity.Web * Treat as a public member. */ #if NETSTANDARD2_0 || NET462 || NET472 - internal partial class TokenAcquisition : ITokenAcquisitionInternal + internal partial class TokenAcquisition : ITokenAcquisitionInternal, IConfidentialClientApplicationProvider #else - internal partial class TokenAcquisition + internal partial class TokenAcquisition : IConfidentialClientApplicationProvider #endif { #if NETSTANDARD2_0 || NET462 || NET472 @@ -1039,6 +1039,16 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti } #pragma warning disable RS0051 // Add internal types and members to the declared API + + /// + public async Task GetConfidentialClientApplicationAsync( + string? authenticationScheme = null) + { + MergedOptions mergedOptions = _tokenAcquisitionHost.GetOptions(authenticationScheme, out _); + return await GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding: false) + .ConfigureAwait(false); + } + internal /* for testing */ async Task GetOrBuildConfidentialClientApplicationAsync( MergedOptions mergedOptions, bool isTokenBinding) @@ -1562,6 +1572,13 @@ private Task GetAuthenticationResultForWebAppWithAccountFr { builder.WithProofOfPossession(tokenAcquisitionOptions.PoPConfiguration); } + if (tokenAcquisitionOptions.CachePartitionKey != null) + { + foreach (var kvp in tokenAcquisitionOptions.CachePartitionKey) + { + builder.WithCachePartitionKey(kvp.Key, kvp.Value); + } + } } // Acquire an access token as a B2C authority diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs index 54792b04a..d84e314b1 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; using System.Threading; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client.AppConfig; @@ -25,6 +26,13 @@ public class TokenAcquisitionOptions : AcquireTokenOptions /// public CancellationToken CancellationToken { get; set; } = CancellationToken.None; + /// + /// Optional cache partition key-value pairs. When set, the token cache lookup + /// and storage will include these components, isolating cached tokens from + /// entries that have different (or no) partition keys. + /// + public IDictionary? CachePartitionKey { get; set; } + /// /// Clone the options (to be able to override them). /// @@ -46,7 +54,10 @@ public class TokenAcquisitionOptions : AcquireTokenOptions CancellationToken = CancellationToken, LongRunningWebApiSessionKey = LongRunningWebApiSessionKey, ManagedIdentity = ManagedIdentity, - FmiPath = FmiPath + FmiPath = FmiPath, + CachePartitionKey = CachePartitionKey != null + ? new Dictionary(CachePartitionKey) + : null }; } } diff --git a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs index b875facdb..d8c1a7aa9 100644 --- a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs @@ -12,6 +12,7 @@ using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client; using Microsoft.Identity.Web.Test.Common; +using Microsoft.Identity.Web.TokenCacheProviders.InMemory; using Xunit; namespace Microsoft.Identity.Web.Test @@ -25,7 +26,10 @@ public void AddTokenAcquisition_Sdk_AddsWithCorrectLifetime() var services = new ServiceCollection(); services.AddTokenAcquisition(); - ServiceDescriptor[] orderedServices = services.OrderBy(s => s.ServiceType.FullName).ToArray(); + ServiceDescriptor[] orderedServices = services + .Where(s => s.ServiceType != typeof(IConfidentialClientApplicationProvider)) + .OrderBy(s => s.ServiceType.FullName) + .ToArray(); Assert.Collection( orderedServices, @@ -143,7 +147,7 @@ public void AddTokenAcquisition_Sdk_SupportsKeyedServices() services.AddTokenAcquisition(); // Verify the number of services added by AddTokenAcquisition (ignoring the service we added here). - Assert.Equal(13, services.Count(t => t.ServiceType != typeof(ServiceCollectionExtensionsTests))); + Assert.Equal(14, services.Count(t => t.ServiceType != typeof(ServiceCollectionExtensionsTests))); } #endif @@ -161,6 +165,43 @@ public void AddTokenAcquisition_AbleToOverrideICredentialsLoader() Assert.Single(orderedServices, s => s.ServiceType == typeof(ICredentialsLoader)); } + [Fact] + public void AddTokenAcquisition_RegistersIConfidentialClientApplicationProvider() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddTokenAcquisition(); + + // Assert + ServiceDescriptor serviceDescriptor = Assert.Single(services, s => s.ServiceType == typeof(IConfidentialClientApplicationProvider)); + Assert.Equal(ServiceLifetime.Scoped, serviceDescriptor.Lifetime); + Assert.Null(serviceDescriptor.ImplementationType); + Assert.Null(serviceDescriptor.ImplementationInstance); + Assert.NotNull(serviceDescriptor.ImplementationFactory); + } + + [Fact] + public void AddTokenAcquisition_ResolvesIConfidentialClientApplicationProviderToTokenAcquisition() + { + // Arrange + var services = new ServiceCollection(); + services.AddTokenAcquisition(); + services.AddInMemoryTokenCaches(); + services.AddHttpClient(); + using ServiceProvider serviceProvider = services.BuildServiceProvider(); + using IServiceScope serviceScope = serviceProvider.CreateScope(); + + // Act + var tokenAcquisition = serviceScope.ServiceProvider.GetRequiredService(); + var confidentialClientApplicationProvider = serviceScope.ServiceProvider.GetRequiredService(); + + // Assert + Assert.Same(tokenAcquisition, confidentialClientApplicationProvider); + Assert.IsAssignableFrom(confidentialClientApplicationProvider); + } + [Fact] public void AddHttpContextAccessor_ThrowsWithoutServices() { @@ -181,7 +222,10 @@ public void AddTokenAcquisitionCalledTwice_RegistersTokenAcquisitionOnlyAsSingle services.AddTokenAcquisition(isTokenAcquisitionSingleton: true); // Assert - var orderedServices = services.OrderBy(s => s.ServiceType.FullName).ToList(); + var orderedServices = services + .Where(s => s.ServiceType != typeof(IConfidentialClientApplicationProvider)) + .OrderBy(s => s.ServiceType.FullName) + .ToList(); // Check that the first service is registered as singleton Assert.Collection( diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs index 65e4ee2d1..8e5a20d32 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs @@ -24,6 +24,41 @@ public class TokenAcquisitionTests private const string TenantId = "tenant-id"; private const string AppHomeTenantId = "app-home-tenant-id"; + [Fact] + public void CachePartitionKey_DefaultsToNull() + { + // Arrange + var options = new TokenAcquisitionOptions(); + + // Act + IDictionary? cachePartitionKey = options.CachePartitionKey; + + // Assert + Assert.Null(cachePartitionKey); + } + + [Fact] + public void CachePartitionKey_CanBeSet() + { + // Arrange + IDictionary cachePartitionKey = new Dictionary + { + ["tenant"] = "contoso", + ["user"] = "alice" + }; + + // Act + var options = new TokenAcquisitionOptions + { + CachePartitionKey = cachePartitionKey + }; + + // Assert + Assert.Same(cachePartitionKey, options.CachePartitionKey); + Assert.Equal("contoso", options.CachePartitionKey!["tenant"]); + Assert.Equal("alice", options.CachePartitionKey["user"]); + } + [Theory] [InlineData(null, null, null, null)] [InlineData(null, null, AppHomeTenantId, null)] From 60c2f1e1a448fdaef702ac32996fa55a0f1da480 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 20 May 2026 10:46:13 +0100 Subject: [PATCH 2/6] Bump MSAL to 4.84.1 and fix PublicAPI entries Update MicrosoftIdentityClientVersion to 4.84.1 which includes WithReservedScopes and WithCachePartitionKey APIs. Add PublicAPI.Unshipped.txt entries for IConfidentialClientApplicationProvider and TokenAcquisitionOptions.CachePartitionKey to NetCore and NetFramework folders (matching the existing project convention). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Build.props | 2 +- .../PublicAPI/NetCore/PublicAPI.Unshipped.txt | 4 ++++ .../PublicAPI/NetFramework/PublicAPI.Unshipped.txt | 4 ++++ .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 12 ------------ .../PublicAPI/net462/PublicAPI.Unshipped.txt | 12 ------------ .../PublicAPI/net472/PublicAPI.Unshipped.txt | 12 ------------ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 12 ------------ .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 12 ------------ .../PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | 12 ------------ 9 files changed, 9 insertions(+), 73 deletions(-) delete mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt delete mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt delete mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt delete mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt delete mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt delete mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt diff --git a/Directory.Build.props b/Directory.Build.props index 816fcbd05..f88d98466 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -80,7 +80,7 @@ 8.18.0 - 4.84.0 + 4.84.1 12.0.0 3.3.0 4.7.2 diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt index 7dc5c5811..4683d6151 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ #nullable enable +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void +Microsoft.Identity.Web.IConfidentialClientApplicationProvider +Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt index 7dc5c5811..4683d6151 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ #nullable enable +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void +Microsoft.Identity.Web.IConfidentialClientApplicationProvider +Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 4a38603b5..000000000 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void -Microsoft.Identity.Web.ICredentialsProvider -Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void -Microsoft.Identity.Web.IConfidentialClientApplicationProvider -Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt deleted file mode 100644 index 4a38603b5..000000000 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void -Microsoft.Identity.Web.ICredentialsProvider -Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void -Microsoft.Identity.Web.IConfidentialClientApplicationProvider -Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt deleted file mode 100644 index 4a38603b5..000000000 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void -Microsoft.Identity.Web.ICredentialsProvider -Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void -Microsoft.Identity.Web.IConfidentialClientApplicationProvider -Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 4a38603b5..000000000 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void -Microsoft.Identity.Web.ICredentialsProvider -Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void -Microsoft.Identity.Web.IConfidentialClientApplicationProvider -Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 4a38603b5..000000000 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void -Microsoft.Identity.Web.ICredentialsProvider -Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void -Microsoft.Identity.Web.IConfidentialClientApplicationProvider -Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index 4a38603b5..000000000 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? -Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void -Microsoft.Identity.Web.ICredentialsProvider -Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void -Microsoft.Identity.Web.IConfidentialClientApplicationProvider -Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! From 0c9a6f69bb3309db621ecad988cf3be3d09e715d Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 20 May 2026 11:09:19 +0100 Subject: [PATCH 3/6] Include IConfidentialClientApplicationProvider in DI assertion tests Add the new interface to Assert.Collection in the correct sorted position (after IMsalHttpClientFactory) instead of filtering it out. Update service count to 14. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ServiceCollectionExtensionsTests.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs index d8c1a7aa9..155b3114d 100644 --- a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs @@ -27,7 +27,6 @@ public void AddTokenAcquisition_Sdk_AddsWithCorrectLifetime() services.AddTokenAcquisition(); ServiceDescriptor[] orderedServices = services - .Where(s => s.ServiceType != typeof(IConfidentialClientApplicationProvider)) .OrderBy(s => s.ServiceType.FullName) .ToArray(); @@ -96,6 +95,14 @@ public void AddTokenAcquisition_Sdk_AddsWithCorrectLifetime() Assert.Null(actual.ImplementationFactory); }, actual => + { + Assert.Equal(ServiceLifetime.Scoped, actual.Lifetime); + Assert.Equal(typeof(IConfidentialClientApplicationProvider), actual.ServiceType); + Assert.Null(actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.NotNull(actual.ImplementationFactory); + }, + actual => { Assert.Equal(typeof(ICredentialsProvider), actual.ServiceType); Assert.Equal(typeof(CredentialsProvider), actual.ImplementationType); @@ -223,7 +230,6 @@ public void AddTokenAcquisitionCalledTwice_RegistersTokenAcquisitionOnlyAsSingle // Assert var orderedServices = services - .Where(s => s.ServiceType != typeof(IConfidentialClientApplicationProvider)) .OrderBy(s => s.ServiceType.FullName) .ToList(); @@ -292,6 +298,14 @@ public void AddTokenAcquisitionCalledTwice_RegistersTokenAcquisitionOnlyAsSingle Assert.Null(actual.ImplementationFactory); }, actual => + { + Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); + Assert.Equal(typeof(IConfidentialClientApplicationProvider), actual.ServiceType); + Assert.Null(actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.NotNull(actual.ImplementationFactory); + }, + actual => { Assert.Equal(typeof(ICredentialsProvider), actual.ServiceType); Assert.Equal(typeof(CredentialsProvider), actual.ImplementationType); From a97458648492aae10c38ea98ebd53f2aedef3b96 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 20 May 2026 11:33:17 +0100 Subject: [PATCH 4/6] Address Copilot review: DI registration check and Clone comparer - Include IConfidentialClientApplicationProvider in the existing-registration check and lifetime-mismatch removal in ServiceCollectionExtensions, matching the pattern used for ITokenAcquisitionInternal and ICredentialsProvider. - Preserve dictionary comparer in TokenAcquisitionOptions.Clone() when CachePartitionKey is a Dictionary with a custom comparer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ServiceCollectionExtensions.cs | 6 ++++++ .../TokenAcquisitionOptions.cs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs index 1e6c36b02..14fbbfabf 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs @@ -71,6 +71,7 @@ public static IServiceCollection AddTokenAcquisition( ServiceDescriptor? tokenAcquisitionService = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquisition)); ServiceDescriptor? tokenAcquisitionInternalService = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquisitionInternal)); + ServiceDescriptor? ccaProviderService = services.FirstOrDefault(s => s.ServiceType == typeof(IConfidentialClientApplicationProvider)); ServiceDescriptor? tokenAcquisitionhost = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquisitionHost)); ServiceDescriptor? authenticationHeaderCreator = services.FirstOrDefault(s => s.ServiceType == typeof(IAuthorizationHeaderProvider)); ServiceDescriptor? tokenAcquirerFactory = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquirerFactory)); @@ -89,6 +90,11 @@ public static IServiceCollection AddTokenAcquisition( services.Remove(authenticationHeaderCreator); services.Remove(authSchemeInfoProvider); + if (ccaProviderService != null) + { + services.Remove(ccaProviderService); + } + if (credentialsProviderService != null) { services.Remove(credentialsProviderService); diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs index d84e314b1..025d7183a 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs @@ -56,7 +56,9 @@ public class TokenAcquisitionOptions : AcquireTokenOptions ManagedIdentity = ManagedIdentity, FmiPath = FmiPath, CachePartitionKey = CachePartitionKey != null - ? new Dictionary(CachePartitionKey) + ? (CachePartitionKey is Dictionary dict + ? new Dictionary(dict, dict.Comparer) + : new Dictionary(CachePartitionKey)) : null }; } From eb8a8280391b0ea5644ea22a5545a410e2dd1611 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 20 May 2026 11:33:41 +0100 Subject: [PATCH 5/6] Add comment noting CachePartitionKey is not applied in ROPC silent path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 70c882652..ee9fb41ba 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -443,6 +443,8 @@ private async Task GetAuthenticationResultForUserInternalA var account = await application.GetAccountAsync(user.GetMsalAccountId()).ConfigureAwait(false); // Silent flow + // Note: CachePartitionKey from TokenAcquisitionOptions is not applied here. + // This path is used by ROPC, which does not support TokenAcquisitionOptions. return await application.AcquireTokenSilent( scopes.Except(_scopesRequestedByMsal), account) From b0f0e1096fe07dbd551d44d1bbc9d8a1c64d3f6d Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 20 May 2026 22:18:01 +0100 Subject: [PATCH 6/6] Address Bogdan's review feedback - Move IConfidentialClientApplicationProvider to Microsoft.Identity.Web.Extensibility namespace - Make CachePartitionKeys internal, expose via extension method in Extensibility namespace - Rename CachePartitionKey to CachePartitionKeys - Simplify Clone (drop comparer preservation) - Remove unnecessary #pragma if applicable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TokenAcquisitionOptionsExtensions.cs | 29 +++++++++++++++++++ .../IConfidentialClientApplicationProvider.cs | 2 +- .../PublicAPI/NetCore/PublicAPI.Unshipped.txt | 8 ++--- .../NetFramework/PublicAPI.Unshipped.txt | 8 ++--- .../ServiceCollectionExtensions.cs | 1 + .../TokenAcquisition.cs | 9 +++--- .../TokenAcquisitionOptions.cs | 13 ++------- .../ServiceCollectionExtensionsTests.cs | 1 + .../TokenAcquisitionTests.cs | 23 +++++++-------- 9 files changed, 58 insertions(+), 36 deletions(-) create mode 100644 src/Microsoft.Identity.Web.TokenAcquisition/Extensibility/TokenAcquisitionOptionsExtensions.cs diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/Extensibility/TokenAcquisitionOptionsExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/Extensibility/TokenAcquisitionOptionsExtensions.cs new file mode 100644 index 000000000..be8f9398e --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/Extensibility/TokenAcquisitionOptionsExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.Identity.Web.Extensibility +{ + /// + /// Extension methods for in extensibility scenarios. + /// + public static class TokenAcquisitionOptionsExtensions + { + /// + /// Sets cache partition key-value pairs on the options. When set, the token cache + /// lookup and storage will include these components, isolating cached tokens from + /// entries that have different (or no) partition keys. + /// + /// The token acquisition options. + /// The partition key-value pairs. + /// The options instance for chaining. + public static TokenAcquisitionOptions WithCachePartitionKeys( + this TokenAcquisitionOptions options, + IDictionary partitionKeys) + { + options.CachePartitionKeys = partitionKeys; + return options; + } + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/IConfidentialClientApplicationProvider.cs b/src/Microsoft.Identity.Web.TokenAcquisition/IConfidentialClientApplicationProvider.cs index 1eb695aee..b33de9622 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/IConfidentialClientApplicationProvider.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/IConfidentialClientApplicationProvider.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client; -namespace Microsoft.Identity.Web +namespace Microsoft.Identity.Web.Extensibility { /// /// Provides access to the managed by diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt index 4683d6151..5824eb06f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetCore/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void -Microsoft.Identity.Web.IConfidentialClientApplicationProvider -Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider +Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions +static Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions.WithCachePartitionKeys(this Microsoft.Identity.Web.TokenAcquisitionOptions! options, System.Collections.Generic.IDictionary! partitionKeys) -> Microsoft.Identity.Web.TokenAcquisitionOptions! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt index 4683d6151..5824eb06f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/NetFramework/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.TokenAcquisitionOptions.CachePartitionKey.set -> void -Microsoft.Identity.Web.IConfidentialClientApplicationProvider -Microsoft.Identity.Web.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider +Microsoft.Identity.Web.Extensibility.IConfidentialClientApplicationProvider.GetConfidentialClientApplicationAsync(string? authenticationScheme = null) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions +static Microsoft.Identity.Web.Extensibility.TokenAcquisitionOptionsExtensions.WithCachePartitionKeys(this Microsoft.Identity.Web.TokenAcquisitionOptions! options, System.Collections.Generic.IDictionary! partitionKeys) -> Microsoft.Identity.Web.TokenAcquisitionOptions! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs index 14fbbfabf..4fd3ff838 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Extensibility; using Microsoft.Identity.Web.Hosts; namespace Microsoft.Identity.Web diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index ee9fb41ba..eff4fb715 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -21,6 +21,7 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Web.Experimental; +using Microsoft.Identity.Web.Extensibility; using Microsoft.Identity.Web.TestOnly; using Microsoft.Identity.Web.TokenCacheProviders; using Microsoft.Identity.Web.TokenCacheProviders.InMemory; @@ -443,7 +444,7 @@ private async Task GetAuthenticationResultForUserInternalA var account = await application.GetAccountAsync(user.GetMsalAccountId()).ConfigureAwait(false); // Silent flow - // Note: CachePartitionKey from TokenAcquisitionOptions is not applied here. + // Note: CachePartitionKeys from TokenAcquisitionOptions is not applied here. // This path is used by ROPC, which does not support TokenAcquisitionOptions. return await application.AcquireTokenSilent( scopes.Except(_scopesRequestedByMsal), @@ -1040,8 +1041,6 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti return clientClaims; } -#pragma warning disable RS0051 // Add internal types and members to the declared API - /// public async Task GetConfidentialClientApplicationAsync( string? authenticationScheme = null) @@ -1574,9 +1573,9 @@ private Task GetAuthenticationResultForWebAppWithAccountFr { builder.WithProofOfPossession(tokenAcquisitionOptions.PoPConfiguration); } - if (tokenAcquisitionOptions.CachePartitionKey != null) + if (tokenAcquisitionOptions.CachePartitionKeys != null) { - foreach (var kvp in tokenAcquisitionOptions.CachePartitionKey) + foreach (var kvp in tokenAcquisitionOptions.CachePartitionKeys) { builder.WithCachePartitionKey(kvp.Key, kvp.Value); } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs index 025d7183a..0ab4cfb36 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionOptions.cs @@ -26,12 +26,7 @@ public class TokenAcquisitionOptions : AcquireTokenOptions /// public CancellationToken CancellationToken { get; set; } = CancellationToken.None; - /// - /// Optional cache partition key-value pairs. When set, the token cache lookup - /// and storage will include these components, isolating cached tokens from - /// entries that have different (or no) partition keys. - /// - public IDictionary? CachePartitionKey { get; set; } + internal IDictionary? CachePartitionKeys { get; set; } /// /// Clone the options (to be able to override them). @@ -55,10 +50,8 @@ public class TokenAcquisitionOptions : AcquireTokenOptions LongRunningWebApiSessionKey = LongRunningWebApiSessionKey, ManagedIdentity = ManagedIdentity, FmiPath = FmiPath, - CachePartitionKey = CachePartitionKey != null - ? (CachePartitionKey is Dictionary dict - ? new Dictionary(dict, dict.Comparer) - : new Dictionary(CachePartitionKey)) + CachePartitionKeys = CachePartitionKeys != null + ? new Dictionary(CachePartitionKeys) : null }; } diff --git a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs index 155b3114d..4f4b844af 100644 --- a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Extensibility; using Microsoft.Identity.Web.Test.Common; using Microsoft.Identity.Web.TokenCacheProviders.InMemory; using Xunit; diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs index 8e5a20d32..ef66fa427 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Extensibility; using Microsoft.Identity.Web.Test.Common; using Microsoft.Identity.Web.Test.Common.Mocks; using Microsoft.Identity.Web.TestOnly; @@ -25,38 +26,36 @@ public class TokenAcquisitionTests private const string AppHomeTenantId = "app-home-tenant-id"; [Fact] - public void CachePartitionKey_DefaultsToNull() + public void CachePartitionKeys_DefaultsToNull() { // Arrange var options = new TokenAcquisitionOptions(); // Act - IDictionary? cachePartitionKey = options.CachePartitionKey; + IDictionary? cachePartitionKeys = options.CachePartitionKeys; // Assert - Assert.Null(cachePartitionKey); + Assert.Null(cachePartitionKeys); } [Fact] - public void CachePartitionKey_CanBeSet() + public void CachePartitionKeys_CanBeSet() { // Arrange - IDictionary cachePartitionKey = new Dictionary + IDictionary cachePartitionKeys = new Dictionary { ["tenant"] = "contoso", ["user"] = "alice" }; // Act - var options = new TokenAcquisitionOptions - { - CachePartitionKey = cachePartitionKey - }; + var options = new TokenAcquisitionOptions() + .WithCachePartitionKeys(cachePartitionKeys); // Assert - Assert.Same(cachePartitionKey, options.CachePartitionKey); - Assert.Equal("contoso", options.CachePartitionKey!["tenant"]); - Assert.Equal("alice", options.CachePartitionKey["user"]); + Assert.Same(cachePartitionKeys, options.CachePartitionKeys); + Assert.Equal("contoso", options.CachePartitionKeys!["tenant"]); + Assert.Equal("alice", options.CachePartitionKeys["user"]); } [Theory]