Skip to content

Commit a7a2d5b

Browse files
committed
Fix for #5809
1 parent 33ec4a7 commit a7a2d5b

3 files changed

Lines changed: 73 additions & 7 deletions

File tree

src/client/Microsoft.Identity.Client/Instance/Discovery/NetworkMetadataProvider.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Linq;
66
using System.Globalization;
7+
using System.Threading;
78
using System.Threading.Tasks;
89
using Microsoft.Identity.Client.Http;
910
using Microsoft.Identity.Client.OAuth2;
@@ -21,15 +22,24 @@ internal class NetworkMetadataProvider : INetworkMetadataProvider
2122
private readonly IHttpManager _httpManager;
2223
private readonly INetworkCacheMetadataProvider _networkCacheMetadataProvider;
2324
private readonly Uri _userProvidedInstanceDiscoveryUri; // can be null
25+
private readonly int _instanceDiscoveryTimeoutMs;
26+
27+
/// <summary>
28+
/// Default timeout for instance discovery network calls.
29+
/// Prevents waiting for the full HttpClient timeout (100s) when the discovery endpoint is unreachable.
30+
/// </summary>
31+
internal const int DefaultInstanceDiscoveryTimeoutMs = 10_000;
2432

2533
public NetworkMetadataProvider(
2634
IHttpManager httpManager,
2735
INetworkCacheMetadataProvider networkCacheMetadataProvider,
28-
Uri userProvidedInstanceDiscoveryUri = null)
36+
Uri userProvidedInstanceDiscoveryUri = null,
37+
int instanceDiscoveryTimeoutMs = DefaultInstanceDiscoveryTimeoutMs)
2938
{
3039
_httpManager = httpManager ?? throw new ArgumentNullException(nameof(httpManager));
3140
_networkCacheMetadataProvider = networkCacheMetadataProvider ?? throw new ArgumentNullException(nameof(networkCacheMetadataProvider));
3241
_userProvidedInstanceDiscoveryUri = userProvidedInstanceDiscoveryUri; // can be null
42+
_instanceDiscoveryTimeoutMs = instanceDiscoveryTimeoutMs;
3343
}
3444

3545
public async Task<InstanceDiscoveryMetadataEntry> GetMetadataAsync(Uri authority, RequestContext requestContext)
@@ -83,11 +93,25 @@ private async Task<InstanceDiscoveryResponse> SendInstanceDiscoveryRequestAsync(
8393

8494
Uri instanceDiscoveryEndpoint = ComputeHttpEndpoint(authority, requestContext);
8595

86-
InstanceDiscoveryResponse discoveryResponse = await client
87-
.DiscoverAadInstanceAsync(instanceDiscoveryEndpoint, requestContext)
88-
.ConfigureAwait(false);
96+
using var timeoutCts = new CancellationTokenSource(_instanceDiscoveryTimeoutMs);
97+
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
98+
requestContext.UserCancellationToken, timeoutCts.Token);
8999

90-
return discoveryResponse;
100+
CancellationToken originalToken = requestContext.UserCancellationToken;
101+
requestContext.UserCancellationToken = linkedCts.Token;
102+
103+
try
104+
{
105+
InstanceDiscoveryResponse discoveryResponse = await client
106+
.DiscoverAadInstanceAsync(instanceDiscoveryEndpoint, requestContext)
107+
.ConfigureAwait(false);
108+
109+
return discoveryResponse;
110+
}
111+
finally
112+
{
113+
requestContext.UserCancellationToken = originalToken;
114+
}
91115
}
92116

93117
private Uri ComputeHttpEndpoint(Uri authority, RequestContext requestContext)

src/client/Microsoft.Identity.Client/Internal/RequestContext.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ internal class RequestContext
2727
/// </summary>
2828
public ApiEvent ApiEvent { get; set; }
2929

30-
public CancellationToken UserCancellationToken { get; }
30+
public CancellationToken UserCancellationToken { get; set; }
3131

3232
public X509Certificate2 MtlsCertificate { get; }
3333

3434
public bool IsAttestationRequested { get; set; }
35-
35+
3636
public bool IsMtlsRequested { get; set; }
3737

3838
public RequestContext(IServiceBundle serviceBundle, Guid correlationId, X509Certificate2 mtlsCertificate, CancellationToken cancellationToken = default)

tests/Microsoft.Identity.Test.Unit/PublicApiTests/InstanceDiscoveryTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,5 +212,47 @@ public async Task InstanceDiscoveryFailure_IsCached_NotRetriedOnSubsequentCalls_
212212
// MockHttpManager.Dispose() asserts all handlers were consumed and no extra calls were made
213213
}
214214
}
215+
216+
[TestMethod]
217+
[WorkItem(5805)] // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/5805
218+
public async Task InstanceDiscoveryTimeout_FallsBackAndCachesResult_Async()
219+
{
220+
using (var httpManager = new MockHttpManager(disableInternalRetries: true))
221+
{
222+
// Arrange - use an authority unknown to MSAL so instance discovery goes to the network
223+
var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId)
224+
.WithAuthority(TestConstants.AuthorityNotKnownTenanted)
225+
.WithClientSecret(TestConstants.ClientSecret)
226+
.WithHttpManager(httpManager)
227+
.BuildConcrete();
228+
229+
// First call: instance discovery times out (TaskCanceledException is what HttpClient
230+
// throws on timeout), then token endpoint succeeds
231+
httpManager.AddMockHandler(new MockHttpMessageHandler()
232+
{
233+
ExpectedMethod = HttpMethod.Get,
234+
ExceptionToThrow = new TaskCanceledException("simulated timeout")
235+
});
236+
httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage();
237+
238+
var result1 = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray())
239+
.ExecuteAsync(CancellationToken.None)
240+
.ConfigureAwait(false);
241+
242+
Assert.AreEqual(TokenSource.IdentityProvider, result1.AuthenticationResultMetadata.TokenSource);
243+
244+
// Second call with a different scope to force a new token request from the STS.
245+
// Only mock the token endpoint — NO instance discovery mock.
246+
// If instance discovery were retried, the test would fail because
247+
// MockHttpManager would receive an unexpected HTTP call.
248+
httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage();
249+
250+
var result2 = await app.AcquireTokenForClient(TestConstants.s_scopeForAnotherResource.ToArray())
251+
.ExecuteAsync(CancellationToken.None)
252+
.ConfigureAwait(false);
253+
254+
Assert.AreEqual(TokenSource.IdentityProvider, result2.AuthenticationResultMetadata.TokenSource);
255+
}
256+
}
215257
}
216258
}

0 commit comments

Comments
 (0)