Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ namespace Microsoft.Identity.Client
/// Implementations must be thread safe.
/// Do not create a new HttpClient for each call to <see cref="GetHttpClient"/> - this leads to socket exhaustion.
/// If your app uses Integrated Windows Authentication, ensure <see cref="HttpClientHandler.UseDefaultCredentials"/> is set to true.
/// Custom factories own their HTTP transport behavior. For WS-Trust scenarios, they must enforce equivalent
/// redirect and credential-target policies that prevent credentials from being sent to insecure or untrusted
/// redirect targets.
/// </remarks>
public interface IMsalHttpClientFactory
{
Expand Down
100 changes: 89 additions & 11 deletions src/client/Microsoft.Identity.Client/Http/HttpManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,14 @@ public async Task<HttpResponse> SendRequestAsync(
Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> validateServerCert,
CancellationToken cancellationToken,
IRetryPolicy retryPolicy,
int retryCount = 0)
int retryCount = 0,
bool allowAutoRedirect = true,
bool useDefaultCredentials = true,
HttpRequestOperationContext operationContext = null)
{
Exception timeoutException = null;
HttpResponse response = null;
int currentRetryCount = operationContext?.RetryCount ?? retryCount;

try
{
Expand All @@ -89,7 +93,10 @@ public async Task<HttpResponse> SendRequestAsync(
method,
bindingCertificate,
validateServerCert, logger,
cancellationToken).ConfigureAwait(false);
cancellationToken,
allowAutoRedirect,
useDefaultCredentials,
operationContext).ConfigureAwait(false);
}

if (response.StatusCode == HttpStatusCode.OK)
Expand All @@ -112,10 +119,43 @@ public async Task<HttpResponse> SendRequestAsync(
logger.Error("The HTTP request failed. " + exception.Message);
timeoutException = exception;
}

while (!_disableInternalRetries && await retryPolicy.PauseForRetryAsync(response, timeoutException, retryCount, logger).ConfigureAwait(false))

if (operationContext?.IsTimedOut == true && timeoutException is null)
{
timeoutException = new TaskCanceledException(MsalErrorMessage.RequestTimeOut);
}

