Skip to content

Commit e682cd1

Browse files
authored
Fix for 5108 - avoid concurrent writes to extra QP dictionary
* Fix for 5108 - avoid concurrent writes to extra QP dictionary * test fix * 2 * Address PR comments * Fix for 5108 - avoid concurrent writes to extra QP dictionary * 2 * Delete * Update comments in ParallelRequestMockHandler.cs
1 parent a003c14 commit e682cd1

File tree

7 files changed

+194
-136
lines changed

7 files changed

+194
-136
lines changed

src/client/Microsoft.Identity.Client/AppConfig/AbstractApplicationBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ protected T WithOptions(ApplicationOptions applicationOptions)
294294
/// <returns>The builder to chain the .With methods</returns>
295295
public T WithExtraQueryParameters(IDictionary<string, string> extraQueryParameters)
296296
{
297-
Config.ExtraQueryParameters = extraQueryParameters ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
297+
Config.ExtraQueryParameters = extraQueryParameters;
298298
return this as T;
299299
}
300300

src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public string ClientVersion
103103
public LogCallback LoggingCallback { get; internal set; }
104104
public IIdentityLogger IdentityLogger { get; internal set; }
105105
public string Component { get; internal set; }
106-
public IDictionary<string, string> ExtraQueryParameters { get; internal set; } = new Dictionary<string, string>();
106+
public IDictionary<string, string> ExtraQueryParameters { get; internal set; }
107107
public bool UseRecommendedDefaultRedirectUri { get; internal set; }
108108

109109
public bool ExperimentalFeaturesEnabled { get; set; } = false;

