From b5177a41ae943c405d9b0554c2a4c237c3919335 Mon Sep 17 00:00:00 2001 From: Travis Walker Date: Fri, 13 Sep 2024 00:52:55 -0700 Subject: [PATCH] Adding api to enable additional caching parameters (#4919) * Adding api to enable additional caching parameters * clean up * Clean up, Refactoring, Updating tests * Fixing test issue * Resolving build issue * Test fix * Adding support for arrays and objects * Refactoring. Test updates --------- Co-authored-by: trwalke --- build/platform_and_feature_flags.props | 3 + .../AbstractAcquireTokenParameterBuilder.cs | 1 + .../AcquireTokenCommonParameters.cs | 2 +- .../AuthenticationResult.cs | 4 +- .../Cache/Items/MsalAccessTokenCacheItem.cs | 43 ++++- ...ntAcquireTokenParameterBuilderExtension.cs | 36 ++++ .../AuthenticationRequestParameters.cs | 2 + .../OAuth2/MsalTokenResponse.cs | 11 +- .../TokenCache.ITokenCacheInternal.cs | 3 +- .../Core/Mocks/MockHelpers.cs | 13 +- .../Core/Mocks/MockHttpManagerExtensions.cs | 20 ++ .../AuthenticationResultTests.cs | 4 - .../PublicApiTests/ExtensiblityTests.cs | 178 ++++++++++++++++++ 13 files changed, 304 insertions(+), 16 deletions(-) diff --git a/build/platform_and_feature_flags.props b/build/platform_and_feature_flags.props index b980df182e..cc0447148a 100644 --- a/build/platform_and_feature_flags.props +++ b/build/platform_and_feature_flags.props @@ -23,4 +23,7 @@ $(DefineConstants);NETSTANDARD;SUPPORTS_CONFIDENTIAL_CLIENT;SUPPORTS_BROKER;SUPPORTS_CUSTOM_CACHE;SUPPORTS_WIN32; + + $(DefineConstants);MOBILE + diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractAcquireTokenParameterBuilder.cs index dfe775678a..c6b4270170 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractAcquireTokenParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractAcquireTokenParameterBuilder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.ApiConfig.Parameters; diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs index 2e2461fb00..6e98c5f572 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs @@ -29,6 +29,6 @@ internal class AcquireTokenCommonParameters public PoPAuthenticationConfiguration PopAuthenticationConfiguration { get; set; } public Func OnBeforeTokenRequestHandler { get; internal set; } public X509Certificate2 MtlsCertificate { get; internal set; } - + public List AdditionalCacheParameters { get; set; } } } diff --git a/src/client/Microsoft.Identity.Client/AuthenticationResult.cs b/src/client/Microsoft.Identity.Client/AuthenticationResult.cs index 5f84e87a03..928117f710 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationResult.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationResult.cs @@ -166,7 +166,9 @@ internal AuthenticationResult( CorrelationId = correlationID; ApiEvent = apiEvent; AuthenticationResultMetadata = new AuthenticationResultMetadata(tokenSource); - AdditionalResponseParameters = additionalResponseParameters; + AdditionalResponseParameters = msalAccessTokenCacheItem?.PersistedCacheParameters?.Count > 0 ? + (IReadOnlyDictionary)msalAccessTokenCacheItem.PersistedCacheParameters : + additionalResponseParameters; if (msalAccessTokenCacheItem != null) { AccessToken = authenticationScheme.FormatAccessToken(msalAccessTokenCacheItem); diff --git a/src/client/Microsoft.Identity.Client/Cache/Items/MsalAccessTokenCacheItem.cs b/src/client/Microsoft.Identity.Client/Cache/Items/MsalAccessTokenCacheItem.cs index 2376a43079..596e2aa63f 100644 --- a/src/client/Microsoft.Identity.Client/Cache/Items/MsalAccessTokenCacheItem.cs +++ b/src/client/Microsoft.Identity.Client/Cache/Items/MsalAccessTokenCacheItem.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Identity.Client.AuthScheme; using Microsoft.Identity.Client.Cache.Keys; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.Utils; #if SUPPORTS_SYSTEM_TEXT_JSON +using System.Text.Json; using JObject = System.Text.Json.Nodes.JsonObject; #else using Microsoft.Identity.Json.Linq; @@ -28,7 +30,8 @@ internal MsalAccessTokenCacheItem( string tenantId, string homeAccountId, string keyId = null, - string oboCacheKey = null) + string oboCacheKey = null, + IEnumerable persistedCacheParameters = null) : this( scopes: ScopeHelper.OrderScopesAlphabetically(response.Scope), // order scopes to avoid cache duplication. This is not in the hot path. cachedAt: DateTimeOffset.UtcNow, @@ -45,11 +48,39 @@ internal MsalAccessTokenCacheItem( RawClientInfo = response.ClientInfo; HomeAccountId = homeAccountId; OboCacheKey = oboCacheKey; - +#if !MOBILE + PersistedCacheParameters = AcquireCacheParametersFromResponse(persistedCacheParameters, response.ExtensionData); +#endif InitCacheKey(); } - +#if !MOBILE + private IDictionary AcquireCacheParametersFromResponse( + IEnumerable persistedCacheParameters, +#if SUPPORTS_SYSTEM_TEXT_JSON + Dictionary extraDataFromResponse) +#else + Dictionary extraDataFromResponse) +#endif + { + if (persistedCacheParameters == null || !persistedCacheParameters.Any()) + { + return null; + } + + var cacheParameters = extraDataFromResponse + .Where(x => persistedCacheParameters.Contains(x.Key, StringComparer.InvariantCultureIgnoreCase)) +#if SUPPORTS_SYSTEM_TEXT_JSON + .ToDictionary(x => x.Key, x => x.Value.ToString()); +#else + //Avoid formatting arrays because it adds new lines after every element + .ToDictionary(x => x.Key, x => x.Value.Type == JTokenType.Array || x.Value.Type == JTokenType.Object ? + x.Value.ToString(Json.Formatting.None) : + x.Value.ToString()); +#endif + return cacheParameters; + } +#endif internal /* for test */ MsalAccessTokenCacheItem( string preferredCacheEnv, string clientId, @@ -215,6 +246,12 @@ internal string TenantId internal string CacheKey { get; private set; } + /// + /// Additional parameters that were requested in the token request and are stored in the cache. + /// These are acquired from the response and are stored in the cache for later use. + /// + internal IDictionary PersistedCacheParameters { get; private set; } + private Lazy iOSCacheKeyLazy; public IiOSKey iOSCacheKey => iOSCacheKeyLazy.Value; diff --git a/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs b/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs index 4618d04748..38550f3ee0 100644 --- a/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs +++ b/src/client/Microsoft.Identity.Client/Extensibility/AbstractConfidentialClientAcquireTokenParameterBuilderExtension.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -55,5 +57,39 @@ public static AbstractAcquireTokenParameterBuilder WithProofOfPosessionKeyId< return builder; } + +#if !MOBILE + /// + /// Specifies additional parameters acquired from authentication responses to be cached with the access token that are normally not included in the cache object. + /// these values can be read from the parameter. + /// + /// + /// The builder to chain options to + /// Additional parameters to cache + /// + public static AbstractAcquireTokenParameterBuilder WithAdditionalCacheParameters( + this AbstractAcquireTokenParameterBuilder builder, + IEnumerable cacheParameters) + where T : AbstractAcquireTokenParameterBuilder + { + if (cacheParameters != null && !cacheParameters.Any()) + { + return builder; + } + + builder.ValidateUseOfExperimentalFeature(); + + //Check if the cache parameters are already initialized, if so, add to the existing list + if (builder.CommonParameters.AdditionalCacheParameters != null) + { + builder.CommonParameters.AdditionalCacheParameters.AddRange(cacheParameters); + } + else + { + builder.CommonParameters.AdditionalCacheParameters = cacheParameters.ToList(); + } + return builder; + } +#endif } } diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs index e9c624ab8a..64b7091e29 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs @@ -115,6 +115,8 @@ public string Claims public IAuthenticationScheme AuthenticationScheme => _commonParameters.AuthenticationScheme; + public IEnumerable PersistedCacheParameters => _commonParameters.AdditionalCacheParameters; + #region TODO REMOVE FROM HERE AND USE FROM SPECIFIC REQUEST PARAMETERS // TODO: ideally, these can come from the particular request instance and not be in RequestBase since it's not valid for all requests. diff --git a/src/client/Microsoft.Identity.Client/OAuth2/MsalTokenResponse.cs b/src/client/Microsoft.Identity.Client/OAuth2/MsalTokenResponse.cs index 93f548cc95..a7453fc899 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/MsalTokenResponse.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/MsalTokenResponse.cs @@ -93,11 +93,8 @@ public IReadOnlyDictionary CreateExtensionDataStringMap() #if SUPPORTS_SYSTEM_TEXT_JSON foreach (KeyValuePair item in ExtensionData) { - if (item.Value.ValueKind == JsonValueKind.String || - item.Value.ValueKind == JsonValueKind.Number || - item.Value.ValueKind == JsonValueKind.True || - item.Value.ValueKind == JsonValueKind.False || - item.Value.ValueKind == JsonValueKind.Null) + if (item.Value.ValueKind != JsonValueKind.Undefined || + item.Value.ValueKind != JsonValueKind.Null) { stringExtensionData.Add(item.Key, item.Value.ToString()); } @@ -117,6 +114,10 @@ public IReadOnlyDictionary CreateExtensionDataStringMap() { stringExtensionData.Add(item.Key, item.Value.ToString()); } + else if (item.Value.Type == JTokenType.Array || item.Value.Type == JTokenType.Object) + { + stringExtensionData.Add(item.Key, item.Value.ToString(Formatting.None)); + } } #endif return stringExtensionData; diff --git a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs index 88e0a67a6e..0b4f41cb5f 100644 --- a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs +++ b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs @@ -79,7 +79,8 @@ async Task> IToke tenantId, homeAccountId, requestParams.AuthenticationScheme.KeyId, - CacheKeyFactory.GetOboKey(requestParams.LongRunningOboCacheKey, requestParams.UserAssertion)); + CacheKeyFactory.GetOboKey(requestParams.LongRunningOboCacheKey, requestParams.UserAssertion), + requestParams.PersistedCacheParameters); } if (!string.IsNullOrEmpty(response.RefreshToken)) diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs index a42e055e84..b8b3cd8a4d 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs @@ -341,7 +341,18 @@ public static HttpResponseMessage CreateSuccessfulClientCredentialTokenResponseM string tokenType = "Bearer") { return CreateSuccessResponseMessage( - "{\"token_type\":\"" + tokenType + "\",\"expires_in\":\"" + expiry + "\",\"access_token\":\"" + token + "\"}"); + "{\"token_type\":\"" + tokenType + "\",\"expires_in\":\"" + expiry + "\",\"access_token\":\"" + token + "\",\"additional_param1\":\"value1\",\"additional_param2\":\"value2\",\"additional_param3\":\"value3\"}"); + } + + public static HttpResponseMessage CreateSuccessfulClientCredentialTokenResponseWithAdditionalParamsMessage( + string token = "header.payload.signature", + string expiry = "3599", + string tokenType = "Bearer", + string additionalparams = ",\"additional_param1\":\"value1\",\"additional_param2\":\"value2\",\"additional_param3\":\"value3\",\"additional_param4\":[\"GUID\",\"GUID2\",\"GUID3\"],\"additional_param5\":{\"value5json\":\"value5\"}" + ) + { + return CreateSuccessResponseMessage( + "{\"token_type\":\"" + tokenType + "\",\"expires_in\":\"" + expiry + "\",\"access_token\":\"" + token + "\"" + additionalparams + "}"); } public static HttpResponseMessage CreateSuccessTokenResponseMessage( diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs index 7a7bd8b109..64d33b3c80 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs @@ -197,6 +197,26 @@ public static MockHttpMessageHandler AddMockHandlerSuccessfulClientCredentialTok return handler; } + public static MockHttpMessageHandler AddMockHandlerSuccessfulClientCredentialTokenResponseWithAdditionalParamsMessage( + this MockHttpManager httpManager, + string token = "header.payload.signature", + string expiresIn = "3599", + string tokenType = "Bearer", + IList unexpectedHttpHeaders = null, + string additionalparams = ",\"additional_param1\":\"value1\",\"additional_param2\":\"value2\",\"additional_param3\":\"value3\",\"additional_param4\":[\"GUID\",\"GUID2\",\"GUID3\"],\"additional_param5\":{\"value5json\":\"value5\"}") + { + var handler = new MockHttpMessageHandler() + { + ExpectedMethod = HttpMethod.Post, + ResponseMessage = MockHelpers.CreateSuccessfulClientCredentialTokenResponseWithAdditionalParamsMessage(token, expiresIn, tokenType, additionalparams), + UnexpectedRequestHeaders = unexpectedHttpHeaders + }; + + httpManager.AddMockHandler(handler); + + return handler; + } + public static MockHttpMessageHandler AddMockHandlerForThrottledResponseMessage( this MockHttpManager httpManager) { diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationResultTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationResultTests.cs index 58c460267a..068753126b 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationResultTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationResultTests.cs @@ -180,10 +180,6 @@ public async Task MsalTokenResponseParseTestAsync() Assert.IsFalse(extMap.ContainsKey("id_token")); Assert.IsFalse(extMap.ContainsKey("client_info")); - // only scalar properties should be in the map - Assert.IsFalse(extMap.ContainsKey("object_extension")); - Assert.IsFalse(extMap.ContainsKey("array_extension")); - // all other properties should be in the map Assert.AreEqual("1209599", extMap["number_extension"]); Assert.AreEqual("True", extMap["true_extension"]); diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtensiblityTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtensiblityTests.cs index 935305e3e9..06e9cc74dc 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtensiblityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtensiblityTests.cs @@ -12,6 +12,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.Identity.Client.Extensibility; using System.Threading; +using Microsoft.Identity.Test.Common.Core.Helpers; namespace Microsoft.Identity.Test.Unit.PublicApiTests { @@ -218,6 +219,183 @@ public async Task ValidateAppTokenProviderAsync() } } + [TestMethod] + public async Task ValidateAdditionalCacheParametersAreStored() + { + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority("https://login.microsoftonline.com/tid/") + .WithClientSecret(TestConstants.ClientSecret) + .WithExperimentalFeatures(true) + .WithHttpManager(httpManager) + .BuildConcrete(); + + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseWithAdditionalParamsMessage(); + + var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) + .WithAdditionalCacheParameters(new List { "additional_param1", "additional_param2", "additional_param3", "additional_param4", "additional_param5", "additional_param5" }) + .ExecuteAsync() + .ConfigureAwait(false); + + var parameters = app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Single().PersistedCacheParameters; + Assert.IsTrue(parameters.Count == 5); + + parameters.TryGetValue("additional_param1", out string additionalParam1); + parameters.TryGetValue("additional_param2", out string additionalParam2); + parameters.TryGetValue("additional_param3", out string additionalParam3); + parameters.TryGetValue("additional_param4", out string additionalParam4); + parameters.TryGetValue("additional_param5", out string additionalParam5); + + Assert.AreEqual("value1", additionalParam1); + Assert.AreEqual("value2", additionalParam2); + Assert.AreEqual("value3", additionalParam3); + Assert.AreEqual("[\"GUID\",\"GUID2\",\"GUID3\"]", additionalParam4); + Assert.AreEqual("{\"value5json\":\"value5\"}", additionalParam5); + + Assert.AreEqual("Bearer", result.TokenType); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + //Validate that the additional parameters are reflected in the AuthenticationResult.AdditionalResponseParameters + Assert.AreEqual((IReadOnlyDictionary)parameters, result.AdditionalResponseParameters); + + //Verify cache parameters still exist + result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) + .WithAdditionalCacheParameters(new List { "additional_param1", "additional_param2", "additional_param3" }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("Bearer", result.TokenType); + Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource); + + parameters = app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Single().PersistedCacheParameters; + Assert.IsTrue(parameters.Count == 5); + + parameters.TryGetValue("additional_param1", out additionalParam1); + parameters.TryGetValue("additional_param2", out additionalParam2); + parameters.TryGetValue("additional_param3", out additionalParam3); + parameters.TryGetValue("additional_param4", out additionalParam4); + parameters.TryGetValue("additional_param5", out additionalParam5); + + Assert.AreEqual("value1", additionalParam1); + Assert.AreEqual("value2", additionalParam2); + Assert.AreEqual("value3", additionalParam3); + Assert.AreEqual("[\"GUID\",\"GUID2\",\"GUID3\"]", additionalParam4); + Assert.AreEqual("{\"value5json\":\"value5\"}", additionalParam5); + Assert.AreEqual((IReadOnlyDictionary)parameters, result.AdditionalResponseParameters); + + //Verify cache parameters still exist without using WithAdditionalCacheParameters + result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("Bearer", result.TokenType); + Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource); + + parameters = app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Single().PersistedCacheParameters; + Assert.IsTrue(parameters.Count == 5); + + parameters.TryGetValue("additional_param1", out additionalParam1); + parameters.TryGetValue("additional_param2", out additionalParam2); + parameters.TryGetValue("additional_param3", out additionalParam3); + parameters.TryGetValue("additional_param4", out additionalParam4); + parameters.TryGetValue("additional_param5", out additionalParam5); + + Assert.AreEqual("value1", additionalParam1); + Assert.AreEqual("value2", additionalParam2); + Assert.AreEqual("value3", additionalParam3); + Assert.AreEqual("[\"GUID\",\"GUID2\",\"GUID3\"]", additionalParam4); + Assert.AreEqual("{\"value5json\":\"value5\"}", additionalParam5); + Assert.AreEqual((IReadOnlyDictionary)parameters, result.AdditionalResponseParameters); + + //Verify cache parameters still exist after token expires + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseWithAdditionalParamsMessage(); + TokenCacheHelper.ExpireAllAccessTokens(app.AppTokenCacheInternal); + result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) + .WithAdditionalCacheParameters(new List { "additional_param1", "additional_param2", "additional_param3", "additional_param4", "additional_param5"}) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("Bearer", result.TokenType); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + + parameters = app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Single().PersistedCacheParameters; + Assert.IsTrue(parameters.Count == 5); + + parameters.TryGetValue("additional_param1", out additionalParam1); + parameters.TryGetValue("additional_param2", out additionalParam2); + parameters.TryGetValue("additional_param3", out additionalParam3); + parameters.TryGetValue("additional_param4", out additionalParam4); + parameters.TryGetValue("additional_param5", out additionalParam5); + + Assert.AreEqual("value1", additionalParam1); + Assert.AreEqual("value2", additionalParam2); + Assert.AreEqual("value3", additionalParam3); + Assert.AreEqual("[\"GUID\",\"GUID2\",\"GUID3\"]", additionalParam4); + Assert.AreEqual("{\"value5json\":\"value5\"}", additionalParam5); + + Assert.AreEqual((IReadOnlyDictionary)parameters, result.AdditionalResponseParameters); + + //Ensure not all cache parameters are required + app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithClientSecret(TestConstants.ClientSecret) + .WithExperimentalFeatures(true) + .WithAuthority("https://login.microsoftonline.com/tid/") + .WithHttpManager(httpManager) + .BuildConcrete(); + + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseWithAdditionalParamsMessage(); + + result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) + .WithAdditionalCacheParameters(new List { "additional_param1", "additional_param3" }) + .ExecuteAsync() + .ConfigureAwait(false); + + parameters = app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Single().PersistedCacheParameters; + Assert.IsTrue(parameters.Count == 2); + + parameters.TryGetValue("additional_param1", out additionalParam1); + parameters.TryGetValue("additional_param3", out additionalParam3); + + Assert.AreEqual("value1", additionalParam1); + Assert.AreEqual("value3", additionalParam3); + + Assert.AreEqual((IReadOnlyDictionary)parameters, result.AdditionalResponseParameters); + + //Ensure no parameters are required + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseWithAdditionalParamsMessage(); + + result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) + .WithForceRefresh(true) + .WithAdditionalCacheParameters(new List { }) + .ExecuteAsync() + .ConfigureAwait(false); + + parameters = app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Single().PersistedCacheParameters; + Assert.IsTrue(parameters == null); + + //Ensure missing cache parameters are not added + app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithClientSecret(TestConstants.ClientSecret) + .WithExperimentalFeatures(true) + .WithAuthority("https://login.microsoftonline.com/tid/") + .WithHttpManager(httpManager) + .BuildConcrete(); + + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseWithAdditionalParamsMessage(); + result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) + .WithAdditionalCacheParameters(new List { "additional_paramN" }) + .ExecuteAsync() + .ConfigureAwait(false); + + parameters = app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Single().PersistedCacheParameters; + parameters.TryGetValue("additional_param1", out string additionalParam); + Assert.IsNull(additionalParam); + Assert.IsTrue(result.AdditionalResponseParameters.Count == 5); + } + } + private AppTokenProviderResult GetAppTokenProviderResult(string differentScopesForAt = "", long? refreshIn = 1000) { var token = new AppTokenProviderResult();