bool shouldRetry = false;
if (!_disableInternalRetries && operationContext?.IsTimedOut != true)
{
retryCount++;
try
{
shouldRetry = await retryPolicy.PauseForRetryAsync(
response,
timeoutException,
currentRetryCount,
logger,
operationContext?.CancellationToken ?? cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException exception)
{
if (cancellationToken.IsCancellationRequested)
{
logger.Info("The HTTP request was canceled. ");
throw;
}

timeoutException = exception;
}
}

if (shouldRetry)
{
currentRetryCount++;
if (operationContext is not null)
{
operationContext.RetryCount = currentRetryCount;
}

return await SendRequestAsync(
endpoint,
Expand All @@ -128,7 +168,10 @@ public async Task<HttpResponse> SendRequestAsync(
validateServerCert,
cancellationToken,
retryPolicy,
retryCount) // Pass the updated retry count
currentRetryCount,
allowAutoRedirect,
useDefaultCredentials,
operationContext)
.ConfigureAwait(false);
}

Expand Down Expand Up @@ -176,13 +219,32 @@ public async Task<HttpResponse> SendRequestAsync(
return response;
}

private HttpClient GetHttpClient(X509Certificate2 x509Certificate2, Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> validateServerCert)
private HttpClient GetHttpClient(
X509Certificate2 x509Certificate2,
Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> validateServerCert,
bool allowAutoRedirect,
bool useDefaultCredentials,
ILoggerAdapter logger)
{
if (x509Certificate2 != null && validateServerCert != null)
{
throw new NotImplementedException("Mtls certificate cannot be used with service fabric. A custom http client is used for service fabric managed identity to validate the server certificate.");
}

if (x509Certificate2 is null &&
validateServerCert is null &&
_httpClientFactory is IHttpClientFactoryWithRedirectControl redirectControlFactory)
Comment thread
logdog16 marked this conversation as resolved.
Outdated
Comment thread
logdog16 marked this conversation as resolved.
Outdated
{
return redirectControlFactory.GetHttpClient(
allowAutoRedirect,
useDefaultCredentials);
}

if (!allowAutoRedirect || !useDefaultCredentials)
Comment thread
logdog16 marked this conversation as resolved.
Outdated
{
logger.Warning(MsalErrorMessage.CustomHttpClientFactoryRedirectControlUnavailable);
}
Comment thread
logdog16 marked this conversation as resolved.

if (validateServerCert != null)
{
// If the factory is an IMsalSFHttpClientFactory, use it to get an HttpClient with the custom handler
Expand Down Expand Up @@ -239,7 +301,10 @@ private async Task<HttpResponse> ExecuteAsync(
X509Certificate2 bindingCertificate,
Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> validateServerCert,
ILoggerAdapter logger,
CancellationToken cancellationToken = default)
CancellationToken cancellationToken,
bool allowAutoRedirect,
bool useDefaultCredentials,
HttpRequestOperationContext operationContext)
{
using (HttpRequestMessage requestMessage = CreateRequestMessage(endpoint, headers))
{
Expand All @@ -252,12 +317,24 @@ private async Task<HttpResponse> ExecuteAsync(

Stopwatch sw = Stopwatch.StartNew();

HttpClient client = GetHttpClient(bindingCertificate, validateServerCert);
HttpClient client = GetHttpClient(
bindingCertificate,
validateServerCert,
allowAutoRedirect,
useDefaultCredentials,
logger);

CancellationToken effectiveCancellationToken = cancellationToken;
if (operationContext is not null)
{
operationContext.InitializeTimeout(client.Timeout);
effectiveCancellationToken = operationContext.CancellationToken;
}

try
{
using (HttpResponseMessage responseMessage =
await client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false))
await client.SendAsync(requestMessage, effectiveCancellationToken).ConfigureAwait(false))
{
logger.Verbose(() => $"[HttpManager] Received response. Status code: {responseMessage.StatusCode}. ");

Expand Down Expand Up @@ -300,7 +377,8 @@ protected static async Task<HttpContent> CloneHttpContentAsync(HttpContent httpC
{
Headers = response.Headers,
Body = body,
StatusCode = response.StatusCode
StatusCode = response.StatusCode,
RequestUri = response.RequestMessage?.RequestUri
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Threading;

namespace Microsoft.Identity.Client.Http
{
internal sealed class HttpRequestOperationContext : IDisposable
Comment thread
logdog16 marked this conversation as resolved.
Outdated
{
private readonly CancellationToken _callerCancellationToken;
private readonly CancellationTokenSource _cancellationTokenSource;
private bool _timeoutInitialized;

public HttpRequestOperationContext(CancellationToken cancellationToken)
{
_callerCancellationToken = cancellationToken;
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
}

public CancellationToken CancellationToken => _cancellationTokenSource.Token;

public int RetryCount { get; set; }

public bool IsTimedOut =>
_cancellationTokenSource.IsCancellationRequested &&
!_callerCancellationToken.IsCancellationRequested;

public void InitializeTimeout(TimeSpan timeout)
{
if (_timeoutInitialized)
{
return;
}

_timeoutInitialized = true;

if (timeout != Timeout.InfiniteTimeSpan)
{
_cancellationTokenSource.CancelAfter(timeout);
}
}

public void Dispose()
{
_cancellationTokenSource.Dispose();
}
}
}
3 changes: 3 additions & 0 deletions src/client/Microsoft.Identity.Client/Http/HttpResponse.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http.Headers;
Expand Down Expand Up @@ -34,5 +35,7 @@ public IDictionary<string, string> HeadersAsDictionary
public string UserAgent { get; set; }

public string Body { get; set; }

public Uri RequestUri { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Net.Http;

namespace Microsoft.Identity.Client.Http
{
internal interface IHttpClientFactoryWithRedirectControl
{
HttpClient GetHttpClient(bool allowAutoRedirect, bool useDefaultCredentials);
}
}
8 changes: 7 additions & 1 deletion src/client/Microsoft.Identity.Client/Http/IHttpManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ internal interface IHttpManager
/// <param name="cancellationToken"></param>
/// <param name="retryPolicy">Retry policy to be used for the request.</param>
/// <param name="retryCount">Number of retries to be attempted in case of retriable status codes.</param>
/// <param name="allowAutoRedirect">Whether the HTTP client can automatically follow redirects.</param>
/// <param name="useDefaultCredentials">Whether the HTTP client can use the current user's credentials.</param>
/// <param name="operationContext">State shared across all requests in one logical HTTP operation.</param>
/// <returns></returns>
Task<HttpResponse> SendRequestAsync(
Uri endpoint,
Expand All @@ -43,6 +46,9 @@ Task<HttpResponse> SendRequestAsync(
Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> validateServerCertificate,
CancellationToken cancellationToken,
IRetryPolicy retryPolicy,
int retryCount = 0);
int retryCount = 0,
bool allowAutoRedirect = true,
bool useDefaultCredentials = true,
HttpRequestOperationContext operationContext = null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;

Expand Down Expand Up @@ -40,12 +41,17 @@ public DefaultRetryPolicy(RequestType requestType)
}
}

internal virtual Task DelayAsync(int milliseconds)
internal virtual Task DelayAsync(int milliseconds, CancellationToken cancellationToken)
{
return Task.Delay(milliseconds);
return Task.Delay(milliseconds, cancellationToken);
}

public async Task<bool> PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger)
public async Task<bool> PauseForRetryAsync(
Comment thread
logdog16 marked this conversation as resolved.
HttpResponse response,
Exception exception,
int retryCount,
ILoggerAdapter logger,
CancellationToken cancellationToken)
{
// Check if the status code is retriable and if the current retry count is less than max retries
if (_retryCondition(response, exception) &&
Expand All @@ -63,7 +69,7 @@ public async Task<bool> PauseForRetryAsync(HttpResponse response, Exception exce
logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})");

// Pause execution for the calculated delay
await DelayAsync(retryAfterDelay).ConfigureAwait(false);
await DelayAsync(retryAfterDelay, cancellationToken).ConfigureAwait(false);

return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;

Expand All @@ -19,7 +20,13 @@ internal interface IRetryPolicy
/// <param name="exception">The exception encountered during the request.</param>
/// <param name="retryCount">The current retry attempt count.</param>
/// <param name="logger">The logger used for diagnostic and informational messages.</param>
/// <param name="cancellationToken">The cancellation token for the complete HTTP operation.</param>
/// <returns>A task that returns true if a retry should be performed; otherwise, false.</returns>
Task<bool> PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger);
Task<bool> PauseForRetryAsync(
HttpResponse response,
Exception exception,
int retryCount,
ILoggerAdapter logger,
CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;

Expand All @@ -28,19 +29,24 @@ internal class ImdsRetryPolicy : IRetryPolicy
ImdsRetryPolicy.ExponentialDeltaBackoffMs
);

internal virtual Task DelayAsync(int milliseconds)
internal virtual Task DelayAsync(int milliseconds, CancellationToken cancellationToken)
{
return Task.Delay(milliseconds);
return Task.Delay(milliseconds, cancellationToken);
}

protected virtual bool ShouldRetry(HttpResponse response, Exception exception)
{
return HttpRetryConditions.Imds(response, exception);
}

public async Task<bool> PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger)
public async Task<bool> PauseForRetryAsync(
HttpResponse response,
Exception exception,
int retryCount,
ILoggerAdapter logger,
CancellationToken cancellationToken)
{
int httpStatusCode = (int)response.StatusCode;
int httpStatusCode = response is null ? 0 : (int)response.StatusCode;

if (retryCount == 0)
{
Expand All @@ -61,7 +67,7 @@ public async Task<bool> PauseForRetryAsync(HttpResponse response, Exception exce
logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})");

// Pause execution for the calculated delay
await DelayAsync(retryAfterDelay).ConfigureAwait(false);
await DelayAsync(retryAfterDelay, cancellationToken).ConfigureAwait(false);

return true;
}
Expand Down
Loading
Loading