From 215f70388744898cbdf7bcfd4dc765100630c807 Mon Sep 17 00:00:00 2001 From: avdunn Date: Fri, 17 Oct 2025 18:41:10 -0700 Subject: [PATCH 01/10] Add new WithExtraQueryParameters API and behavior affects cache keys, deprecate existing WithExtraQueryParameters APIs --- .../AbstractAcquireTokenParameterBuilder.cs | 1 + ...aseAbstractAcquireTokenParameterBuilder.cs | 40 ++- .../AppConfig/AbstractApplicationBuilder.cs | 40 ++- .../Microsoft.Identity.Client.csproj | 1 + .../PublicApi/net462/PublicAPI.Unshipped.txt | 2 + .../PublicApi/net472/PublicAPI.Unshipped.txt | 2 + .../net8.0-android/PublicAPI.Unshipped.txt | 2 + .../net8.0-ios/PublicAPI.Unshipped.txt | 2 + .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 2 + .../netstandard2.0/PublicAPI.Unshipped.txt | 2 + .../Utils/CoreHelpers.cs | 18 ++ .../TestConstants.cs | 12 + .../AcquireTokenInteractiveBuilderTests.cs | 2 +- .../OAuthClientTests.cs | 3 +- .../ParallelRequestsTests.cs | 6 +- .../PublicApiTests/CacheKeyExtensionTests.cs | 136 +++++++++ .../ConfidentialClientApplicationTests.cs | 6 +- .../ExtraQueryParametersTests.cs | 287 ++++++++++++++++++ ...egratedWindowsAuthUsernamePasswordTests.cs | 6 +- .../TelemetryTests/HttpTelemetryTests.cs | 12 +- .../OTelInstrumentationTests.cs | 42 +-- 21 files changed, 585 insertions(+), 39 deletions(-) create mode 100644 tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractAcquireTokenParameterBuilder.cs index febea0d99e..e4343ecb78 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractAcquireTokenParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractAcquireTokenParameterBuilder.cs @@ -67,6 +67,7 @@ public T WithClaims(string claims) /// The string needs to be properly URL-encoded and ready to send as a string of segments of the form key=value separated by an ampersand character. /// /// The builder to chain .With methods. + [Obsolete("This method is deprecated. Please use the WithExtraQueryParameters(IDictionary) method instead, which provides control over which parameters are included in the cache key.", false)] public T WithExtraQueryParameters(string extraQueryParameters) { if (!string.IsNullOrWhiteSpace(extraQueryParameters)) diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs index 7871a7eafc..cfbb2435e1 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs @@ -89,10 +89,46 @@ public T WithCorrelationId(Guid correlationId) /// as a string of segments of the form key=value separated by an ampersand character. /// The parameter can be null. /// The builder to chain the .With methods. + [Obsolete("This method is deprecated. Please use the WithExtraQueryParameters(IDictionary) method instead, which provides control over which parameters are included in the cache key.", false)] public T WithExtraQueryParameters(Dictionary extraQueryParameters) { - CommonParameters.ExtraQueryParameters = extraQueryParameters ?? - new Dictionary(StringComparer.OrdinalIgnoreCase); + return WithExtraQueryParameters(CoreHelpers.ConvertToTupleParameters(extraQueryParameters)); + } + + /// + /// Sets Extra Query Parameters for the query string in the HTTP authentication request with control over which parameters are included in the cache key + /// + /// This parameter will be appended as is to the query string in the HTTP authentication request to the authority. + /// For each parameter, you can specify whether it should be included in the cache key. + /// The parameter can be null. + /// The builder to chain .With methods. + public T WithExtraQueryParameters(IDictionary extraQueryParameters) + { + if (extraQueryParameters == null) + { + CommonParameters.ExtraQueryParameters = null; + return this as T; + } + + // Add each parameter to ExtraQueryParameters and, if requested, to CacheKeyComponents + foreach (var kvp in extraQueryParameters) + { + CommonParameters.ExtraQueryParameters = CommonParameters.ExtraQueryParameters ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + + CommonParameters.ExtraQueryParameters[kvp.Key] = kvp.Value.value; + + if (kvp.Value.includeInCacheKey) + { + CommonParameters.CacheKeyComponents = CommonParameters.CacheKeyComponents ?? new SortedList>>(); + + // Capture the value in a local to avoid closure issues + string valueToCache = kvp.Value.value; + + // Add to cache key components - uses a func that returns the value as a task + CommonParameters.CacheKeyComponents[kvp.Key] = (CancellationToken _) => Task.FromResult(valueToCache); + } + } + return this as T; } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/AbstractApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/AbstractApplicationBuilder.cs index 4d7d57e2c8..2159580a9c 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/AbstractApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/AbstractApplicationBuilder.cs @@ -292,10 +292,10 @@ protected T WithOptions(ApplicationOptions applicationOptions) /// as a string of segments of the form key=value separated by an ampersand character. /// The parameter can be null. /// The builder to chain the .With methods + [Obsolete("This method is deprecated. Please use the WithExtraQueryParameters(IDictionary) method instead, which provides control over which parameters are included in the cache key.", false)] public T WithExtraQueryParameters(IDictionary extraQueryParameters) { - Config.ExtraQueryParameters = extraQueryParameters; - return this as T; + return WithExtraQueryParameters(CoreHelpers.ConvertToTupleParameters(extraQueryParameters)); } /// @@ -305,6 +305,7 @@ public T WithExtraQueryParameters(IDictionary extraQueryParamete /// The string needs to be properly URL-encoded and ready to send as a string of segments of the form key=value separated by an ampersand character. /// /// + [Obsolete("This method is deprecated. Please use the WithExtraQueryParameters(IDictionary) method instead, which provides control over which parameters are included in the cache key.", false)] public T WithExtraQueryParameters(string extraQueryParameters) { if (!string.IsNullOrWhiteSpace(extraQueryParameters)) @@ -314,6 +315,41 @@ public T WithExtraQueryParameters(string extraQueryParameters) return this as T; } + /// + /// Sets Extra Query Parameters for the query string in the HTTP authentication request with control over which parameters are included in the cache key + /// + /// This parameter will be appended as is to the query string in the HTTP authentication request to the authority. + /// For each parameter, you can specify whether it should be included in the cache key. + /// The parameter can be null. + /// The builder to chain the .With methods + public T WithExtraQueryParameters(IDictionary extraQueryParameters) + { + if (extraQueryParameters == null) + { + Config.ExtraQueryParameters = null; + return this as T; + } + + // Add each parameter to ExtraQueryParameters and, if requested, to CacheKeyComponents + foreach (var kvp in extraQueryParameters) + { + Config.ExtraQueryParameters = Config.ExtraQueryParameters ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + + Config.ExtraQueryParameters[kvp.Key] = kvp.Value.value; + + if (kvp.Value.includeInCacheKey) + { + // Initialize the cache key components if needed + Config.CacheKeyComponents = Config.CacheKeyComponents ?? new SortedList(); + + // Add to cache key components - uses a func that returns the value as a task + Config.CacheKeyComponents[kvp.Key] = kvp.Value.value; + } + } + + return this as T; + } + /// /// Microsoft Identity specific OIDC extension that allows resource challenges to be resolved without interaction. /// Allows configuration of one or more client capabilities, e.g. "llt" diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index 2fbcd27067..b129c5d75d 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -151,6 +151,7 @@ + diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index e69de29bb2..5625fc9ca0 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index e69de29bb2..5625fc9ca0 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index e69de29bb2..5625fc9ca0 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index e69de29bb2..5625fc9ca0 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index e69de29bb2..5625fc9ca0 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2..5625fc9ca0 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/Utils/CoreHelpers.cs b/src/client/Microsoft.Identity.Client/Utils/CoreHelpers.cs index fd53a2ee0c..656ebf245a 100644 --- a/src/client/Microsoft.Identity.Client/Utils/CoreHelpers.cs +++ b/src/client/Microsoft.Identity.Client/Utils/CoreHelpers.cs @@ -140,6 +140,24 @@ public static Dictionary ParseKeyValueList(string input, char de return ParseKeyValueList(input, delimiter, urlDecode, true, requestContext); } + // Helper method intended to help deprecate some WithExtraQueryParameters APIs. + // Convert from Dictionary to Dictionary, + // with all includeInCacheKey set to false by default to maintain existing behavior of those older APIs. + internal static IDictionary ConvertToTupleParameters(IDictionary parameters) + { + if (parameters == null) + { + return null; + } + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in parameters) + { + result[kvp.Key] = (kvp.Value, false); // Include all parameters in cache key by default + } + return result; + } + internal static IReadOnlyList SplitWithQuotes(string input, char delimiter) { if (string.IsNullOrWhiteSpace(input)) diff --git a/tests/Microsoft.Identity.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Test.Common/TestConstants.cs index 8a59bc1d48..1144b7ba1c 100644 --- a/tests/Microsoft.Identity.Test.Common/TestConstants.cs +++ b/tests/Microsoft.Identity.Test.Common/TestConstants.cs @@ -243,6 +243,18 @@ public static IDictionary ExtraQueryParameters }; } } + public static IDictionary ExtraQueryParametersNoAffectOnCacheKeys + { + get + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "extra", ("qp", false) }, + { "key1", ("value1%20with%20encoded%20space", false) }, + { "key2", ("value2", false) } + }; + } + } public const string MsalCCAKeyVaultUri = "https://id4skeyvault.vault.azure.net/secrets/AzureADIdentityDivisionTestAgentSecret/"; diff --git a/tests/Microsoft.Identity.Test.Unit/ApiConfigTests/AcquireTokenInteractiveBuilderTests.cs b/tests/Microsoft.Identity.Test.Unit/ApiConfigTests/AcquireTokenInteractiveBuilderTests.cs index d2ccbe677c..3d02d82daf 100644 --- a/tests/Microsoft.Identity.Test.Unit/ApiConfigTests/AcquireTokenInteractiveBuilderTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ApiConfigTests/AcquireTokenInteractiveBuilderTests.cs @@ -91,7 +91,7 @@ public async Task TestAcquireTokenInteractiveBuilderWithPromptAndExtraQueryParam { await AcquireTokenInteractiveParameterBuilder.Create(_harness.Executor, TestConstants.s_scope) .WithLoginHint(TestConstants.DisplayableId) - .WithExtraQueryParameters("domain_hint=mydomain.com") + .WithExtraQueryParameters(new Dictionary { { "domain_hint", ("mydomain.com", false) } }) .ExecuteAsync() .ConfigureAwait(false); diff --git a/tests/Microsoft.Identity.Test.Unit/OAuthClientTests.cs b/tests/Microsoft.Identity.Test.Unit/OAuthClientTests.cs index f9f48b95be..e298a3a046 100644 --- a/tests/Microsoft.Identity.Test.Unit/OAuthClientTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/OAuthClientTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -197,7 +198,7 @@ out Arg.Any()) AuthenticationResult result = await pca .AcquireTokenInteractive(TestConstants.s_scope) - .WithExtraQueryParameters("qp1=v1") + .WithExtraQueryParameters(new Dictionary{{ "qp1", ("v1", false) }}) .ExecuteAsync() .ConfigureAwait(false); // Assert that the endpoint sent to the device auth manager doesnt not have query params diff --git a/tests/Microsoft.Identity.Test.Unit/ParallelRequestsTests.cs b/tests/Microsoft.Identity.Test.Unit/ParallelRequestsTests.cs index 2c5b1b44ac..89f7b7fd16 100644 --- a/tests/Microsoft.Identity.Test.Unit/ParallelRequestsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ParallelRequestsTests.cs @@ -37,10 +37,10 @@ public override void TestInitialize() [TestMethod] public async Task ExtraQP() { - Dictionary extraQp = new() + Dictionary extraQp = new() { - { "key1", "1" }, - { "key2", "2" } + { "key1", ("1", false) }, + { "key2", ("2", false) } }; // Arrange diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/CacheKeyExtensionTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/CacheKeyExtensionTests.cs index 3591be8c3e..cb7659f732 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/CacheKeyExtensionTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/CacheKeyExtensionTests.cs @@ -370,6 +370,142 @@ public async Task CacheExtEnsureInputKeysAddedCorrectlyTestAsync() } } + [TestMethod] + public async Task CacheExt_WithExtraQueryParameters_NoConflictTestAsync() + { + using (var httpManager = new MockHttpManager()) + { + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) + .WithRedirectUri(TestConstants.RedirectUri) + .WithClientSecret(TestConstants.ClientSecret) + .WithHttpManager(httpManager) + .BuildConcrete(); + + var appCacheAccess = app.AppTokenCache.RecordAccess(); + + httpManager.AddInstanceDiscoveryMockHandler(); + + // Test 1: Use WithAdditionalCacheKeyComponents only + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "token_with_additional_components"); + var result1 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) + .ExecuteAsync(CancellationToken.None) + .ConfigureAwait(false); + + Assert.AreEqual("token_with_additional_components", result1.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(1, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count); + + // Test 2: Use tuple-based WithExtraQueryParameters only + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "token_with_tuple_params"); + var result2 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters(new Dictionary + { + { "param1", ("value1", true) }, + { "param2", ("value2", true) } + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_tuple_params", result2.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result2.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(2, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count); + + // Test 3: Use both APIs together - should create a different cache entry + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "token_with_both_apis"); + var result3 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) + .WithExtraQueryParameters(new Dictionary + { + { "param1", ("value1", true) }, + { "param2", ("value2", true) } + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_both_apis", result3.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result3.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(3, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count); + + // Test 4: Retrieve from cache using the same combination + var result4 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) + .WithExtraQueryParameters(new Dictionary + { + { "param1", ("value1", true) }, + { "param2", ("value2", true) } + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_both_apis", result4.AccessToken); + Assert.AreEqual(TokenSource.Cache, result4.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(3, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count); + + // Test 5: Test with non-cached parameters + var result5 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) + .WithExtraQueryParameters(new Dictionary + { + { "param1", ("value1", true) }, + { "param2", ("value2", true) }, + { "non_cached_param", ("some_value", false) } // This should not affect cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_both_apis", result5.AccessToken); + Assert.AreEqual(TokenSource.Cache, result5.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(3, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count); + + // Test 6: Change a parameter that is included in cache key - should get a new token + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "token_with_changed_param"); + var result6 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync1) + .WithExtraQueryParameters(new Dictionary + { + { "param1", ("different_value", true) }, // Changed value with includeInCacheKey=true + { "param2", ("value2", true) } + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_changed_param", result6.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result6.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(4, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count); + + // Test 7: Now try with includeInCacheKey=false for a parameter + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "token_with_non_cached_param"); + var result7 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync2) // Different additional components + .WithExtraQueryParameters(new Dictionary + { + { "param3", ("value3", false) } // Not included in cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_non_cached_param", result7.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result7.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(5, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count); + + // Test 8: Repeat with same config but change the non-cached parameter value + var result8 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithAdditionalCacheKeyComponents(_additionalCacheKeysAsync2) + .WithExtraQueryParameters(new Dictionary + { + { "param3", ("different_value3", false) } // Changed value but not in cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_non_cached_param", result8.AccessToken); + Assert.AreEqual(TokenSource.Cache, result8.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(5, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count); + } + } + private void BeforeCacheAccess(TokenCacheNotificationArgs args) { args.TokenCache.DeserializeMsalV3(_serializedCache); diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs index 8ccf3568df..d53a93b565 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs @@ -436,7 +436,7 @@ public async Task ClientCreds_And_AAD_LogRequestUri_OnServerError_Async() .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) .WithClientSecret(TestConstants.ClientSecret) .WithHttpManager(httpManager) - .WithExtraQueryParameters("parameter=x") + .WithExtraQueryParameters(TestConstants.ExtraQueryParametersNoAffectOnCacheKeys) .BuildConcrete(); var appCacheAccess = cca.AppTokenCache.RecordAccess(); var userCacheAccess = cca.UserTokenCache.RecordAccess(); @@ -464,7 +464,7 @@ public async Task ClientCreds_And_ADFS_LogRequestUri_OnServerError_Async() .WithAuthority(new Uri(TestConstants.OnPremiseAuthority), false) .WithClientSecret(TestConstants.ClientSecret) .WithHttpManager(httpManager) - .WithExtraQueryParameters("parameter=x") + .WithExtraQueryParameters(TestConstants.ExtraQueryParametersNoAffectOnCacheKeys) .BuildConcrete(); var appCacheAccess = cca.AppTokenCache.RecordAccess(); var userCacheAccess = cca.UserTokenCache.RecordAccess(); @@ -1271,7 +1271,7 @@ public async Task GetAuthorizationRequestUrlDuplicateParamsTestAsync() var uri = await app .GetAuthorizationRequestUrl(TestConstants.s_scope) .WithLoginHint(TestConstants.DisplayableId) - .WithExtraQueryParameters("login_hint=some@value.com") + .WithExtraQueryParameters(new Dictionary { { "login_hint", ("some@value.com", false) } }) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs new file mode 100644 index 0000000000..a295dcc07a --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs @@ -0,0 +1,287 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Identity.Test.Unit.PublicApiTests +{ + [TestClass] + public class ExtraQueryParametersTests + { + + /// + /// Tests that the older WithExtraQueryParameters methods do not affect token caching behavior. This is meant to demonstrate an + /// issue in those methods: the parameters could change the contents of a token however they wer not included in the cache keys, + /// leading to potentially invalid tokens being returned from cache when a new request contained different extra query parameters. + /// + /// This test will no longer be needed when the older APIs are removed. + /// + [TestMethod] + public async Task WithExtraQueryParameters_DeprecatedDoNotAffectTokenCaching_TestAsync() + { + using (var httpManager = new MockHttpManager()) + { + httpManager.AddInstanceDiscoveryMockHandler(); + +#pragma warning disable CS0618 // Type or member is obsolete + // Create a confidential client application with a default extra query parameter + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) + .WithClientSecret(TestConstants.ClientSecret) + .WithExtraQueryParameters("app_param=app_value") + .WithHttpManager(httpManager) + .BuildConcrete(); + + // Step 1: Make a token request with a specific extra query parameter + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "token_with_request_param"); + var result1 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters("request_param=request_value") + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_request_param", result1.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource); + + // Step 2: Make another token request with the same extra query parameter + // Should retrieve token from cache without network call + var result2 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters("request_param=request_value") + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_request_param", result2.AccessToken); + Assert.AreEqual(TokenSource.Cache, result2.AuthenticationResultMetadata.TokenSource); + + // Step 3: Make a token request with different extra query parameters + // Should find the cached token, as older WithExtraQueryParameters APIs do not affect caching + var result3 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters("different_param=different_value") + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_request_param", result3.AccessToken); + Assert.AreEqual(TokenSource.Cache, result3.AuthenticationResultMetadata.TokenSource); + + // Step 4: Make a token request with the default app-level extra query parameters + // Using authorization code flow to populate user token cache for AcquireTokenSilent test + httpManager.AddMockHandler( + new MockHttpMessageHandler + { + ExpectedMethod = HttpMethod.Post, + ResponseMessage = MockHelpers.CreateSuccessResponseMessage(MockHelpers.GetDefaultTokenResponse("token_for_silent_flow")) + }); + + var result4 = await app.AcquireTokenByAuthorizationCode( + TestConstants.s_scope, + "some-auth-code") + .WithExtraQueryParameters("param_for_silent_flow=silent_value") + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_for_silent_flow", result4.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result4.AuthenticationResultMetadata.TokenSource); + + // Get the account that was cached + var accounts = await app.GetAccountsAsync().ConfigureAwait(false); + var account = accounts.FirstOrDefault(); + Assert.IsNotNull(account, "An account should be present in the cache"); + + // Step 5: Test AcquireTokenSilent with the cached account + // Should retrieve token from cache without network call + var silentResult = await app.AcquireTokenSilent(TestConstants.s_scope, account) + .WithExtraQueryParameters("param_for_silent_flow=silent_value") + .ExecuteAsync() + .ConfigureAwait(false); +#pragma warning restore CS0618 // Type or member is obsolete + + Assert.AreEqual("token_for_silent_flow", silentResult.AccessToken); + Assert.AreEqual(TokenSource.Cache, silentResult.AuthenticationResultMetadata.TokenSource); + + // Verify expected cache state + Assert.AreEqual(1, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count); + Assert.AreEqual(1, app.UserTokenCacheInternal.Accessor.GetAllAccessTokens().Count); + Assert.AreEqual(1, app.UserTokenCacheInternal.Accessor.GetAllRefreshTokens().Count); + } + } + + /// + /// Tests the new tuple-based WithExtraQueryParameters method that allows control over which parameters are used in cache keys. + /// + [TestMethod] + public async Task WithExtraQueryParameters_TupleVersion_ControlsCaching_TestAsync() + { + using (var httpManager = new MockHttpManager()) + { + // Create a confidential client application with a default extra query parameter + // Using the new tuple-based API, specifying that app_param should be included in the cache key + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) + .WithClientSecret(TestConstants.ClientSecret) + .WithExtraQueryParameters(new Dictionary + { + { "app_param", ("app_value", true) } + }) + .WithHttpManager(httpManager) + .BuildConcrete(); + + // Step 1: Make a token request with a specific extra query parameter included in the cache key + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "token_with_cache_param"); + var result1 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters(new Dictionary + { + { "req_param", ("req_value", true) } // Include in cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_cache_param", result1.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource); + + // Step 2: Same parameter with includeInCacheKey=true, should use cache + var result2 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters(new Dictionary + { + { "req_param", ("req_value", true) } // Same as before, should hit cache + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_cache_param", result2.AccessToken); + Assert.AreEqual(TokenSource.Cache, result2.AuthenticationResultMetadata.TokenSource); + + // Step 3: Using the same parameter but NOT including it in the cache key + // This will create a different cache key (without this parameter) and won't match previous entries + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "token_without_cache_param"); + var result3 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters(new Dictionary + { + { "req_param", ("req_value", false) } // NOT included in cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + // Should get a new token since cache key is different (doesn't include req_param) + Assert.AreEqual("token_without_cache_param", result3.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result3.AuthenticationResultMetadata.TokenSource); + + // Step 4: Reusing the same configuration as Step 3 (parameter not in cache key) + // Should now use the cache since we've stored a token with this cache key + var result4 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters(new Dictionary + { + { "req_param", ("req_value", false) } // NOT included in cache key, same as Step 3 + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_without_cache_param", result4.AccessToken); + Assert.AreEqual(TokenSource.Cache, result4.AuthenticationResultMetadata.TokenSource); + + // Step 5: Using a different value but still not including in cache key + // Should still hit the same cache entry because the cache key is the same + var result5 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters(new Dictionary + { + { "req_param", ("different_value", false) } // Different value but not in cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_without_cache_param", result5.AccessToken); + Assert.AreEqual(TokenSource.Cache, result5.AuthenticationResultMetadata.TokenSource); + + // Step 6: Multiple parameters with different cache inclusion settings + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "token_with_mixed_params"); + var result6 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters(new Dictionary + { + { "cache_param", ("cache_value", true) }, // Include in cache key + { "non_cache_param", ("value1", false) } // Don't include in cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_mixed_params", result6.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result6.AuthenticationResultMetadata.TokenSource); + + // Step 7: Same cache key parameter but different non-cache parameter, should use cache + var result7 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters(new Dictionary + { + { "cache_param", ("cache_value", true) }, // Same cached parameter + { "non_cache_param", ("value2", false) } // Different value, not in cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_mixed_params", result7.AccessToken); + Assert.AreEqual(TokenSource.Cache, result7.AuthenticationResultMetadata.TokenSource); + + // Step 8: Different cache key parameter, should make a new request + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "token_with_different_cache_param"); + var result8 = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithExtraQueryParameters(new Dictionary + { + { "cache_param", ("different_value", true) }, // Different value, included in cache key + { "non_cache_param", ("value1", false) } // Same as before, not in cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_with_different_cache_param", result8.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result8.AuthenticationResultMetadata.TokenSource); + + // Step 9: Use AcquireTokenByAuthorizationCode with tuple-based query parameters + httpManager.AddMockHandler( + new MockHttpMessageHandler + { + ExpectedMethod = HttpMethod.Post, + ResponseMessage = MockHelpers.CreateSuccessResponseMessage(MockHelpers.GetDefaultTokenResponse("token_from_auth_code")) + }); + + var result9 = await app.AcquireTokenByAuthorizationCode(TestConstants.s_scope, "some-auth-code") + .WithExtraQueryParameters(new Dictionary + { + { "auth_code_param", ("auth_code_value", true) }, // Include in cache key + { "transient_param", ("transient_value", false) } // Don't include in cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("token_from_auth_code", result9.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result9.AuthenticationResultMetadata.TokenSource); + + // Get the account that was cached + var accounts = await app.GetAccountsAsync().ConfigureAwait(false); + var account = accounts.FirstOrDefault(); + Assert.IsNotNull(account, "An account should be present in the cache after auth code flow"); + + // Step 10: Test AcquireTokenSilent with the same cache key parameters but different non-cache parameters + var silentResult1 = await app.AcquireTokenSilent(TestConstants.s_scope, account) + .WithExtraQueryParameters(new Dictionary + { + { "auth_code_param", ("auth_code_value", true) }, // Same as auth code flow + { "transient_param", ("different_value", false) } // Different value, not in cache key + }) + .ExecuteAsync() + .ConfigureAwait(false); + + // Should get token from cache since cache key parameters match + Assert.AreEqual("token_from_auth_code", silentResult1.AccessToken); + Assert.AreEqual(TokenSource.Cache, silentResult1.AuthenticationResultMetadata.TokenSource); + + // Verify final cache state + Assert.AreEqual(4, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count, "Should have 4 app tokens in cache"); + Assert.AreEqual(1, app.UserTokenCacheInternal.Accessor.GetAllAccessTokens().Count, "Should have 2 user tokens in cache"); + Assert.AreEqual(1, app.UserTokenCacheInternal.Accessor.GetAllRefreshTokens().Count, "Should have 1 refresh token in cache"); + } + } + } +} diff --git a/tests/Microsoft.Identity.Test.Unit/RequestsTests/IntegratedWindowsAuthUsernamePasswordTests.cs b/tests/Microsoft.Identity.Test.Unit/RequestsTests/IntegratedWindowsAuthUsernamePasswordTests.cs index c2945a87b0..b292b48c26 100644 --- a/tests/Microsoft.Identity.Test.Unit/RequestsTests/IntegratedWindowsAuthUsernamePasswordTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/RequestsTests/IntegratedWindowsAuthUsernamePasswordTests.cs @@ -301,7 +301,7 @@ public async Task AcquireTokenByIntegratedWindowsAuth3rdPartyIDPTestAsync(string PublicClientApplication app = PublicClientApplicationBuilder.Create(TestConstants.ClientId) .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) .WithHttpManager(httpManager) - .WithExtraQueryParameters(TestConstants.ExtraQueryParameters) + .WithExtraQueryParameters(TestConstants.ExtraQueryParametersNoAffectOnCacheKeys) .BuildConcrete(); AuthenticationResult result = await app @@ -345,7 +345,7 @@ public async Task AcquireTokenByIntegratedWindowsAuthMetadataTestAsync() PublicClientApplication app = PublicClientApplicationBuilder.Create(TestConstants.ClientId) .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) .WithHttpManager(httpManager) - .WithExtraQueryParameters(TestConstants.ExtraQueryParameters) + .WithExtraQueryParameters(TestConstants.ExtraQueryParametersNoAffectOnCacheKeys) .BuildConcrete(); AuthenticationResult result = await app @@ -398,7 +398,7 @@ public async Task AcquireTokenByIntegratedWindowsAuthInvalidClientTestAsync() PublicClientApplication app = PublicClientApplicationBuilder.Create(TestConstants.ClientId) .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) .WithHttpManager(httpManager) - .WithExtraQueryParameters(TestConstants.ExtraQueryParameters) + .WithExtraQueryParameters(TestConstants.ExtraQueryParametersNoAffectOnCacheKeys) .BuildConcrete(); MsalServiceException result = await AssertException.TaskThrowsAsync( diff --git a/tests/Microsoft.Identity.Test.Unit/TelemetryTests/HttpTelemetryTests.cs b/tests/Microsoft.Identity.Test.Unit/TelemetryTests/HttpTelemetryTests.cs index 299862a69b..2f8abee773 100644 --- a/tests/Microsoft.Identity.Test.Unit/TelemetryTests/HttpTelemetryTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/TelemetryTests/HttpTelemetryTests.cs @@ -375,9 +375,9 @@ public async Task CallerSdkDetailsTestAsync() var cca = CreateConfidentialClientApp(); await cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { - { "caller-sdk-id", "testApiId" }, - { "caller-sdk-ver", "testSdkVersion"} }) + .WithExtraQueryParameters(new Dictionary { + { "caller-sdk-id", ("testApiId", false) }, + { "caller-sdk-ver", ("testSdkVersion", false)} }) .ExecuteAsync().ConfigureAwait(false); AssertCurrentTelemetry( @@ -403,9 +403,9 @@ public async Task CallerSdkDetails_ConstraintsTestAsync() var cca = CreateConfidentialClientApp(); await cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { - { "caller-sdk-id", callerSdkId }, - { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(new Dictionary { + { "caller-sdk-id", (callerSdkId, false) }, + { "caller-sdk-ver", (callerSdkVersion, false) } }) .ExecuteAsync().ConfigureAwait(false); AssertCurrentTelemetry( diff --git a/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs b/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs index 2fe90904f8..600dac5bf2 100644 --- a/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -36,8 +37,13 @@ public class OTelInstrumentationTests : TestBase private const string callerSdkId = "123"; private const string callerSdkVersion = "1.1.1.1"; + private Dictionary extraQueryParams = new() + { + { "caller-sdk-id", (callerSdkId, false) }, + { "caller-sdk-ver", (callerSdkVersion, false) } + }; - [TestCleanup] +[TestCleanup] public override void TestCleanup() { s_meterProvider?.Dispose(); @@ -101,7 +107,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_ClientCredential_Async() _harness.HttpManager.AddAllMocks(TokenResponseType.Valid_ClientCredentials); AuthenticationResult result = await _cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync() .ConfigureAwait(false); // Assert @@ -116,7 +122,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_ClientCredential_Async() // Act Trace.WriteLine("4. ATS - should perform an RT refresh"); result = await _cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync() .ConfigureAwait(false); @@ -132,7 +138,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_ClientCredential_Async() Trace.WriteLine("5. ATS - should not perform an RT refresh, as the token is still valid"); result = await _cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync() .ConfigureAwait(false); @@ -174,7 +180,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_MSI_Async() ManagedIdentitySource.AppService); AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(resource) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync() .ConfigureAwait(false); @@ -191,7 +197,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_MSI_Async() // Act Trace.WriteLine("4. ATM - should perform an RT refresh"); result = await mi.AcquireTokenForManagedIdentity(resource) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync() .ConfigureAwait(false); @@ -208,7 +214,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_MSI_Async() Assert.AreEqual(refreshOn, result.AuthenticationResultMetadata.RefreshOn); result = await mi.AcquireTokenForManagedIdentity(resource) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync() .ConfigureAwait(false); @@ -240,7 +246,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_OBO_Async() string oboCacheKey = "obo-cache-key"; var result = await cca.InitiateLongRunningProcessInWebApi(TestConstants.s_scope, TestConstants.DefaultAccessToken, ref oboCacheKey) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TestConstants.ATSecret, result.AccessToken); @@ -253,7 +259,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_OBO_Async() Trace.WriteLine("3. Configure AAD to respond with a valid token"); result = await cca.AcquireTokenInLongRunningProcess(TestConstants.s_scope, oboCacheKey) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TestConstants.ATSecret, result.AccessToken); @@ -265,7 +271,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_OBO_Async() Trace.WriteLine("4. Fetch token from cache"); result = await cca.AcquireTokenInLongRunningProcess(TestConstants.s_scope, oboCacheKey) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync().ConfigureAwait(false); Assert.AreEqual(TestConstants.ATSecret, result.AccessToken); @@ -299,7 +305,7 @@ public async Task ProactiveTokenRefresh_AadUnavailableResponse_Async() // Act AuthenticationResult result = await _cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync() .ConfigureAwait(false); @@ -313,7 +319,7 @@ public async Task ProactiveTokenRefresh_AadUnavailableResponse_Async() _harness.HttpManager.AddTokenResponse(TokenResponseType.Valid_ClientCredentials); result = await _cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync() .ConfigureAwait(false); Assert.IsNotNull(result); @@ -342,13 +348,13 @@ private async Task AcquireTokenSuccessAsync(bool withExtension = false) // Acquire token for client with scope result = await _cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .WithAuthenticationExtension(authExtension) .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); Assert.IsNotNull(result); result = await _cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .WithAuthenticationExtension(authExtension) .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); Assert.IsNotNull(result); @@ -358,13 +364,13 @@ private async Task AcquireTokenSuccessAsync(bool withExtension = false) _harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); // Acquire token for client with scope result = await _cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); Assert.IsNotNull(result); // Acquire token from the cache result = await _cca.AcquireTokenForClient(TestConstants.s_scope) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); Assert.IsNotNull(result); } @@ -377,7 +383,7 @@ private async Task AcquireTokenMsalServiceExceptionAsync() //Test for MsalServiceException MsalServiceException ex = await AssertException.TaskThrowsAsync( () => _cca.AcquireTokenForClient(TestConstants.s_scopeForAnotherResource) - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .WithTenantId(TestConstants.Utid) .ExecuteAsync(CancellationToken.None)).ConfigureAwait(false); @@ -390,7 +396,7 @@ private async Task AcquireTokenMsalClientExceptionAsync() //Test for MsalClientException MsalClientException exClient = await AssertException.TaskThrowsAsync( () => _cca.AcquireTokenForClient(null) // null scope -> client exception - .WithExtraQueryParameters(new Dictionary { { "caller-sdk-id", callerSdkId }, { "caller-sdk-ver", callerSdkVersion } }) + .WithExtraQueryParameters(extraQueryParams) .WithTenantId(TestConstants.Utid) .ExecuteAsync(CancellationToken.None)).ConfigureAwait(false); From 3f853e0ac34c067575708be02941346700e01f4a Mon Sep 17 00:00:00 2001 From: avdunn Date: Sun, 19 Oct 2025 16:15:31 -0700 Subject: [PATCH 02/10] Fix tests --- .../HeadlessTests/Agentic.cs | 3 ++- .../HeadlessTests/FmiIntegrationTests.cs | 12 +++++++----- .../devapps/DesktopTestApp/PublicClientHandler.cs | 10 +++++++++- tests/devapps/NetFxConsoleTestApp/Program.cs | 14 +++++++------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs index fde8d13181..2ec6b5a9cf 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/Agentic.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; @@ -58,7 +59,7 @@ private static async Task AgentUserIdentityGetsTokenForGraphAsync() .WithAuthority("https://login.microsoftonline.com/", TenantId) .WithCacheOptions(CacheOptions.EnableSharedCacheOptions) .WithExperimentalFeatures(true) - .WithExtraQueryParameters("slice=first") + .WithExtraQueryParameters(new Dictionary { { "slice", ("first", false) } }) .WithClientAssertion((AssertionRequestOptions _) => GetAppCredentialAsync(AgentIdentity)) .Build(); diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs index cde824190f..cb02174816 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs @@ -11,6 +11,7 @@ using Microsoft.Identity.Test.Common.Core.Helpers; using System; using System.IdentityModel.Tokens.Jwt; +using System.Collections.Generic; namespace Microsoft.Identity.Test.Integration.NetCore.HeadlessTests { @@ -26,6 +27,7 @@ public class FmiIntegrationTests { private byte[] _serializedCache; private const string Testslice = "dc=ESTSR-PUB-WUS-LZ1-TEST"; //Updated slice for regional tests + private Dictionary TestsliceQueryParam = new Dictionary { { "domain_hint", ("mydomain.com", false) } }; private const string AzureRegion = "westus3"; private const string TenantId = "f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; //Tenant Id for the test app @@ -45,7 +47,7 @@ public async Task Flow1_Credential_From_Cert() var confidentialApp = ConfidentialClientApplicationBuilder .Create(clientId) .WithAuthority("https://login.microsoftonline.com/", TenantId) - .WithExtraQueryParameters(Testslice) //Enables MSAL to target ESTS Test slice + .WithExtraQueryParameters(TestsliceQueryParam) //Enables MSAL to target ESTS Test slice .WithCertificate(cert, sendX5C: true) //sendX5c enables SN+I auth which is required for FMI flows .WithAzureRegion(AzureRegion) .BuildConcrete(); @@ -90,7 +92,7 @@ public async Task Flow2_Token_From_CertTest() var confidentialApp = ConfidentialClientApplicationBuilder .Create(clientId) .WithAuthority("https://login.microsoftonline.com/", TenantId) - .WithExtraQueryParameters(Testslice) + .WithExtraQueryParameters(TestsliceQueryParam) .WithCertificate(cert, sendX5C: true) .WithAzureRegion(AzureRegion) .BuildConcrete(); @@ -130,7 +132,7 @@ public async Task Flow3_FmiCredential_From_AnotherFmiCredential() var confidentialApp = ConfidentialClientApplicationBuilder .Create(clientId) .WithAuthority("https://login.microsoftonline.com/", TenantId) - .WithExtraQueryParameters(Testslice) + .WithExtraQueryParameters(TestsliceQueryParam) .WithClientAssertion((options) => GetFmiCredentialFromRma(options, Testslice)) .WithAzureRegion(AzureRegion) .BuildConcrete(); @@ -171,7 +173,7 @@ public async Task Flow4_SubRma_FIC_From_FmiCredential() var confidentialApp = ConfidentialClientApplicationBuilder .Create(clientId) .WithAuthority("https://login.microsoftonline.com/", TenantId) - .WithExtraQueryParameters(Testslice) + .WithExtraQueryParameters(TestsliceQueryParam) .WithClientAssertion((options) => GetFmiCredentialFromRma(options, Testslice)) .WithAzureRegion(AzureRegion) .BuildConcrete(); @@ -211,7 +213,7 @@ public async Task Flow5_FmiToken_From_FmiCred() var confidentialApp = ConfidentialClientApplicationBuilder .Create(clientId) .WithAuthority("https://login.microsoftonline.com/", TenantId) - .WithExtraQueryParameters(Testslice) + .WithExtraQueryParameters(TestsliceQueryParam) .WithClientAssertion((options) => GetFmiCredentialFromRma(options, Testslice)) .WithAzureRegion(AzureRegion) .BuildConcrete(); diff --git a/tests/devapps/DesktopTestApp/PublicClientHandler.cs b/tests/devapps/DesktopTestApp/PublicClientHandler.cs index f09cb5af62..0ca4057790 100644 --- a/tests/devapps/DesktopTestApp/PublicClientHandler.cs +++ b/tests/devapps/DesktopTestApp/PublicClientHandler.cs @@ -13,7 +13,8 @@ namespace DesktopTestApp class PublicClientHandler { private const string _clientName = "DesktopTestApp"; - private const string _ciamExtraQParams = "dc=ESTS-PUB-EUS-AZ1-FD000-TEST1"; + private Dictionary _ciamExtraQParams = new Dictionary { { "dc", ("ESTS-PUB-EUS-AZ1-FD000-TEST1", false) } }; + private const string _ciamRedirectUri = "http://localhost"; public PublicClientHandler(string clientId, LogCallback logCallback) @@ -44,6 +45,7 @@ public async Task AcquireTokenInteractiveAsync( CreateOrUpdatePublicClientApp(InteractiveAuthority, ApplicationId); AuthenticationResult result; +#pragma warning disable CS0618 // Type or member is obsolete if (CurrentUser != null) { result = await PublicClientApplication @@ -64,6 +66,7 @@ public async Task AcquireTokenInteractiveAsync( .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); } +#pragma warning restore CS0618 // Type or member is obsolete CurrentUser = result.Account; return result; @@ -77,6 +80,7 @@ public async Task AcquireTokenInteractiveWithAuthorityAsyn CreateOrUpdatePublicClientApp(InteractiveAuthority, ApplicationId); AuthenticationResult result; +#pragma warning disable CS0618 // Type or member is obsolete if (CurrentUser != null) { result = await PublicClientApplication @@ -99,6 +103,7 @@ public async Task AcquireTokenInteractiveWithAuthorityAsyn .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); } +#pragma warning restore CS0618 // Type or member is obsolete CurrentUser = result.Account; return result; @@ -127,6 +132,8 @@ public async Task AcquireTokenInteractiveWithB2CAuthorityA { CreateOrUpdatePublicClientApp(b2cAuthority, ApplicationId); AuthenticationResult result; + +#pragma warning disable CS0618 // Type or member is obsolete result = await PublicClientApplication .AcquireTokenInteractive(scopes) .WithAccount(CurrentUser) @@ -135,6 +142,7 @@ public async Task AcquireTokenInteractiveWithB2CAuthorityA .WithB2CAuthority(b2cAuthority) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); +#pragma warning restore CS0618 // Type or member is obsolete CurrentUser = result.Account; return result; diff --git a/tests/devapps/NetFxConsoleTestApp/Program.cs b/tests/devapps/NetFxConsoleTestApp/Program.cs index 71edf11089..990a708b7d 100644 --- a/tests/devapps/NetFxConsoleTestApp/Program.cs +++ b/tests/devapps/NetFxConsoleTestApp/Program.cs @@ -378,10 +378,10 @@ x. Exit app CancellationTokenSource cts = new CancellationTokenSource(); result = await pca.AcquireTokenInteractive(s_scopes) .WithUseEmbeddedWebView(false) - .WithExtraQueryParameters(new Dictionary() { - { "dc", "prod-wst-test1"}, - { "slice", "test"}, - { "sshcrt", "true" } + .WithExtraQueryParameters(new Dictionary() { + { "dc", ("prod-wst-test1", false)}, + { "slice", ("test", false)}, + { "sshcrt", ("true", false) } }) .WithSSHCertificateAuthenticationScheme(jwk, "1") .WithSystemWebViewOptions(new SystemWebViewOptions() @@ -676,11 +676,11 @@ private static string GetPasswordFromConsole() return pwd; } - private static Dictionary GetTestSliceParams() + private static Dictionary GetTestSliceParams() { - return new Dictionary() + return new Dictionary() { - { "dc", "prod-wst-test1" }, + { "dc", ("prod-wst-test1", false) }, }; } } From 6a8aa9ed006335d9804924f3d53da30cf1fbb544 Mon Sep 17 00:00:00 2001 From: avdunn Date: Sun, 19 Oct 2025 16:42:57 -0700 Subject: [PATCH 03/10] Fix test --- .../HeadlessTests/FmiIntegrationTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs index cb02174816..c9249360db 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs @@ -248,6 +248,7 @@ private static async Task GetFmiCredentialFromRma(AssertionRequestOption X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); +#pragma warning disable CS0618 // Type or member is obsolete //Create application var confidentialApp = ConfidentialClientApplicationBuilder .Create(clientId) @@ -256,6 +257,7 @@ private static async Task GetFmiCredentialFromRma(AssertionRequestOption .WithCertificate(cert, sendX5C: true) //sendX5c enables SN+I auth which is required for FMI flows .WithAzureRegion(AzureRegion) .BuildConcrete(); +#pragma warning restore CS0618 // Type or member is obsolete //Acquire Token var authResult = await confidentialApp.AcquireTokenForClient(new[] { scope }) From 2921a7968e9883b7b676f3e49e0e0f02a0171370 Mon Sep 17 00:00:00 2001 From: avdunn Date: Sun, 19 Oct 2025 17:36:43 -0700 Subject: [PATCH 04/10] More test fixes --- .../HeadlessTests/FmiIntegrationTests.cs | 2 +- tests/devapps/NetCoreTestApp/Program.cs | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs index c9249360db..4996fda4a7 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/FmiIntegrationTests.cs @@ -27,7 +27,7 @@ public class FmiIntegrationTests { private byte[] _serializedCache; private const string Testslice = "dc=ESTSR-PUB-WUS-LZ1-TEST"; //Updated slice for regional tests - private Dictionary TestsliceQueryParam = new Dictionary { { "domain_hint", ("mydomain.com", false) } }; + private Dictionary TestsliceQueryParam = new Dictionary { { "dc", ("ESTSR-PUB-WUS-LZ1-TEST", false) } }; private const string AzureRegion = "westus3"; private const string TenantId = "f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; //Tenant Id for the test app diff --git a/tests/devapps/NetCoreTestApp/Program.cs b/tests/devapps/NetCoreTestApp/Program.cs index f9d312fad3..47e03b9b7a 100644 --- a/tests/devapps/NetCoreTestApp/Program.cs +++ b/tests/devapps/NetCoreTestApp/Program.cs @@ -22,10 +22,10 @@ namespace NetCoreTestApp { public class Program { - internal /* for test */ static Dictionary CallerSDKDetails { get; } = new() + internal /* for test */ static Dictionary CallerSDKDetails { get; } = new() { - { "caller-sdk-id", "IdWeb_1" }, - { "caller-sdk-ver", "123" } + { "caller-sdk-id", ("IdWeb_1", false) }, + { "caller-sdk-ver", ("123", false) } }; // This app has http://localhost redirect uri registered @@ -387,7 +387,10 @@ 0. Exit App var resultX1 = await cca1.AcquireTokenForClient(GraphAppScope) .WithMtlsProofOfPossession() - .WithExtraQueryParameters("dc=ESTSR-PUB-WUS3-AZ1-TEST1&slice=TestSlice") //Feature in test slice + .WithExtraQueryParameters(new Dictionary() { + { "dc", ("ESTSR-PUB-WUS3-AZ1-TEST1", false)}, + { "slice", ("TestSlice", false)} + }) //Feature in test slice .ExecuteAsync() .ConfigureAwait(false); From d8e728eb6c5eba9170f82e752328b0373b03cde0 Mon Sep 17 00:00:00 2001 From: avdunn Date: Sun, 19 Oct 2025 19:33:52 -0700 Subject: [PATCH 05/10] Test fix --- .../PublicApiTests/ExtraQueryParametersTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs index a295dcc07a..570b637464 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs @@ -28,7 +28,6 @@ public async Task WithExtraQueryParameters_DeprecatedDoNotAffectTokenCaching_Tes { using (var httpManager = new MockHttpManager()) { - httpManager.AddInstanceDiscoveryMockHandler(); #pragma warning disable CS0618 // Type or member is obsolete // Create a confidential client application with a default extra query parameter From d8d60efea6cf1609925465ab112ee86e59a6f4ed Mon Sep 17 00:00:00 2001 From: Avery-Dunn <62066438+Avery-Dunn@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:29:50 -0700 Subject: [PATCH 06/10] Update src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs Co-authored-by: Bogdan Gavril --- .../ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs index cfbb2435e1..68cd690cbc 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs @@ -89,7 +89,7 @@ public T WithCorrelationId(Guid correlationId) /// as a string of segments of the form key=value separated by an ampersand character. /// The parameter can be null. /// The builder to chain the .With methods. - [Obsolete("This method is deprecated. Please use the WithExtraQueryParameters(IDictionary) method instead, which provides control over which parameters are included in the cache key.", false)] + [Obsolete("This method is deprecated. Use the WithExtraQueryParameters(IDictionary) method instead, which provides control over which parameters are included in the cache key.", false)] public T WithExtraQueryParameters(Dictionary extraQueryParameters) { return WithExtraQueryParameters(CoreHelpers.ConvertToTupleParameters(extraQueryParameters)); From 8ab80c9c1d83a923d63800856b456a06e4db225f Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 23 Oct 2025 15:39:06 -0700 Subject: [PATCH 07/10] PR feedback --- .../BaseAbstractAcquireTokenParameterBuilder.cs | 15 +++++++++------ .../AppConfig/AbstractApplicationBuilder.cs | 17 ++++++++++------- .../PublicApi/net462/PublicAPI.Unshipped.txt | 4 ++-- .../PublicApi/net472/PublicAPI.Unshipped.txt | 4 ++-- .../net8.0-android/PublicAPI.Unshipped.txt | 4 ++-- .../net8.0-ios/PublicAPI.Unshipped.txt | 4 ++-- .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 4 ++-- .../netstandard2.0/PublicAPI.Unshipped.txt | 4 ++-- .../Utils/CoreHelpers.cs | 2 +- 9 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs index 68cd690cbc..488e104427 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs @@ -98,11 +98,14 @@ public T WithExtraQueryParameters(Dictionary extraQueryParameter /// /// Sets Extra Query Parameters for the query string in the HTTP authentication request with control over which parameters are included in the cache key /// - /// This parameter will be appended as is to the query string in the HTTP authentication request to the authority. - /// For each parameter, you can specify whether it should be included in the cache key. + /// This parameter will be appended as is to the query string in the HTTP authentication request to the authority, and merged with those added to the application-level WithExtraQueryParameters API. + /// Each dictionary entry maps a parameter name to a tuple containing: + /// - Value: The parameter value that will be appended to the query string + /// - IncludeInCacheKey: Whether this parameter should be included when computing the token's cache key. + /// To help ensure the correct token is returned from the cache, IncludeInCacheKey should be true if the parameter affects token content or validity (e.g., resource-specific claims or parameters). /// The parameter can be null. /// The builder to chain .With methods. - public T WithExtraQueryParameters(IDictionary extraQueryParameters) + public T WithExtraQueryParameters(IDictionary extraQueryParameters) { if (extraQueryParameters == null) { @@ -115,14 +118,14 @@ public T WithExtraQueryParameters(IDictionary(StringComparer.OrdinalIgnoreCase); - CommonParameters.ExtraQueryParameters[kvp.Key] = kvp.Value.value; + CommonParameters.ExtraQueryParameters[kvp.Key] = kvp.Value.Value; - if (kvp.Value.includeInCacheKey) + if (kvp.Value.IncludeInCacheKey) { CommonParameters.CacheKeyComponents = CommonParameters.CacheKeyComponents ?? new SortedList>>(); // Capture the value in a local to avoid closure issues - string valueToCache = kvp.Value.value; + string valueToCache = kvp.Value.Value; // Add to cache key components - uses a func that returns the value as a task CommonParameters.CacheKeyComponents[kvp.Key] = (CancellationToken _) => Task.FromResult(valueToCache); diff --git a/src/client/Microsoft.Identity.Client/AppConfig/AbstractApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/AbstractApplicationBuilder.cs index 2159580a9c..bd8a79f47b 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/AbstractApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/AbstractApplicationBuilder.cs @@ -318,11 +318,14 @@ public T WithExtraQueryParameters(string extraQueryParameters) /// /// Sets Extra Query Parameters for the query string in the HTTP authentication request with control over which parameters are included in the cache key /// - /// This parameter will be appended as is to the query string in the HTTP authentication request to the authority. - /// For each parameter, you can specify whether it should be included in the cache key. + /// This parameter will be appended as is to the query string in the HTTP authentication request to the authority, and merged with those added to the request-level WithExtraQueryParameters API. + /// Each dictionary entry maps a parameter name to a tuple containing: + /// - Value: The parameter value that will be appended to the query string + /// - IncludeInCacheKey: Whether this parameter should be included when computing the token's cache key. + /// To help ensure the correct token is returned from the cache, IncludeInCacheKey should be true if the parameter affects token content or validity (e.g., resource-specific claims or parameters). /// The parameter can be null. - /// The builder to chain the .With methods - public T WithExtraQueryParameters(IDictionary extraQueryParameters) + /// The builder to chain .With methods. + public T WithExtraQueryParameters(IDictionary extraQueryParameters) { if (extraQueryParameters == null) { @@ -335,15 +338,15 @@ public T WithExtraQueryParameters(IDictionary(StringComparer.OrdinalIgnoreCase); - Config.ExtraQueryParameters[kvp.Key] = kvp.Value.value; + Config.ExtraQueryParameters[kvp.Key] = kvp.Value.Value; - if (kvp.Value.includeInCacheKey) + if (kvp.Value.IncludeInCacheKey) { // Initialize the cache key components if needed Config.CacheKeyComponents = Config.CacheKeyComponents ?? new SortedList(); // Add to cache key components - uses a func that returns the value as a task - Config.CacheKeyComponents[kvp.Key] = kvp.Value.value; + Config.CacheKeyComponents[kvp.Key] = kvp.Value.Value; } } diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index 5625fc9ca0..7c21bd31ba 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ -Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T -Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index 5625fc9ca0..7c21bd31ba 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ -Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T -Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index 5625fc9ca0..7c21bd31ba 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ -Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T -Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index 5625fc9ca0..7c21bd31ba 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ -Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T -Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index 5625fc9ca0..7c21bd31ba 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ -Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T -Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index 5625fc9ca0..7c21bd31ba 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ -Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T -Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T +Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T diff --git a/src/client/Microsoft.Identity.Client/Utils/CoreHelpers.cs b/src/client/Microsoft.Identity.Client/Utils/CoreHelpers.cs index 656ebf245a..09a3b36a24 100644 --- a/src/client/Microsoft.Identity.Client/Utils/CoreHelpers.cs +++ b/src/client/Microsoft.Identity.Client/Utils/CoreHelpers.cs @@ -153,7 +153,7 @@ public static Dictionary ParseKeyValueList(string input, char de var result = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var kvp in parameters) { - result[kvp.Key] = (kvp.Value, false); // Include all parameters in cache key by default + result[kvp.Key] = (kvp.Value, false); // Exclude all parameters from cache key by default } return result; } From 730714fb6c4053641213123df57d876a191a4cb8 Mon Sep 17 00:00:00 2001 From: Avery-Dunn <62066438+Avery-Dunn@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:28:57 -0700 Subject: [PATCH 08/10] Update tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../PublicApiTests/ExtraQueryParametersTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs index 570b637464..f02ac29a29 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ExtraQueryParametersTests.cs @@ -278,7 +278,7 @@ public async Task WithExtraQueryParameters_TupleVersion_ControlsCaching_TestAsyn // Verify final cache state Assert.AreEqual(4, app.AppTokenCacheInternal.Accessor.GetAllAccessTokens().Count, "Should have 4 app tokens in cache"); - Assert.AreEqual(1, app.UserTokenCacheInternal.Accessor.GetAllAccessTokens().Count, "Should have 2 user tokens in cache"); + Assert.AreEqual(1, app.UserTokenCacheInternal.Accessor.GetAllAccessTokens().Count, "Should have 1 user token in cache"); Assert.AreEqual(1, app.UserTokenCacheInternal.Accessor.GetAllRefreshTokens().Count, "Should have 1 refresh token in cache"); } } From 8b2d4d4ea5d5363e332a5ff637669487740827aa Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 28 Oct 2025 14:50:08 -0700 Subject: [PATCH 09/10] PR feedback --- .../ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs | 2 -- .../ApiConfig/Parameters/AcquireTokenCommonParameters.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs index 488e104427..561c30742c 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs @@ -116,8 +116,6 @@ public T WithExtraQueryParameters(IDictionary(StringComparer.OrdinalIgnoreCase); - CommonParameters.ExtraQueryParameters[kvp.Key] = kvp.Value.Value; if (kvp.Value.IncludeInCacheKey) diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs index e5bd4fdac8..27ca2d7291 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs @@ -26,7 +26,7 @@ internal class AcquireTokenCommonParameters public Guid UserProvidedCorrelationId { get; set; } public bool UseCorrelationIdFromUser { get; set; } public IEnumerable Scopes { get; set; } - public IDictionary ExtraQueryParameters { get; set; } + public IDictionary ExtraQueryParameters { get; set; } = new Dictionary(); public string Claims { get; set; } public AuthorityInfo AuthorityOverride { get; set; } public IAuthenticationOperation AuthenticationOperation { get; set; } = new BearerAuthenticationOperation(); From 47bfd55694313b7616dd9cf64a3e9e8d5f5b1a61 Mon Sep 17 00:00:00 2001 From: avdunn Date: Tue, 28 Oct 2025 16:19:09 -0700 Subject: [PATCH 10/10] PR feedback --- .../ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs | 2 ++ .../ApiConfig/Parameters/AcquireTokenCommonParameters.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs index 561c30742c..c2f50c33cc 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/BaseAbstractAcquireTokenParameterBuilder.cs @@ -113,6 +113,8 @@ public T WithExtraQueryParameters(IDictionary(StringComparer.OrdinalIgnoreCase); + // Add each parameter to ExtraQueryParameters and, if requested, to CacheKeyComponents foreach (var kvp in extraQueryParameters) { diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs index 27ca2d7291..e5bd4fdac8 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs @@ -26,7 +26,7 @@ internal class AcquireTokenCommonParameters public Guid UserProvidedCorrelationId { get; set; } public bool UseCorrelationIdFromUser { get; set; } public IEnumerable Scopes { get; set; } - public IDictionary ExtraQueryParameters { get; set; } = new Dictionary(); + public IDictionary ExtraQueryParameters { get; set; } public string Claims { get; set; } public AuthorityInfo AuthorityOverride { get; set; } public IAuthenticationOperation AuthenticationOperation { get; set; } = new BearerAuthenticationOperation();