src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.Security.Cryptography.X509Certificates;
78
using System.Text;
89
using System.Threading.Tasks;
@@ -45,9 +46,18 @@ public AuthenticationRequestParameters(
4546
RedirectUri = new Uri(serviceBundle.Config.RedirectUri);
4647
AuthorityManager = new AuthorityManager(RequestContext, initialAuthority);
4748

48-
// Set application wide query parameters.
49-
ExtraQueryParameters = serviceBundle.Config.ExtraQueryParameters ??
50-
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
49+
// it is important to copy the values from the config to the request, so that the request can be modified
50+
// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/5108
51+
if (serviceBundle.Config.ExtraQueryParameters is null)
52+
{
53+
ExtraQueryParameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
54+
}
55+
else
56+
{
57+
ExtraQueryParameters = new Dictionary<string, string>(
58+
serviceBundle.Config.ExtraQueryParameters,
59+
StringComparer.OrdinalIgnoreCase);
60+
}
5161

5262
// Copy in call-specific query parameters.
5363
if (commonParameters.ExtraQueryParameters != null)

src/client/Microsoft.Identity.Client/Internal/Requests/Interactive/InteractiveRequest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,12 @@ private async Task<MsalTokenResponse> GetTokenResponseAsync(CancellationToken ca
123123
if (_requestParams.AppConfig.MultiCloudSupportEnabled)
124124
{
125125
_logger.Info("Instance Aware was configured.");
126-
_requestParams.AppConfig.ExtraQueryParameters[InstanceAwareParam] = "true";
126+
_requestParams.ExtraQueryParameters[InstanceAwareParam] = "true";
127127
}
128128

129129
// Aligh with MSAL CPP: https://github.com/AzureAD/microsoft-authentication-library-for-cpp/blob/4fa774db2b38cdce9c9b94e3e686ab53fc24b948/source/xplat/requests/WebRequestManager.cpp#L566
130130
// Always add 'haschrome=1' in the protocol to avoid unexpected back buttons on the first page.
131-
_requestParams.AppConfig.ExtraQueryParameters["haschrome"] = "1";
131+
_requestParams.ExtraQueryParameters["haschrome"] = "1";
132132

133133
IAuthCodeRequestComponent authorizationFetcher =
134134
_authCodeRequestComponentOverride ??
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Net.Http;
8+
using System.Security.Cryptography.X509Certificates;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.Identity.Client.Core;
12+
using Microsoft.Identity.Client.Http;
13+
using Microsoft.Identity.Client.OAuth2;
14+
using Microsoft.Identity.Test.Common.Core.Mocks;
15+
using Microsoft.Identity.Test.Unit.RequestsTests;
16+
using Microsoft.VisualStudio.TestTools.UnitTesting;
17+
18+
namespace Microsoft.Identity.Test.Unit.Helpers
19+
{
20+
/// <summary>
21+
/// This custom HttpManager does the following:
22+
/// - responds to instance discovery calls
23+
/// - responds with valid token response based on a naming convention (uid = "uid" + rtSecret, upn = "user_" + rtSecret) for "refresh_token" flow
24+
/// - responds with valid app token response for client_credentials flow.
25+
/// </summary>
26+
internal class ParallelRequestMockHandler : IHttpManager
27+
{
28+
public long LastRequestDurationInMs => 50;
29+
30+
public async Task<HttpResponse> SendRequestAsync(
31+
Uri endpoint,
32+
IDictionary<string, string> headers,
33+
HttpContent body,
34+
HttpMethod method,
35+
ILoggerAdapter logger,
36+
bool doNotThrow,
37+
X509Certificate2 mtlsCertificate,
38+
HttpClient customHttpClient,
39+
CancellationToken cancellationToken,
40+
int retryCount = 0)
41+
{
42+
// simulate delay and also add complexity due to thread context switch
43+
await Task.Delay(ParallelRequestsTests.NetworkAccessPenaltyMs).ConfigureAwait(false);
44+
45+
if (HttpMethod.Get == method &&
46+
endpoint.AbsoluteUri.StartsWith("https://login.microsoftonline.com/common/discovery/instance?api-version=1.1"))
47+
{
48+
return new HttpResponse()
49+
{
50+
Body = TestConstants.DiscoveryJsonResponse,
51+
StatusCode = System.Net.HttpStatusCode.OK
52+
};
53+
}
54+
55+
if (HttpMethod.Post == method &&
56+
UriWithoutQuery(endpoint).AbsoluteUri.Equals("https://login.microsoftonline.com/my-utid/oauth2/v2.0/token"))
57+
{
58+
var bodyString = await (body as FormUrlEncodedContent).ReadAsStringAsync().ConfigureAwait(false);
59+
var bodyDict = bodyString.Replace("?", "").Split('&').ToDictionary(x => x.Split('=')[0], x => x.Split('=')[1]);
60+
61+
if (bodyDict["grant_type"] == "refresh_token")
62+
{
63+
bodyDict.TryGetValue(OAuth2Parameter.RefreshToken, out string rtSecret);
64+
65+
return new HttpResponse()
66+
{
67+
Body = GetTokenResponseForRt(rtSecret),
68+
StatusCode = System.Net.HttpStatusCode.OK
69+
};
70+
}
71+
72+
if (bodyDict["grant_type"] == "client_credentials")
73+
{
74+
HttpResponseMessage response = MockHelpers.CreateSuccessfulClientCredentialTokenResponseMessage();
75+
string payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
76+
77+
return new HttpResponse()
78+
{
79+
Body = payload,
80+
StatusCode = System.Net.HttpStatusCode.OK
81+
};
82+
}
83+
}
84+
85+
Assert.Fail("Test issue - this HttpRequest is not mocked");
86+
return null;
87+
}
88+
89+
private Uri UriWithoutQuery(Uri uri)
90+
{
91+
return new UriBuilder(uri) { Query = string.Empty }.Uri;
92+
}
93+
94+
private string GetTokenResponseForRt(string rtSecret)
95+
{
96+
if (int.TryParse(rtSecret, out int i))
97+
{
98+
var upn = ParallelRequestsTests.GetUpn(i);
99+
var uid = ParallelRequestsTests.GetUid(i);
100+
HttpResponseMessage response = MockHelpers.CreateSuccessTokenResponseMessageWithUid(uid, TestConstants.Utid, upn);
101+
return response.Content.ReadAsStringAsync().Result;
102+
}
103+
104+
Assert.Fail("Expecting the rt secret to be a number, to be able to craft a response");
105+
return null;
106+
}
107+
}
108+
}

tests/Microsoft.Identity.Test.Unit/ParallelRequestsTests.cs

Lines changed: 45 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Microsoft.Identity.Client.OAuth2;
1818
using Microsoft.Identity.Test.Common.Core.Helpers;
1919
using Microsoft.Identity.Test.Common.Core.Mocks;
20+
using Microsoft.Identity.Test.Unit.Helpers;
2021
using Microsoft.VisualStudio.TestTools.UnitTesting;
2122
using NSubstitute.Core;
2223

@@ -41,6 +42,49 @@ public override void TestInitialize()
4142
_inMemoryCache = "{}";
4243
}
4344

45+
// regression test for https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/5108
46+
[TestMethod]
47+
public async Task ExtraQP()
48+
{
49+
Dictionary<string, string> extraQp = new()
50+
{
51+
{ "key1", "1" },
52+
{ "key2", "2" }
53+
};
54+
55+
// Arrange
56+
const int NumberOfRequests = 20;
57+
58+
ParallelRequestMockHandler httpManager = new ParallelRequestMockHandler();
59+
60+
var cca = ConfidentialClientApplicationBuilder
61+
.Create(TestConstants.ClientId)
62+
.WithAuthority(TestConstants.AuthorityUtidTenant, true)
63+
.WithClientSecret(TestConstants.ClientSecret)
64+
.WithHttpManager(httpManager)
65+
.Build();
66+
67+
var tasks = new List<Task<AuthenticationResult>>();
68+
69+
for (int i = 0; i < NumberOfRequests; i++)
70+
{
71+
tasks.Add(Task.Run(async () =>
72+
{
73+
return await cca.AcquireTokenForClient(TestConstants.s_scope)
74+
.WithExtraQueryParameters(extraQp)
75+
.ExecuteAsync()
76+
.ConfigureAwait(false);
77+
78+
}));
79+
}
80+
81+
// Wait for all tasks to complete
82+
AuthenticationResult[] results = await Task.WhenAll(tasks).ConfigureAwait(false);
83+
84+
// Assert
85+
Assert.AreEqual(NumberOfRequests, results.Length);
86+
}
87+
4488
[TestMethod]
4589
public async Task AcquireTokenSilent_ValidATs_ParallelRequests_Async()
4690
{
@@ -116,7 +160,7 @@ public async Task AcquireTokenSilent_ExpiredATs_ParallelRequests_Async()
116160
const int NumberOfRequests = 10;
117161

118162
// The typical HttpMockHandler used by other tests can't deal with parallel request
119-
ParallelRequestMockHanler httpManager = new ParallelRequestMockHanler();
163+
ParallelRequestMockHandler httpManager = new ParallelRequestMockHandler();
120164

121165
PublicClientApplication pca = PublicClientApplicationBuilder
122166
.Create(TestConstants.ClientId)
@@ -228,97 +272,4 @@ private void ConfigureCacheSerialization(IPublicClientApplication pca)
228272
});
229273
}
230274
}
231-
232-
/// <summary>
233-
/// This custom HttpManager does the following:
234-
/// - provides a standard response for discovery calls
235-
/// - responds with valid tokens based on a naming convention (uid = "uid" + rtSecret, upn = "user_" + rtSecret)
236-
/// </summary>
237-
internal class ParallelRequestMockHanler : IHttpManager
238-
{
239-
public long LastRequestDurationInMs => 50;
240-
241-
public Task<HttpResponse> SendGetAsync(Uri endpoint, IDictionary<string, string> headers, ILoggerAdapter logger, bool retry = true, CancellationToken cancellationToken = default)
242-
{
243-
Assert.Fail("Only instance discovery is supported");
244-
return Task.FromResult<HttpResponse>(null);
245-
}
246-
247-
public async Task<HttpResponse> SendPostAsync(Uri endpoint, IDictionary<string, string> headers, IDictionary<string, string> bodyParameters, ILoggerAdapter logger, CancellationToken cancellationToken = default)
248-
{
249-
await Task.Delay(ParallelRequestsTests.NetworkAccessPenaltyMs).ConfigureAwait(false);
250-
251-
if (endpoint.AbsoluteUri.Equals("https://login.microsoftonline.com/my-utid/oauth2/v2.0/token"))
252-
{
253-
bodyParameters.TryGetValue(OAuth2Parameter.RefreshToken, out string rtSecret);
254-
255-
return new HttpResponse()
256-
{
257-
Body = GetTokenResponseForRt(rtSecret),
258-
StatusCode = System.Net.HttpStatusCode.OK
259-
};
260-
}
261-
262-
Assert.Fail("Only refresh flow is supported");
263-
return null;
264-
}
265-
266-
public async Task<HttpResponse> SendRequestAsync(
267-
Uri endpoint,
268-
IDictionary<string, string> headers,
269-
HttpContent body,
270-
HttpMethod method,
271-
ILoggerAdapter logger,
272-
bool doNotThrow,
273-
X509Certificate2 mtlsCertificate,
274-
HttpClient customHttpClient,
275-
CancellationToken cancellationToken,
276-
int retryCount = 0)
277-
{
278-
// simulate delay and also add complexity due to thread context switch
279-
await Task.Delay(ParallelRequestsTests.NetworkAccessPenaltyMs).ConfigureAwait(false);
280-
281-
if (HttpMethod.Get == method &&
282-
endpoint.AbsoluteUri.StartsWith("https://login.microsoftonline.com/common/discovery/instance?api-version=1.1"))
283-
{
284-
return new HttpResponse()
285-
{
286-
Body = TestConstants.DiscoveryJsonResponse,
287-
StatusCode = System.Net.HttpStatusCode.OK
288-
};
289-
}
290-
291-
if (HttpMethod.Post == method &&
292-
endpoint.AbsoluteUri.Equals("https://login.microsoftonline.com/my-utid/oauth2/v2.0/token"))
293-
{
294-
var bodyString = (body as FormUrlEncodedContent).ReadAsStringAsync().GetAwaiter().GetResult();
295-
var bodyDict = bodyString.Replace("?", "").Split('&').ToDictionary(x => x.Split('=')[0], x => x.Split('=')[1]);
296-
297-
bodyDict.TryGetValue(OAuth2Parameter.RefreshToken, out string rtSecret);
298-
299-
return new HttpResponse()
300-
{
301-
Body = GetTokenResponseForRt(rtSecret),
302-
StatusCode = System.Net.HttpStatusCode.OK
303-
};
304-
}
305-
306-
Assert.Fail("Test issue - this HttpRequest is not mocked");
307-
return null;
308-
}
309-
310-
private string GetTokenResponseForRt(string rtSecret)
311-
{
312-
if (int.TryParse(rtSecret, out int i))
313-
{
314-
var upn = ParallelRequestsTests.GetUpn(i);
315-
var uid = ParallelRequestsTests.GetUid(i);
316-
HttpResponseMessage response = MockHelpers.CreateSuccessTokenResponseMessageWithUid(uid, TestConstants.Utid, upn);
317-
return response.Content.ReadAsStringAsync().Result;
318-
}
319-
320-
Assert.Fail("Expecting the rt secret to be a number, to be able to craft a response");
321-
return null;
322-
}
323-
}
324275
}

0 commit comments

Comments
 (0)