diff --git a/build/platform_and_feature_flags.props b/build/platform_and_feature_flags.props
index 5e0b4b3ebe..4d96368533 100644
--- a/build/platform_and_feature_flags.props
+++ b/build/platform_and_feature_flags.props
@@ -3,15 +3,18 @@
$(DefineConstants);NET_CORE;SUPPORTS_CONFIDENTIAL_CLIENT;SUPPORTS_CUSTOM_CACHE;SUPPORTS_BROKER;SUPPORTS_WIN32;
- $(DefineConstants);SUPPORTS_SYSTEM_TEXT_JSON
+ $(DefineConstants);SUPPORTS_SYSTEM_TEXT_JSON
-
+
$(DefineConstants);SUPPORTS_OTEL;
+
+ $(DefineConstants);SUPPORTS_MTLS;
+
$(DefineConstants);ANDROID;SUPPORTS_BROKER
-
+
$(DefineConstants);SUPPORTS_BROKER;SUPPORTS_CONFIDENTIAL_CLIENT;SUPPORTS_CUSTOM_CACHE;SUPPORTS_WIN32
@@ -20,4 +23,4 @@
$(DefineConstants);NETSTANDARD;SUPPORTS_CONFIDENTIAL_CLIENT;SUPPORTS_BROKER;SUPPORTS_CUSTOM_CACHE;SUPPORTS_WIN32;
-
+
\ No newline at end of file
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs
index 1cfcf89f55..6bc22ac492 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs
@@ -3,6 +3,7 @@
using System;
using System.ComponentModel;
+using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Executors;
@@ -86,5 +87,23 @@ public T WithProofOfPossession(PoPAuthenticationConfiguration popAuthenticationC
return this as T;
}
+
+ ///
+ /// Sends a token request over an MTLS (Mutual TLS) connection using the provided client certificate.
+ /// This method is public and part of the S2S (server-to-server) token binding process,
+ /// allowing external clients to securely establish a mutual TLS connection.
+ ///
+ ///
+ /// This method should be used when setting up a secure token exchange via MTLS.
+ /// It's important to ensure that the provided X509Certificate2 is valid and secure.
+ /// Note that only the /token request will be transmitted over this MTLS connection.
+ ///
+ /// An X509Certificate2 object representing the client certificate for MTLS.
+ /// An instance of the class, allowing for method chaining in a fluent interface style.
+ internal T WithMtlsCertificate(X509Certificate2 certificate)
+ {
+ CommonParameters.MtlsCertificate = certificate;
+ return this as T;
+ }
}
}
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs
index 9277789170..2f57da123a 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForManagedIdentityParameterBuilder.cs
@@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Executors;
using Microsoft.Identity.Client.ApiConfig.Parameters;
+using Microsoft.Identity.Client.Extensibility;
using Microsoft.Identity.Client.TelemetryCore.Internal.Events;
using Microsoft.Identity.Client.Utils;
@@ -59,6 +60,45 @@ public AcquireTokenForManagedIdentityParameterBuilder WithForceRefresh(bool forc
return this;
}
+ ///
+ /// Adds a claims challenge to the token request. The SDK will bypass the token cache when a claims challenge is specified.. Retry the
+ /// token acquisition, and use this value in the method. A claims challenge typically arises when
+ /// calling the protected downstream API, for example when the tenant administrator wants to revokes credentials. Apps are required
+ /// to look for a 401 Unauthorized response from the protected api and to parse the WWW-Authenticate response header in order to
+ /// extract the claims.See https://aka.ms/msal-net-claim-challenge for details. This API is not always available for managed identity flows,
+ /// depending on the client and that apps can monitor this by using method
+ ///
+ /// A string with one or multiple claims.
+ /// The builder to chain .With methods.
+ public AcquireTokenForManagedIdentityParameterBuilder WithClaims(string claims)
+ {
+ ValidateUseOfExperimentalFeature("WithClaims");
+
+ CommonParameters.Claims = claims;
+ return this;
+ }
+
+ ///
+ /// Registers an asynchronous delegate that will be invoked just before the token request is executed.
+ /// This delegate allows for modifications to the token request data, such as adding or removing headers,
+ /// or altering body parameters. Use this method to inject custom logic or to manipulate the request
+ /// based on dynamic conditions or application-specific requirements.
+ ///
+ /// An async delegate that takes an instance of
+ /// and allows for the manipulation of the request data before the token request is made. The delegate can perform
+ /// operations such as modifying the request headers, changing the request body, or logging request data.
+ /// The same instance to enable method chaining.
+ ///
+ /// This method is part of experimental features and may change in future releases. It is provided for testability purposes.
+ ///
+ public AcquireTokenForManagedIdentityParameterBuilder OnBeforeTokenRequest(Func onBeforeTokenRequestHandler)
+ {
+ ValidateUseOfExperimentalFeature("OnBeforeTokenRequest");
+
+ CommonParameters.OnBeforeTokenRequestHandler = onBeforeTokenRequestHandler;
+ return this;
+ }
+
///
internal override Task ExecuteInternalAsync(CancellationToken cancellationToken)
{
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs
index 87cbbdc278..921f6d426d 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs
@@ -2,13 +2,16 @@
// Licensed under the MIT License.
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Parameters;
+using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Instance.Discovery;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Internal.Requests;
-using Microsoft.Identity.Client.ManagedIdentity;
+using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
using Microsoft.Identity.Client.Utils;
namespace Microsoft.Identity.Client.ApiConfig.Executors
@@ -20,7 +23,7 @@ internal class ManagedIdentityExecutor : AbstractExecutor, IManagedIdentityAppli
{
private readonly ManagedIdentityApplication _managedIdentityApplication;
- public ManagedIdentityExecutor(IServiceBundle serviceBundle, ManagedIdentityApplication managedIdentityApplication)
+ public ManagedIdentityExecutor(IServiceBundle serviceBundle, ManagedIdentityApplication managedIdentityApplication)
: base(serviceBundle)
{
ClientApplicationBase.GuardMobileFrameworks();
@@ -40,14 +43,21 @@ public async Task ExecuteAsync(
requestContext,
_managedIdentityApplication.AppTokenCacheInternal).ConfigureAwait(false);
- var handler = new ManagedIdentityAuthRequest(
+ // MSI factory logic - decide if we need to use the legacy or the new MSI flow
+ RequestBase handler = null;
+
+ // May or may not be initialized, depending on the state of the Azure resource
+ handler = SlcManagedIdentityAuthRequest.TryCreate(
ServiceBundle,
requestParams,
managedIdentityParameters);
+ handler ??= new LegacyManagedIdentityAuthRequest(
+ ServiceBundle,
+ requestParams,
+ managedIdentityParameters);
+
return await handler.RunAsync(cancellationToken).ConfigureAwait(false);
}
-
-
}
}
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
index 79e594e030..2e2461fb00 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Client.AuthScheme;
@@ -27,6 +28,7 @@ internal class AcquireTokenCommonParameters
public IDictionary ExtraHttpHeaders { get; set; }
public PoPAuthenticationConfiguration PopAuthenticationConfiguration { get; set; }
public Func OnBeforeTokenRequestHandler { get; internal set; }
+ public X509Certificate2 MtlsCertificate { get; internal set; }
}
}
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
index d18476ac3f..db00dbc091 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
@@ -119,13 +119,19 @@ public string ClientVersion
public bool RetryOnServerErrors { get; set; } = true;
+#region ManagedIdentity
public ManagedIdentityId ManagedIdentityId { get; internal set; }
-
public bool IsManagedIdentity { get; }
+
+ public CryptoKeyType ManagedIdentityCredentialKeyType { get; internal set; }
+
+ public X509Certificate2 ManagedIdentityClientCertificate { get; internal set; }
+
+ #endregion
+
public bool IsConfidentialClient { get; }
public bool IsPublicClient => !IsConfidentialClient && !IsManagedIdentity;
-
public Func> AppTokenProvider;
#region ClientCredentials
@@ -205,7 +211,8 @@ public X509Certificate2 ClientCredentialCertificate
public ITokenCacheInternal UserTokenCacheInternalForTest { get; set; }
public ITokenCacheInternal AppTokenCacheInternalForTest { get; set; }
- public IDeviceAuthManager DeviceAuthManagerForTest { get; set; }
+ public IDeviceAuthManager DeviceAuthManagerForTest { get; set; }
+ public IKeyMaterialManager KeyMaterialManagerForTest { get; set; }
public bool IsInstanceDiscoveryEnabled { get; internal set; } = true;
#endregion
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/IMsalMtlsHttpClientFactory.cs b/src/client/Microsoft.Identity.Client/AppConfig/IMsalMtlsHttpClientFactory.cs
new file mode 100644
index 0000000000..be61953d79
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/AppConfig/IMsalMtlsHttpClientFactory.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.Identity.Client
+{
+ ///
+ /// Internal factory responsible for creating HttpClient instances configured for mutual TLS (MTLS).
+ /// This factory is specifically intended for use within the MSAL library for secure communication with Azure AD using MTLS.
+ /// For more details on HttpClient instancing, see https://learn.microsoft.com/dotnet/api/system.net.http.httpclient?view=net-7.0#instancing.
+ ///
+ ///
+ /// Implementations of this interface must be thread-safe.
+ /// It is important to reuse HttpClient instances to avoid socket exhaustion.
+ /// Do not create a new HttpClient for each call to .
+ /// If your application requires Integrated Windows Authentication, set to true.
+ /// This interface is intended for internal use by MSAL only and is designed to support MTLS scenarios.
+ ///
+ internal interface IMsalMtlsHttpClientFactory : IMsalHttpClientFactory
+ {
+ ///
+ /// Returns an HttpClient configured with a certificate for mutual TLS authentication.
+ /// This method enables advanced MTLS scenarios within Azure AD communications in MSAL.
+ ///
+ /// The certificate to be used for MTLS authentication.
+ /// An HttpClient instance configured with the specified certificate.
+ HttpClient GetHttpClient(X509Certificate2 x509Certificate2);
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs
index e015211982..b5417251ca 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs
@@ -100,6 +100,28 @@ public ManagedIdentityApplicationBuilder WithTelemetryClient(params ITelemetryCl
return this;
}
+ ///
+ /// 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"
+ ///
+ ///
+ /// MSAL will transform these into special claims request. See https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter for
+ /// details on claim requests. This is an experimental API. The method signature may change in the future
+ /// without involving a major version upgrade.
+ /// For more details see https://aka.ms/msal-net-claims-request
+ ///
+ public ManagedIdentityApplicationBuilder WithClientCapabilities(IEnumerable clientCapabilities)
+ {
+ ValidateUseOfExperimentalFeature();
+
+ if (clientCapabilities != null && clientCapabilities.Any())
+ {
+ Config.ClientCapabilities = clientCapabilities;
+ }
+
+ return this;
+ }
+
private void TelemetryClientLogMsalVersion()
{
if (Config.TelemetryClients.HasEnabledClients(TelemetryConstants.ConfigurationUpdateEventName))
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ManagedIdentityApplicationExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/ManagedIdentityApplicationExtensions.cs
new file mode 100644
index 0000000000..8d9b2de59b
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ManagedIdentityApplicationExtensions.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Identity.Client
+{
+ ///
+ /// Extensibility methods for
+ ///
+ public static class ManagedIdentityApplicationExtensions
+ {
+ ///
+ /// Used to determine if managed identity is able to handle claims.
+ ///
+ /// Boolean indicating if Claims is supported
+ public static bool IsClaimsSupportedByClient(this IManagedIdentityApplication app)
+ {
+ if (app is ManagedIdentityApplication mia)
+ {
+ return mia.IsClaimsSupportedByClient();
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs
index 7567df72c3..f98aca7ce0 100644
--- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs
+++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs
@@ -8,11 +8,10 @@
using System.IO;
using System.Net;
using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;
-using Microsoft.Identity.Client.Internal.Requests;
-using Microsoft.Identity.Client.Utils;
namespace Microsoft.Identity.Client.Http
{
@@ -27,123 +26,38 @@ namespace Microsoft.Identity.Client.Http
internal class HttpManager : IHttpManager
{
protected readonly IMsalHttpClientFactory _httpClientFactory;
+ private readonly Func _retryCondition;
public long LastRequestDurationInMs { get; private set; }
- public HttpManager(IMsalHttpClientFactory httpClientFactory)
- {
- _httpClientFactory = httpClientFactory ??
- throw new ArgumentNullException(nameof(httpClientFactory));
- }
-
- protected virtual HttpClient GetHttpClient()
- {
- return _httpClientFactory.GetHttpClient();
- }
-
- public async Task SendPostAsync(
- Uri endpoint,
- IDictionary headers,
- IDictionary bodyParameters,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default)
- {
- HttpContent body = bodyParameters == null ? null : new FormUrlEncodedContent(bodyParameters);
- return await SendPostAsync(endpoint, headers, body, logger, cancellationToken).ConfigureAwait(false);
- }
-
- public virtual Task SendPostAsync(
- Uri endpoint,
- IDictionary headers,
- HttpContent body,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default)
- {
- return SendRequestAsync(endpoint, headers, body, HttpMethod.Post, logger, cancellationToken: cancellationToken);
- }
-
- public virtual Task SendGetAsync(
- Uri endpoint,
- IDictionary headers,
- ILoggerAdapter logger,
- bool retry = true,
- CancellationToken cancellationToken = default)
- {
- return SendRequestAsync(endpoint, headers, null, HttpMethod.Get, logger, cancellationToken: cancellationToken);
- }
-
///
- /// Performs the GET request just like
- /// but does not throw a ServiceUnavailable service exception. Instead, it returns the associated
- /// with the request.
+ /// A new instance of the HTTP manager with a retry *condition*. The retry policy hardcodes:
+ /// - the number of retries (1)
+ /// - the delay between retries (1 second)
///
- public virtual Task SendGetForceResponseAsync(
- Uri endpoint,
- IDictionary headers,
- ILoggerAdapter logger,
- bool retry = true,
- CancellationToken cancellationToken = default)
- {
- return SendRequestAsync(endpoint, headers, null, HttpMethod.Get, logger, doNotThrow: true, cancellationToken: cancellationToken);
- }
-
- ///
- /// Performs the POST request just like
- /// but does not throw a ServiceUnavailable service exception. Instead, it returns the associated
- /// with the request.
- ///
- public virtual Task SendPostForceResponseAsync(
- Uri uri,
- IDictionary headers,
- IDictionary bodyParameters,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default)
+ public HttpManager(
+ IMsalHttpClientFactory httpClientFactory,
+ Func retryCondition)
{
- HttpContent body = bodyParameters == null ? null : new FormUrlEncodedContent(bodyParameters);
- return SendRequestAsync(uri, headers, body, HttpMethod.Post, logger, doNotThrow: true, cancellationToken: cancellationToken);
- }
-
- ///
- /// Performs the POST request just like
- /// but does not throw a ServiceUnavailable service exception. Instead, it returns the associated
- /// with the request.
- ///
- public virtual Task SendPostForceResponseAsync(
- Uri uri,
- IDictionary headers,
- StringContent body,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default)
- {
- return SendRequestAsync(uri, headers, body, HttpMethod.Post, logger, doNotThrow: true, cancellationToken: cancellationToken);
- }
-
- private static HttpRequestMessage CreateRequestMessage(Uri endpoint, IDictionary headers)
- {
- HttpRequestMessage requestMessage = new HttpRequestMessage { RequestUri = endpoint };
- requestMessage.Headers.Accept.Clear();
- if (headers != null)
- {
- foreach (KeyValuePair kvp in headers)
- {
- requestMessage.Headers.Add(kvp.Key, kvp.Value);
- }
- }
-
- return requestMessage;
+ _httpClientFactory = httpClientFactory ??
+ throw new ArgumentNullException(nameof(httpClientFactory));
+ _retryCondition = retryCondition;
}
- protected virtual async Task SendRequestAsync(
+ public async Task SendRequestAsync(
Uri endpoint,
- IDictionary headers,
+ Dictionary headers,
HttpContent body,
HttpMethod method,
ILoggerAdapter logger,
- bool doNotThrow = false,
- bool retry = false,
- CancellationToken cancellationToken = default)
+ bool doNotThrow,
+ bool retry,
+ X509Certificate2 bindingCertificate,
+ CancellationToken cancellationToken)
{
+ Exception timeoutException = null;
HttpResponse response = null;
-
+ bool isRetriable = false;
+
try
{
HttpContent clonedBody = body;
@@ -156,7 +70,14 @@ protected virtual async Task SendRequestAsync(
using (logger.LogBlockDuration("[HttpManager] ExecuteAsync"))
{
- response = await ExecuteAsync(endpoint, headers, clonedBody, method, logger, cancellationToken).ConfigureAwait(false);
+ response = await ExecuteAsync(
+ endpoint,
+ headers,
+ clonedBody,
+ method,
+ bindingCertificate,
+ logger,
+ cancellationToken).ConfigureAwait(false);
}
if (response.StatusCode == HttpStatusCode.OK)
@@ -167,20 +88,45 @@ protected virtual async Task SendRequestAsync(
logger.Info(() => string.Format(CultureInfo.InvariantCulture,
MsalErrorMessage.HttpRequestUnsuccessful,
(int)response.StatusCode, response.StatusCode));
+
+ isRetriable = _retryCondition(response);
}
catch (TaskCanceledException exception)
{
if (cancellationToken.IsCancellationRequested)
{
- logger.Info("The HTTP request was cancelled. ");
+ logger.Info("The HTTP request was canceled. ");
throw;
}
logger.Error("The HTTP request failed. " + exception.Message);
+ isRetriable = true;
+ timeoutException = exception;
+ }
+
+ if (isRetriable && retry)
+ {
+ logger.Warning("Retry condition met. Retrying 1 time after waiting 1 second.");
+ await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
+ return await SendRequestAsync(
+ endpoint,
+ headers,
+ body,
+ method,
+ logger,
+ doNotThrow,
+ retry: false, // retry just once
+ bindingCertificate,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ logger.Warning("Request retry failed.");
+ if (timeoutException != null)
+ {
throw new MsalServiceException(
MsalError.RequestTimeout,
"Request to the endpoint timed out.",
- exception);
+ timeoutException);
}
if (doNotThrow)
@@ -189,7 +135,7 @@ protected virtual async Task SendRequestAsync(
}
// package 500 errors in a "service not available" exception
- if (IsRetryableStatusCode((int)response.StatusCode))
+ if ((int)response.StatusCode >= 500 && (int)response.StatusCode < 600)
{
throw MsalServiceExceptionFactory.FromHttpResponse(
MsalError.ServiceNotAvailable,
@@ -200,11 +146,39 @@ protected virtual async Task SendRequestAsync(
return response;
}
- protected async Task ExecuteAsync(
+ private HttpClient GetHttpClient(X509Certificate2 x509Certificate2)
+ {
+ if (_httpClientFactory is IMsalMtlsHttpClientFactory msalMtlsHttpClientFactory)
+ {
+ // If the factory is an IMsalMtlsHttpClientFactory, use it to get an HttpClient with the certificate
+ return msalMtlsHttpClientFactory.GetHttpClient(x509Certificate2);
+ }
+
+ // If the factory is not an IMsalMtlsHttpClientFactory, use it to get a default HttpClient
+ return _httpClientFactory.GetHttpClient();
+ }
+
+ private static HttpRequestMessage CreateRequestMessage(Uri endpoint, IDictionary headers)
+ {
+ HttpRequestMessage requestMessage = new HttpRequestMessage { RequestUri = endpoint };
+ requestMessage.Headers.Accept.Clear();
+ if (headers != null)
+ {
+ foreach (KeyValuePair kvp in headers)
+ {
+ requestMessage.Headers.Add(kvp.Key, kvp.Value);
+ }
+ }
+
+ return requestMessage;
+ }
+
+ private async Task ExecuteAsync(
Uri endpoint,
IDictionary headers,
HttpContent body,
HttpMethod method,
+ X509Certificate2 bindingCertificate,
ILoggerAdapter logger,
CancellationToken cancellationToken = default)
{
@@ -214,19 +188,18 @@ protected async Task ExecuteAsync(
requestMessage.Content = body;
logger.VerbosePii(
- () => $"[HttpManager] Sending request. Method: {method}. URI: {(endpoint == null ? "NULL" : $"{endpoint.Scheme}://{endpoint.Authority}{endpoint.AbsolutePath}")}. ",
- () => $"[HttpManager] Sending request. Method: {method}. Host: {(endpoint == null ? "NULL" : $"{endpoint.Scheme}://{endpoint.Authority}")}. ");
+ () => $"[HttpManager] Sending request. Method: {method}. URI: {(endpoint == null ? "NULL" : $"{endpoint.Scheme}://{endpoint.Authority}{endpoint.AbsolutePath}")}. Binding Certificate: {bindingCertificate != null}. Endpoint: {endpoint} ",
+ () => $"[HttpManager] Sending request. Method: {method}. Host: {(endpoint == null ? "NULL" : $"{endpoint.Scheme}://{endpoint.Authority}")}. Binding Certificate: {bindingCertificate != null} ");
- var measureDurationResult = await StopwatchService.MeasureCodeBlockAsync(async () =>
- {
- HttpClient client = GetHttpClient();
- return await client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
- }).ConfigureAwait(false);
+ Stopwatch sw = Stopwatch.StartNew();
- using (HttpResponseMessage responseMessage = measureDurationResult.Result)
+ HttpClient client = GetHttpClient(bindingCertificate);
+
+ using (HttpResponseMessage responseMessage =
+ await client.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false))
{
- LastRequestDurationInMs = measureDurationResult.Milliseconds;
- logger.Verbose(()=>$"[HttpManager] Received response. Status code: {responseMessage.StatusCode}. ");
+ LastRequestDurationInMs = sw.ElapsedMilliseconds;
+ logger.Verbose(() => $"[HttpManager] Received response. Status code: {responseMessage.StatusCode}. ");
HttpResponse returnValue = await CreateResponseAsync(responseMessage).ConfigureAwait(false);
returnValue.UserAgent = requestMessage.Headers.UserAgent.ToString();
@@ -235,19 +208,6 @@ protected async Task ExecuteAsync(
}
}
- internal /* internal for test only */ static async Task CreateResponseAsync(HttpResponseMessage response)
- {
- var body = response.Content == null
- ? null
- : await response.Content.ReadAsStringAsync().ConfigureAwait(false);
- return new HttpResponse
- {
- Headers = response.Headers,
- Body = body,
- StatusCode = response.StatusCode
- };
- }
-
protected static async Task CloneHttpContentAsync(HttpContent httpContent)
{
var temp = new MemoryStream();
@@ -265,13 +225,20 @@ protected static async Task CloneHttpContentAsync(HttpContent httpC
return clone;
}
- ///
- /// In HttpManager, the retry policy is based on this simple condition.
- /// Avoid changing this, as it's breaking change.
- ///
- protected virtual bool IsRetryableStatusCode(int statusCode)
+ #region Helpers
+ internal /* internal for test only */ static async Task CreateResponseAsync(HttpResponseMessage response)
{
- return statusCode >= 500 && statusCode < 600;
+ var body = response.Content == null
+ ? null
+ : await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ return new HttpResponse
+ {
+ Headers = response.Headers,
+ Body = body,
+ StatusCode = response.StatusCode
+ };
}
+
+ #endregion
}
}
diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs
index fe0fe4e048..652ed8abec 100644
--- a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs
+++ b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs
@@ -14,16 +14,19 @@ namespace Microsoft.Identity.Client.Http
///
internal sealed class HttpManagerFactory
{
- public static IHttpManager GetHttpManager(IMsalHttpClientFactory httpClientFactory, bool withRetry, bool isManagedIdentity)
+ public static IHttpManager GetHttpManager(
+ IMsalHttpClientFactory httpClientFactory,
+ bool withRetry,
+ bool isManagedIdentity)
{
if (!withRetry)
{
- return new HttpManager(httpClientFactory);
+ return new HttpManager(httpClientFactory, HttpRetryConditions.NoRetry);
}
return isManagedIdentity ?
- new HttpManagerManagedIdentity(httpClientFactory) :
- new HttpManagerWithRetry(httpClientFactory);
+ new HttpManager(httpClientFactory, HttpRetryConditions.ManagedIdentity) :
+ new HttpManager(httpClientFactory, HttpRetryConditions.Sts);
}
}
}
diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerManagedIdentity.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerManagedIdentity.cs
deleted file mode 100644
index 6bbcef7148..0000000000
--- a/src/client/Microsoft.Identity.Client/Http/HttpManagerManagedIdentity.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Identity.Client.Core;
-
-namespace Microsoft.Identity.Client.Http
-{
- ///
- /// HTTP Manager specific to managed identity to implement the retry for specific HTTP status codes.
- ///
- internal class HttpManagerManagedIdentity : HttpManagerWithRetry
- {
- public HttpManagerManagedIdentity(IMsalHttpClientFactory httpClientFactory) :
- base(httpClientFactory) { }
-
- ///
- /// Retry policy specific to managed identity flow.
- /// Avoid changing this, as it's breaking change.
- ///
- protected override bool IsRetryableStatusCode(int statusCode)
- {
- switch (statusCode)
- {
- case 404: //Not Found
- case 408: // Request Timeout
- case 429: // Too Many Requests
- case 500: // Internal Server Error
- case 503: // Service Unavailable
- case 504: // Gateway Timeout
- return true;
- default:
- return false;
- }
- }
- }
-}
diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerWithRetry.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerWithRetry.cs
deleted file mode 100644
index 3a18551cb7..0000000000
--- a/src/client/Microsoft.Identity.Client/Http/HttpManagerWithRetry.cs
+++ /dev/null
@@ -1,195 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Identity.Client.Core;
-
-namespace Microsoft.Identity.Client.Http
-{
- ///
- /// We invoke this class from different threads and they all use the same HttpClient.
- /// To prevent race conditions, make sure you do not get / set anything on HttpClient itself,
- /// instead rely on HttpRequest objects which are thread specific.
- ///
- /// In particular, do not change any properties on HttpClient such as BaseAddress, buffer sizes and Timeout. You should
- /// also not access DefaultRequestHeaders because the getters are not thread safe (use HttpRequestMessage.Headers instead).
- ///
- internal class HttpManagerWithRetry : HttpManager
- {
-
- public HttpManagerWithRetry(IMsalHttpClientFactory httpClientFactory) :
- base(httpClientFactory) { }
-
- ///
- public override Task SendPostAsync(
- Uri endpoint,
- IDictionary headers,
- HttpContent body,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default)
- {
- return SendRequestAsync(endpoint, headers, body, HttpMethod.Post, logger, retry: true, cancellationToken: cancellationToken);
- }
-
- ///
- public override Task SendGetAsync(
- Uri endpoint,
- IDictionary headers,
- ILoggerAdapter logger,
- bool retry = true,
- CancellationToken cancellationToken = default)
- {
- return SendRequestAsync(endpoint, headers, null, HttpMethod.Get, logger, retry: true, cancellationToken: cancellationToken);
- }
-
- ///
- public override Task SendGetForceResponseAsync(
- Uri endpoint,
- IDictionary headers,
- ILoggerAdapter logger,
- bool retry = true,
- CancellationToken cancellationToken = default)
- {
- return SendRequestAsync(endpoint, headers, null, HttpMethod.Get, logger, retry: true, doNotThrow: true, cancellationToken: cancellationToken);
- }
-
- ///
- public override Task SendPostForceResponseAsync(
- Uri uri,
- IDictionary headers,
- IDictionary bodyParameters,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default)
- {
- HttpContent body = bodyParameters == null ? null : new FormUrlEncodedContent(bodyParameters);
- return SendRequestAsync(uri, headers, body, HttpMethod.Post, logger, retry: true, doNotThrow: true, cancellationToken: cancellationToken);
- }
-
- ///
- public override Task SendPostForceResponseAsync(
- Uri uri,
- IDictionary headers,
- StringContent body,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default)
- {
- return SendRequestAsync(uri, headers, body, HttpMethod.Post, logger, retry: true, doNotThrow: true, cancellationToken: cancellationToken);
- }
-
- protected override HttpClient GetHttpClient()
- {
- return _httpClientFactory.GetHttpClient();
- }
-
- protected override async Task SendRequestAsync(
- Uri endpoint,
- IDictionary headers,
- HttpContent body,
- HttpMethod method,
- ILoggerAdapter logger,
- bool doNotThrow = false,
- bool retry = true,
- CancellationToken cancellationToken = default)
- {
- Exception timeoutException = null;
- bool isRetriableStatusCode = false;
- HttpResponse response = null;
- bool isRetriable;
-
- try
- {
- HttpContent clonedBody = body;
- if (body != null)
- {
- // Since HttpContent would be disposed by underlying client.SendAsync(),
- // we duplicate it so that we will have a copy in case we would need to retry
- clonedBody = await CloneHttpContentAsync(body).ConfigureAwait(false);
- }
-
- using (logger.LogBlockDuration("[HttpManager] ExecuteAsync"))
- {
- response = await ExecuteAsync(endpoint, headers, clonedBody, method, logger, cancellationToken).ConfigureAwait(false);
- }
-
- if (response.StatusCode == HttpStatusCode.OK)
- {
- return response;
- }
-
- logger.Info(() => string.Format(CultureInfo.InvariantCulture,
- MsalErrorMessage.HttpRequestUnsuccessful,
- (int)response.StatusCode, response.StatusCode));
-
- isRetriableStatusCode = IsRetryableStatusCode((int)response.StatusCode);
- isRetriable = isRetriableStatusCode && !HasRetryAfterHeader(response);
- }
- catch (TaskCanceledException exception)
- {
- if (cancellationToken.IsCancellationRequested)
- {
- logger.Info("The HTTP request was cancelled. ");
- throw;
- }
-
- logger.Error("The HTTP request failed. " + exception.Message);
- isRetriable = true;
- timeoutException = exception;
- }
-
- if (isRetriable && retry)
- {
- logger.Info("Retrying one more time..");
- await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
- return await SendRequestAsync(
- endpoint,
- headers,
- body,
- method,
- logger,
- doNotThrow,
- retry: false,
- cancellationToken: cancellationToken).ConfigureAwait(false);
- }
-
- logger.Warning("Request retry failed.");
- if (timeoutException != null)
- {
- throw new MsalServiceException(
- MsalError.RequestTimeout,
- "Request to the endpoint timed out.",
- timeoutException);
- }
-
- if (doNotThrow)
- {
- return response;
- }
-
- // package 500 errors in a "service not available" exception
- if (isRetriableStatusCode)
- {
- throw MsalServiceExceptionFactory.FromHttpResponse(
- MsalError.ServiceNotAvailable,
- "Service is unavailable to process the request",
- response);
- }
-
- return response;
- }
-
- private static bool HasRetryAfterHeader(HttpResponse response)
- {
- var retryAfter = response?.Headers?.RetryAfter;
- return retryAfter != null &&
- (retryAfter.Delta.HasValue || retryAfter.Date.HasValue);
- }
- }
-}
diff --git a/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs b/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs
new file mode 100644
index 0000000000..0ff16cd4bf
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Net;
+
+namespace Microsoft.Identity.Client.Http
+{
+ internal static class HttpRetryConditions
+ {
+ public static bool NoRetry(HttpResponse response)
+ {
+ return false;
+ }
+
+ ///
+ /// Retry policy specific to managed identity flow.
+ /// Avoid changing this, as it's breaking change.
+ ///
+ public static bool ManagedIdentity(HttpResponse response)
+ {
+ return (int)response.StatusCode switch
+ {
+ //Not Found
+ 404 or 408 or 429 or 500 or 503 or 504 => true,
+ _ => false,
+ };
+ }
+
+ ///
+ /// Retry condition for /token and /authorize endpoints
+ ///
+ ///
+ ///
+ public static bool Sts(HttpResponse response)
+ {
+ var retryAfter = response?.Headers?.RetryAfter;
+ bool hasRetryAfterHeader = retryAfter != null &&
+ (retryAfter.Delta.HasValue || retryAfter.Date.HasValue);
+
+ // Don't retry if the STS told us to back off
+ if (hasRetryAfterHeader)
+ return false;
+
+ int statusCode = (int)response.StatusCode;
+
+ return statusCode >= 500 && statusCode < 600;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs b/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs
index 3dc2cc58b5..eff685d866 100644
--- a/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs
+++ b/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;
@@ -14,46 +15,15 @@ internal interface IHttpManager
{
long LastRequestDurationInMs { get; }
- Task SendPostAsync(
- Uri endpoint,
- IDictionary headers,
- IDictionary bodyParameters,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default);
-
- Task SendPostAsync(
- Uri endpoint,
- IDictionary headers,
- HttpContent body,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default);
-
- Task SendGetAsync(
- Uri endpoint,
- IDictionary headers,
- ILoggerAdapter logger,
- bool retry = true,
- CancellationToken cancellationToken = default);
-
- Task SendPostForceResponseAsync(
- Uri uri,
- IDictionary headers,
- StringContent body,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default);
-
- Task SendPostForceResponseAsync(
- Uri uri,
- IDictionary headers,
- IDictionary bodyParameters,
- ILoggerAdapter logger,
- CancellationToken cancellationToken = default);
-
- Task SendGetForceResponseAsync(
- Uri endpoint,
- IDictionary headers,
- ILoggerAdapter logger,
- bool retry = true,
- CancellationToken cancellationToken = default);
+ Task SendRequestAsync(
+ Uri endpoint,
+ Dictionary headers,
+ HttpContent body,
+ HttpMethod method,
+ ILoggerAdapter logger,
+ bool doNotThrow,
+ bool retry,
+ X509Certificate2 mtlsCertificate,
+ CancellationToken cancellationToken);
}
}
diff --git a/src/client/Microsoft.Identity.Client/Instance/Discovery/NetworkMetadataProvider.cs b/src/client/Microsoft.Identity.Client/Instance/Discovery/NetworkMetadataProvider.cs
index 1fbebf5b35..7c5d36d513 100644
--- a/src/client/Microsoft.Identity.Client/Instance/Discovery/NetworkMetadataProvider.cs
+++ b/src/client/Microsoft.Identity.Client/Instance/Discovery/NetworkMetadataProvider.cs
@@ -24,7 +24,7 @@ internal class NetworkMetadataProvider : INetworkMetadataProvider
public NetworkMetadataProvider(
IHttpManager httpManager,
- INetworkCacheMetadataProvider networkCacheMetadataProvider,
+ INetworkCacheMetadataProvider networkCacheMetadataProvider,
Uri userProvidedInstanceDiscoveryUri = null)
{
_httpManager = httpManager ?? throw new ArgumentNullException(nameof(httpManager));
@@ -34,17 +34,17 @@ public NetworkMetadataProvider(
public async Task GetMetadataAsync(Uri authority, RequestContext requestContext)
{
- var logger = requestContext.Logger;
+ ILoggerAdapter logger = requestContext.Logger;
string environment = authority.Host;
- var cachedEntry = _networkCacheMetadataProvider.GetMetadata(environment, logger);
+ InstanceDiscoveryMetadataEntry cachedEntry = _networkCacheMetadataProvider.GetMetadata(environment, logger);
if (cachedEntry != null)
{
logger.Verbose(() => $"[Instance Discovery] The network provider found an entry for {environment}. ");
return cachedEntry;
}
- var discoveryResponse = await FetchAllDiscoveryMetadataAsync(authority, requestContext).ConfigureAwait(false);
+ InstanceDiscoveryResponse discoveryResponse = await FetchAllDiscoveryMetadataAsync(authority, requestContext).ConfigureAwait(false);
CacheInstanceDiscoveryMetadata(discoveryResponse);
cachedEntry = _networkCacheMetadataProvider.GetMetadata(environment, logger);
@@ -76,7 +76,7 @@ private async Task SendInstanceDiscoveryRequestAsync(
Uri authority,
RequestContext requestContext)
{
- var client = new OAuth2Client(requestContext.Logger, _httpManager);
+ var client = new OAuth2Client(requestContext.Logger, _httpManager, mtlsCertificate: null);
client.AddQueryParameter("api-version", "1.1");
client.AddQueryParameter("authorization_endpoint", BuildAuthorizeEndpoint(authority));
@@ -101,8 +101,11 @@ private Uri ComputeHttpEndpoint(Uri authority, RequestContext requestContext)
authority.Host :
AadAuthority.DefaultTrustedHost;
- string instanceDiscoveryEndpoint = UriBuilderExtensions.GetHttpsUriWithOptionalPort(
- $"https://{discoveryHost}/common/discovery/instance",
+ string instanceDiscoveryEndpoint = UriBuilderExtensions.GetHttpsUriWithOptionalPort(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "https://{0}/common/discovery/instance",
+ discoveryHost),
authority.Port);
requestContext.Logger.InfoPii(
diff --git a/src/client/Microsoft.Identity.Client/Instance/Oidc/OidcRetrieverWithCache.cs b/src/client/Microsoft.Identity.Client/Instance/Oidc/OidcRetrieverWithCache.cs
index 931dfeebcd..10fbedfd79 100644
--- a/src/client/Microsoft.Identity.Client/Instance/Oidc/OidcRetrieverWithCache.cs
+++ b/src/client/Microsoft.Identity.Client/Instance/Oidc/OidcRetrieverWithCache.cs
@@ -27,7 +27,7 @@ public static async Task GetOidcAsync(
}
await s_lockOidcRetrieval.WaitAsync().ConfigureAwait(false);
-
+
Uri oidcMetadataEndpoint = null;
try
{
@@ -44,7 +44,7 @@ public static async Task GetOidcAsync(
builder.Path = existingPath.TrimEnd('/') + "/" + Constants.WellKnownOpenIdConfigurationPath;
oidcMetadataEndpoint = builder.Uri;
- var client = new OAuth2Client(requestContext.Logger, requestContext.ServiceBundle.HttpManager);
+ var client = new OAuth2Client(requestContext.Logger, requestContext.ServiceBundle.HttpManager, null);
configuration = await client.DiscoverOidcMetadataAsync(oidcMetadataEndpoint, requestContext).ConfigureAwait(false);
s_cache[authority] = configuration;
diff --git a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs
index 13e80285e9..12197293cd 100644
--- a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs
+++ b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;
@@ -197,16 +198,34 @@ private async Task DiscoverAsync(ILoggerAdapter logger, Cancellation
Uri imdsUri = BuildImdsUri(DefaultApiVersion);
- HttpResponse response = await _httpManager.SendGetAsync(imdsUri, headers, logger, retry: false, cancellationToken: GetCancellationToken(requestCancellationToken))
- .ConfigureAwait(false);
+ HttpResponse response = await _httpManager.SendRequestAsync(
+ imdsUri,
+ headers,
+ body: null,
+ HttpMethod.Get,
+ logger: logger,
+ doNotThrow: false,
+ retry: false,
+ mtlsCertificate: null,
+ GetCancellationToken(requestCancellationToken))
+ .ConfigureAwait(false);
// A bad request occurs when the version in the IMDS call is no longer supported.
if (response.StatusCode == HttpStatusCode.BadRequest)
{
string apiVersion = await GetImdsUriApiVersionAsync(logger, headers, requestCancellationToken).ConfigureAwait(false); // Get the latest version
imdsUri = BuildImdsUri(apiVersion);
- response = await _httpManager.SendGetAsync(BuildImdsUri(apiVersion), headers, logger, retry: false, cancellationToken: GetCancellationToken(requestCancellationToken))
- .ConfigureAwait(false); // Call again with updated version
+ response = await _httpManager.SendRequestAsync(
+ imdsUri,
+ headers,
+ body: null,
+ HttpMethod.Get,
+ logger: logger,
+ doNotThrow: false,
+ retry: false,
+ mtlsCertificate: null,
+ GetCancellationToken(requestCancellationToken))
+ .ConfigureAwait(false); // Call again with updated version
}
if (response.StatusCode == HttpStatusCode.OK && !response.Body.IsNullOrEmpty())
@@ -249,7 +268,6 @@ private async Task DiscoverAsync(ILoggerAdapter logger, Cancellation
s_failedAutoDiscovery = result.RegionSource == RegionAutodetectionSource.FailedAutoDiscovery;
s_autoDiscoveredRegion = result.Region;
s_regionDiscoveryDetails = result.RegionDetails;
-
_lockDiscover.Release();
}
@@ -297,9 +315,19 @@ private static bool ValidateRegion(string region, string source, ILoggerAdapter
private async Task GetImdsUriApiVersionAsync(ILoggerAdapter logger, Dictionary headers, CancellationToken userCancellationToken)
{
- Uri imdsErrorUri = new Uri(ImdsEndpoint);
-
- HttpResponse response = await _httpManager.SendGetAsync(imdsErrorUri, headers, logger, retry: false, cancellationToken: GetCancellationToken(userCancellationToken)).ConfigureAwait(false);
+ Uri imdsErrorUri = new(ImdsEndpoint);
+
+ HttpResponse response = await _httpManager.SendRequestAsync(
+ imdsErrorUri,
+ headers,
+ body: null,
+ HttpMethod.Get,
+ logger: logger,
+ doNotThrow: false,
+ retry: false,
+ mtlsCertificate: null,
+ GetCancellationToken(userCancellationToken))
+ .ConfigureAwait(false);
// When IMDS endpoint is called without the api version query param, bad request response comes back with latest version.
if (response.StatusCode == HttpStatusCode.BadRequest)
diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs
index ea9ff2117c..5814ab0f30 100644
--- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs
+++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs
@@ -29,11 +29,17 @@ public async Task ValidateAuthorityAsync(
var resource = $"https://{authorityInfo.Host}";
string webFingerUrl = Constants.FormatAdfsWebFingerUrl(authorityInfo.Host, resource);
- Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendGetAsync(
- new Uri(webFingerUrl),
- null,
- _requestContext.Logger,
- cancellationToken: _requestContext.UserCancellationToken).ConfigureAwait(false);
+ Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync(
+ new Uri(webFingerUrl),
+ null,
+ body: null,
+ System.Net.Http.HttpMethod.Get,
+ logger: _requestContext.Logger,
+ doNotThrow: false,
+ retry: true,
+ mtlsCertificate: null,
+ _requestContext.UserCancellationToken)
+ .ConfigureAwait(false);
if (httpResponse.StatusCode != HttpStatusCode.OK)
{
diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
index d3f482aa75..2600912d69 100644
--- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
@@ -1,10 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-using System;
using System.Collections.Generic;
-using System.Runtime.ConstrainedExecution;
-using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
@@ -20,29 +17,26 @@ internal class CertificateAndClaimsClientCredential : IClientCredential
{
private readonly IDictionary _claimsToSign;
private readonly bool _appendDefaultClaims;
-
+ private readonly string _base64EncodedThumbprint; // x5t
public X509Certificate2 Certificate { get; }
public AssertionType AssertionType => AssertionType.CertificateWithoutSni;
- public CertificateAndClaimsClientCredential(
- X509Certificate2 certificate,
- IDictionary claimsToSign,
- bool appendDefaultClaims)
+ public CertificateAndClaimsClientCredential(X509Certificate2 certificate, IDictionary claimsToSign, bool appendDefaultClaims)
{
Certificate = certificate;
_claimsToSign = claimsToSign;
_appendDefaultClaims = appendDefaultClaims;
-
+ _base64EncodedThumbprint = Base64UrlHelpers.Encode(certificate.GetCertHash());
}
public Task AddConfidentialClientParametersAsync(
- OAuth2Client oAuth2Client,
- ILoggerAdapter logger,
- ICryptographyManager cryptographyManager,
- string clientId,
- string tokenEndpoint,
- bool sendX5C,
+ OAuth2Client oAuth2Client,
+ ILoggerAdapter logger,
+ ICryptographyManager cryptographyManager,
+ string clientId,
+ string tokenEndpoint,
+ bool sendX5C,
bool useSha2AndPss,
CancellationToken cancellationToken)
{
diff --git a/src/client/Microsoft.Identity.Client/Internal/Constants.cs b/src/client/Microsoft.Identity.Client/Internal/Constants.cs
index 226bf2c169..d243e20ef5 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Constants.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Constants.cs
@@ -43,6 +43,7 @@ internal static class Constants
public const string ManagedIdentityResourceId = "mi_res_id";
public const string ManagedIdentityDefaultClientId = "system_assigned_managed_identity";
public const string ManagedIdentityDefaultTenant = "managed_identity";
+ public const string CredentialEndpoint = "http://169.254.169.254/metadata/identity/credential?cred-api-version=1.0";
public const string CiamAuthorityHostSuffix = ".ciamlogin.com";
public static string FormatEnterpriseRegistrationOnPremiseUri(string domain)
diff --git a/src/client/Microsoft.Identity.Client/Internal/IServiceBundle.cs b/src/client/Microsoft.Identity.Client/Internal/IServiceBundle.cs
index 1dd1286aab..b26835c7fa 100644
--- a/src/client/Microsoft.Identity.Client/Internal/IServiceBundle.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/IServiceBundle.cs
@@ -26,8 +26,8 @@ internal interface IServiceBundle
IWsTrustWebRequestManager WsTrustWebRequestManager { get; }
IDeviceAuthManager DeviceAuthManager { get; }
IThrottlingProvider ThrottlingManager { get; }
-
IHttpTelemetryManager HttpTelemetryManager { get; }
+ IKeyMaterialManager KeyMaterialManager { get; }
#region Testing
void SetPlatformProxyForTest(IPlatformProxy platformProxy);
diff --git a/src/client/Microsoft.Identity.Client/Internal/MsalIdHelper.cs b/src/client/Microsoft.Identity.Client/Internal/MsalIdHelper.cs
index 532511030c..792a2ff6d9 100644
--- a/src/client/Microsoft.Identity.Client/Internal/MsalIdHelper.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/MsalIdHelper.cs
@@ -59,7 +59,7 @@ internal static class MsalIdHelper
return version[1];
});
- public static IDictionary GetMsalIdParameters(ILoggerAdapter logger)
+ public static Dictionary GetMsalIdParameters(ILoggerAdapter logger)
{
var platformProxy = PlatformProxyFactory.CreatePlatformProxy(logger);
if (platformProxy == null)
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs
index 4adb42a7e0..e9c624ab8a 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Parameters;
@@ -98,6 +99,8 @@ public AuthenticationRequestParameters(
public Guid CorrelationId => _commonParameters.CorrelationId;
+ public X509Certificate2 MtlsCertificate => _commonParameters.MtlsCertificate;
+
///
/// Indicates if the user configured claims via .WithClaims. Not affected by Client Capabilities
///
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/DeviceCodeRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/DeviceCodeRequest.cs
index 9602be7fbd..dd19fdd236 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/DeviceCodeRequest.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/DeviceCodeRequest.cs
@@ -29,7 +29,7 @@ protected override async Task ExecuteAsync(CancellationTok
{
await ResolveAuthorityAsync().ConfigureAwait(false);
- var client = new OAuth2Client(ServiceBundle.ApplicationLogger, ServiceBundle.HttpManager);
+ var client = new OAuth2Client(ServiceBundle.ApplicationLogger, ServiceBundle.HttpManager, null);
var deviceCodeScopes = new HashSet();
deviceCodeScopes.UnionWith(AuthenticationRequestParameters.Scope);
@@ -50,7 +50,7 @@ protected override async Task ExecuteAsync(CancellationTok
var response = await client.ExecuteRequestAsync(
builder.Uri,
HttpMethod.Post,
- AuthenticationRequestParameters.RequestContext,
+ AuthenticationRequestParameters.RequestContext,
// Normally AAD responds with an error HTTP code, but /devicecode endpoint sends errors on 200OK
expectErrorsOn200OK: true).ConfigureAwait(false);
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/LegacyManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/LegacyManagedIdentityAuthRequest.cs
new file mode 100644
index 0000000000..af511a0b11
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/LegacyManagedIdentityAuthRequest.cs
@@ -0,0 +1,99 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client.ApiConfig.Parameters;
+using Microsoft.Identity.Client.Cache.Items;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.ManagedIdentity;
+using Microsoft.Identity.Client.OAuth2;
+using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
+using Microsoft.Identity.Client.Utils;
+
+namespace Microsoft.Identity.Client.Internal.Requests
+{
+ ///
+ /// Old implementation of MSI, which works by getting tokens directly
+ /// from the managed identity token endpoints.
+ ///
+ internal class LegacyManagedIdentityAuthRequest
+ : ManagedIdentityAuthRequest
+ {
+ public LegacyManagedIdentityAuthRequest(
+ IServiceBundle serviceBundle,
+ AuthenticationRequestParameters authenticationRequestParameters,
+ AcquireTokenForManagedIdentityParameters managedIdentityParameters)
+ : base(serviceBundle, authenticationRequestParameters, managedIdentityParameters)
+ { }
+
+ protected override async Task GetAccessTokenAsync(
+ IKeyMaterialManager keyMaterial,
+ CancellationToken cancellationToken,
+ ILoggerAdapter logger)
+ {
+ AuthenticationResult authResult;
+ MsalAccessTokenCacheItem cachedAccessTokenItem = null;
+
+ // Requests to a managed identity endpoint must be throttled;
+ // otherwise, the endpoint will throw a HTTP 429.
+ logger.Verbose(() => "[ManagedIdentityRequest] Entering managed identity request semaphore.");
+ await s_semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
+ logger.Verbose(() => "[ManagedIdentityRequest] Entered managed identity request semaphore.");
+
+ try
+ {
+ // Bypass cache and send request to token endpoint, when
+ // 1. Force refresh is requested, or
+ // 2. Proactively Refreshed
+ if (_managedIdentityParameters.ForceRefresh ||
+ AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo == CacheRefreshReason.ProactivelyRefreshed)
+ {
+ authResult = await SendTokenRequestForManagedIdentityAsync(logger, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ logger.Info("[ManagedIdentityRequest] Checking for a cached access token.");
+ cachedAccessTokenItem = await GetCachedAccessTokenAsync().ConfigureAwait(false);
+
+ // Check the cache again after acquiring the semaphore in case the previous request cached a new token.
+ if (cachedAccessTokenItem != null)
+ {
+ authResult = CreateAuthenticationResultFromCache(cachedAccessTokenItem);
+ }
+ else
+ {
+ authResult = await SendTokenRequestForManagedIdentityAsync(logger, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ return authResult;
+ }
+ finally
+ {
+ s_semaphoreSlim.Release();
+ logger.Verbose(() => "[ManagedIdentityRequest] Released managed identity request semaphore.");
+ }
+ }
+
+ private async Task SendTokenRequestForManagedIdentityAsync(ILoggerAdapter logger, CancellationToken cancellationToken)
+ {
+ logger.Info("[ManagedIdentityRequest] Acquiring a token from the managed identity endpoint.");
+
+ await ResolveAuthorityAsync().ConfigureAwait(false);
+
+ ManagedIdentityClient managedIdentityClient = new(AuthenticationRequestParameters.RequestContext);
+
+ ManagedIdentityResponse managedIdentityResponse =
+ await managedIdentityClient
+ .SendTokenRequestForManagedIdentityAsync(_managedIdentityParameters, cancellationToken)
+ .ConfigureAwait(false);
+
+ var msalTokenResponse = MsalTokenResponse.CreateFromManagedIdentityResponse(managedIdentityResponse);
+ msalTokenResponse.Scope = AuthenticationRequestParameters.Scope.AsSingleString();
+
+ return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
index b0f3c57b21..ab6a7cee78 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ManagedIdentityAuthRequest.cs
@@ -2,24 +2,21 @@
// Licensed under the MIT License.
using System.Collections.Generic;
-using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Parameters;
using Microsoft.Identity.Client.Cache.Items;
using Microsoft.Identity.Client.Core;
-using Microsoft.Identity.Client.ManagedIdentity;
-using Microsoft.Identity.Client.OAuth2;
-using Microsoft.Identity.Client.Utils;
+using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
namespace Microsoft.Identity.Client.Internal.Requests
{
- internal class ManagedIdentityAuthRequest : RequestBase
+ internal abstract class ManagedIdentityAuthRequest : RequestBase
{
- private readonly AcquireTokenForManagedIdentityParameters _managedIdentityParameters;
- private static readonly SemaphoreSlim s_semaphoreSlim = new SemaphoreSlim(1, 1);
+ protected readonly AcquireTokenForManagedIdentityParameters _managedIdentityParameters;
+ protected static readonly SemaphoreSlim s_semaphoreSlim = new(1, 1);
- public ManagedIdentityAuthRequest(
+ protected ManagedIdentityAuthRequest(
IServiceBundle serviceBundle,
AuthenticationRequestParameters authenticationRequestParameters,
AcquireTokenForManagedIdentityParameters managedIdentityParameters)
@@ -30,46 +27,60 @@ public ManagedIdentityAuthRequest(
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
- AuthenticationResult authResult = null;
+ if (AuthenticationRequestParameters.Scope == null || AuthenticationRequestParameters.Scope.Count == 0)
+ {
+ throw new MsalClientException(
+ MsalError.ScopesRequired,
+ MsalErrorMessage.ScopesRequired);
+ }
+
ILoggerAdapter logger = AuthenticationRequestParameters.RequestContext.Logger;
+ AuthenticationResult authResult = null;
- // Skip checking cache when force refresh is specified
- if (_managedIdentityParameters.ForceRefresh)
+ IKeyMaterialManager keyMaterial = ServiceBundle.Config.KeyMaterialManagerForTest ??
+ AuthenticationRequestParameters.RequestContext.ServiceBundle.PlatformProxy.GetKeyMaterialManager();
+
+ // Skip checking cache for force refresh or when claims are present
+ if (_managedIdentityParameters.ForceRefresh || !string.IsNullOrEmpty(AuthenticationRequestParameters.Claims))
{
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ForceRefreshOrClaims;
- logger.Info("[ManagedIdentityRequest] Skipped looking for a cached access token because ForceRefresh was set.");
- authResult = await GetAccessTokenAsync(cancellationToken, logger).ConfigureAwait(false);
+ logger.Info("[ManagedIdentityRequest] Skipped looking for a cached access token because ForceRefresh or Claims was set.");
+
+ // Managed Identity Client Certificate check
+ if (ServiceBundle.Config.ManagedIdentityClientCertificate != null &&
+ ServiceBundle.Config.ManagedIdentityClientCertificate.Thumbprint != keyMaterial.BindingCertificate.Thumbprint)
+ {
+ logger.Info("[ManagedIdentityRequest] Managed Identity Client Certificate has been renewed.");
+ }
+
+ authResult = await GetAccessTokenAsync(keyMaterial, cancellationToken, logger).ConfigureAwait(false);
return authResult;
}
+ // Check cache for AT
MsalAccessTokenCacheItem cachedAccessTokenItem = await GetCachedAccessTokenAsync().ConfigureAwait(false);
- // No access token or cached access token needs to be refreshed
if (cachedAccessTokenItem != null)
{
authResult = CreateAuthenticationResultFromCache(cachedAccessTokenItem);
-
logger.Info("[ManagedIdentityRequest] Access token retrieved from cache.");
try
- {
+ {
var proactivelyRefresh = SilentRequestHelper.NeedsRefresh(cachedAccessTokenItem);
- // If needed, refreshes token in the background
+ // Proactive refresh logic
if (proactivelyRefresh)
{
- logger.Info("[ManagedIdentityRequest] Initiating a proactive refresh.");
-
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ProactivelyRefreshed;
SilentRequestHelper.ProcessFetchInBackground(
- cachedAccessTokenItem,
- () =>
- {
- // Use a linked token source, in case the original cancellation token source is disposed before this background task completes.
- using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- return GetAccessTokenAsync(tokenSource.Token, logger);
- }, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent.ApiId);
+ cachedAccessTokenItem,
+ () =>
+ {
+ using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ return GetAccessTokenAsync(keyMaterial, tokenSource.Token, logger);
+ }, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent.ApiId);
}
}
catch (MsalServiceException e)
@@ -79,88 +90,35 @@ protected override async Task ExecuteAsync(CancellationTok
}
else
{
- // No AT in the cache
+ // No AT in cache
if (AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo != CacheRefreshReason.Expired)
{
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.NoCachedAccessToken;
}
logger.Info("[ManagedIdentityRequest] No cached access token. Getting a token from the managed identity endpoint.");
- authResult = await GetAccessTokenAsync(cancellationToken, logger).ConfigureAwait(false);
+ authResult = await GetAccessTokenAsync(keyMaterial, cancellationToken, logger).ConfigureAwait(false);
}
return authResult;
}
- private async Task GetAccessTokenAsync(
- CancellationToken cancellationToken,
- ILoggerAdapter logger)
- {
- AuthenticationResult authResult;
- MsalAccessTokenCacheItem cachedAccessTokenItem = null;
-
- // Requests to a managed identity endpoint must be throttled;
- // otherwise, the endpoint will throw a HTTP 429.
- logger.Verbose(() => "[ManagedIdentityRequest] Entering managed identity request semaphore.");
- await s_semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
- logger.Verbose(() => "[ManagedIdentityRequest] Entered managed identity request semaphore.");
-
- try
- {
- // Bypass cache and send request to token endpoint, when
- // 1. Force refresh is requested, or
- // 2. If the access token needs to be refreshed proactively.
- if (_managedIdentityParameters.ForceRefresh ||
- AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo == CacheRefreshReason.ProactivelyRefreshed)
- {
- authResult = await SendTokenRequestForManagedIdentityAsync(logger, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- logger.Info("[ManagedIdentityRequest] Checking for a cached access token.");
- cachedAccessTokenItem = await GetCachedAccessTokenAsync().ConfigureAwait(false);
-
- // Check the cache again after acquiring the semaphore in case the previous request cached a new token.
- if (cachedAccessTokenItem != null)
- {
- authResult = CreateAuthenticationResultFromCache(cachedAccessTokenItem);
- }
- else
- {
- authResult = await SendTokenRequestForManagedIdentityAsync(logger, cancellationToken).ConfigureAwait(false);
- }
- }
-
- return authResult;
- }
- finally
- {
- s_semaphoreSlim.Release();
- logger.Verbose(() => "[ManagedIdentityRequest] Released managed identity request semaphore.");
- }
- }
-
- private async Task SendTokenRequestForManagedIdentityAsync(ILoggerAdapter logger, CancellationToken cancellationToken)
+ internal AuthenticationResult CreateAuthenticationResultFromCache(MsalAccessTokenCacheItem cachedAccessTokenItem)
{
- logger.Info("[ManagedIdentityRequest] Acquiring a token from the managed identity endpoint.");
-
- await ResolveAuthorityAsync().ConfigureAwait(false);
-
- ManagedIdentityClient managedIdentityClient =
- new ManagedIdentityClient(AuthenticationRequestParameters.RequestContext);
-
- ManagedIdentityResponse managedIdentityResponse =
- await managedIdentityClient
- .SendTokenRequestForManagedIdentityAsync(_managedIdentityParameters, cancellationToken)
- .ConfigureAwait(false);
-
- var msalTokenResponse = MsalTokenResponse.CreateFromManagedIdentityResponse(managedIdentityResponse);
- msalTokenResponse.Scope = AuthenticationRequestParameters.Scope.AsSingleString();
-
- return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false);
+ AuthenticationResult authResult = new AuthenticationResult(
+ cachedAccessTokenItem,
+ null,
+ AuthenticationRequestParameters.AuthenticationScheme,
+ AuthenticationRequestParameters.RequestContext.CorrelationId,
+ TokenSource.Cache,
+ AuthenticationRequestParameters.RequestContext.ApiEvent,
+ account: null,
+ spaAuthCode: null,
+ additionalResponseParameters: null);
+ return authResult;
}
- private async Task GetCachedAccessTokenAsync()
+ internal async Task GetCachedAccessTokenAsync()
{
MsalAccessTokenCacheItem cachedAccessTokenItem = await CacheManager.FindAccessTokenAsync().ConfigureAwait(false);
@@ -174,24 +132,18 @@ private async Task GetCachedAccessTokenAsync()
return null;
}
- private AuthenticationResult CreateAuthenticationResultFromCache(MsalAccessTokenCacheItem cachedAccessTokenItem)
- {
- AuthenticationResult authResult = new AuthenticationResult(
- cachedAccessTokenItem,
- null,
- AuthenticationRequestParameters.AuthenticationScheme,
- AuthenticationRequestParameters.RequestContext.CorrelationId,
- TokenSource.Cache,
- AuthenticationRequestParameters.RequestContext.ApiEvent,
- account: null,
- spaAuthCode: null,
- additionalResponseParameters: null);
- return authResult;
- }
+ protected abstract Task GetAccessTokenAsync(IKeyMaterialManager keyMaterial, CancellationToken cancellationToken, ILoggerAdapter logger);
protected override KeyValuePair? GetCcsHeader(IDictionary additionalBodyParameters)
{
return null;
}
+
+ // Override method to return a sorted set of scopes based on the input set.
+ protected override SortedSet GetOverriddenScopes(ISet inputScopes)
+ {
+ // Create a new SortedSet from the inputScopes to ensure a consistent and sorted order.
+ return new SortedSet(inputScopes);
+ }
}
}
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/SlcManagedIdentityAuthRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/SlcManagedIdentityAuthRequest.cs
new file mode 100644
index 0000000000..d0db899f9b
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/SlcManagedIdentityAuthRequest.cs
@@ -0,0 +1,310 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client.ApiConfig.Parameters;
+using Microsoft.Identity.Client.AppConfig;
+using Microsoft.Identity.Client.Cache.Items;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.Http;
+using Microsoft.Identity.Client.OAuth2;
+using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
+using Microsoft.Identity.Client.Utils;
+using Microsoft.Identity.Client.ManagedIdentity;
+using System.Linq;
+
+namespace Microsoft.Identity.Client.Internal.Requests
+{
+ internal class SlcManagedIdentityAuthRequest : ManagedIdentityAuthRequest
+ {
+ private readonly Uri _credentialEndpoint;
+
+ private SlcManagedIdentityAuthRequest(
+ IServiceBundle serviceBundle,
+ AuthenticationRequestParameters authenticationRequestParameters,
+ AcquireTokenForManagedIdentityParameters managedIdentityParameters,
+ Uri credentialEndpoint)
+ : base(serviceBundle, authenticationRequestParameters, managedIdentityParameters)
+ {
+ _credentialEndpoint = credentialEndpoint;
+ }
+
+ public static SlcManagedIdentityAuthRequest TryCreate(
+ IServiceBundle serviceBundle,
+ AuthenticationRequestParameters authenticationRequestParameters,
+ AcquireTokenForManagedIdentityParameters managedIdentityParameters)
+ {
+ return UseSlcManagedIdentity(
+ authenticationRequestParameters.RequestContext,
+ out Uri credentialEndpointUri) ?
+ new SlcManagedIdentityAuthRequest(
+ serviceBundle,
+ authenticationRequestParameters,
+ managedIdentityParameters, credentialEndpointUri) : null;
+ }
+
+ protected override async Task GetAccessTokenAsync(
+ IKeyMaterialManager keyMaterial,
+ CancellationToken cancellationToken,
+ ILoggerAdapter logger)
+ {
+ await ResolveAuthorityAsync().ConfigureAwait(false);
+
+ //calls sent to app token provider
+ AuthenticationResult authResult = null;
+ MsalAccessTokenCacheItem cachedAccessTokenItem = null;
+
+ //allow only one call to the provider
+ logger.Verbose(() => "[SlcManagedIdentityAuthRequest] Entering acquire token for managed identity credential request semaphore.");
+ await s_semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
+ logger.Verbose(() => "[SlcManagedIdentityAuthRequest] Entered acquire token for managed identity credential request semaphore.");
+
+ try
+ {
+ // Bypass cache and send request to token endpoint, when
+ // 1. Force refresh is requested, or
+ // 2. Claims are passed, or
+ // 3. If the AT needs to be refreshed pro-actively
+ if (_managedIdentityParameters.ForceRefresh ||
+ !string.IsNullOrEmpty(AuthenticationRequestParameters.Claims) ||
+ AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo == CacheRefreshReason.ProactivelyRefreshed)
+ {
+ authResult = await GetAccessTokenFromTokenEndpointAsync(keyMaterial, cancellationToken, logger).ConfigureAwait(false);
+ }
+ else
+ {
+ cachedAccessTokenItem = await GetCachedAccessTokenAsync().ConfigureAwait(false);
+
+ if (cachedAccessTokenItem == null)
+ {
+ authResult = await GetAccessTokenFromTokenEndpointAsync(keyMaterial, cancellationToken, logger).ConfigureAwait(false);
+ }
+ else
+ {
+ logger.Verbose(() => "[SlcManagedIdentityAuthRequest] Getting Access token from cache ...");
+ authResult = CreateAuthenticationResultFromCache(cachedAccessTokenItem);
+ }
+ }
+
+ return authResult;
+ }
+ finally
+ {
+ s_semaphoreSlim.Release();
+ logger.Verbose(() => "[SlcManagedIdentityAuthRequest] Released acquire token for managed identity credential request semaphore.");
+ }
+ }
+
+ private async Task GetAccessTokenFromTokenEndpointAsync(
+ IKeyMaterialManager keyMaterial,
+ CancellationToken cancellationToken,
+ ILoggerAdapter logger)
+ {
+ string message;
+ Exception exception = null;
+
+ try
+ {
+ logger.Verbose(() => "[SlcManagedIdentityAuthRequest] Getting token from the managed identity endpoint.");
+
+ SlcCredentialResponse credentialResponse =
+ await GetCredentialAssertionAsync(keyMaterial, logger, cancellationToken).ConfigureAwait(false);
+
+ var baseUri = new Uri(credentialResponse.RegionalTokenUrl);
+ var tokenUrl = new Uri(baseUri, $"{credentialResponse.TenantId}/oauth2/v2.0/token");
+
+ logger.Verbose(() => $"[SlcManagedIdentityAuthRequest] Token endpoint : { tokenUrl }.");
+
+ OAuth2Client client = CreateClientRequest(
+ keyMaterial,
+ AuthenticationRequestParameters.RequestContext.ServiceBundle.HttpManager,
+ credentialResponse);
+
+ MsalTokenResponse msalTokenResponse = await client
+ .GetTokenAsync(tokenUrl,
+ AuthenticationRequestParameters.RequestContext,
+ true,
+ AuthenticationRequestParameters.OnBeforeTokenRequestHandler).ConfigureAwait(false);
+
+ msalTokenResponse.Scope = AuthenticationRequestParameters.Scope.AsSingleString();
+
+ logger.Info("[SlcManagedIdentityAuthRequest] Successful response received.");
+
+ return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse)
+ .ConfigureAwait(false);
+ }
+ catch (MsalClientException ex)
+ {
+ logger.Verbose(() => $"[SlcManagedIdentityAuthRequest] Caught an exception. {ex.Message}");
+ throw;
+ }
+ catch (HttpRequestException ex)
+ {
+ exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
+ MsalError.ManagedIdentityUnreachableNetwork,
+ ex.Message,
+ ex,
+ ManagedIdentitySource.SlcCredential,
+ null);
+
+ logger.Verbose(() => $"[SlcManagedIdentityAuthRequest] Caught an exception. {ex.Message}");
+
+ throw exception;
+ }
+ catch (MsalServiceException ex)
+ {
+ logger.Verbose(() => $"[SlcManagedIdentityAuthRequest] Caught an exception. {ex.Message}. Error Code : {ex.ErrorCode} Status Code : {ex.StatusCode}");
+
+ exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
+ ex.ErrorCode,
+ ex.Message,
+ ex,
+ ManagedIdentitySource.SlcCredential,
+ ex.StatusCode);
+
+ throw exception;
+ }
+ catch (Exception e) when (e is not MsalServiceException)
+ {
+ logger.Error($"[SlcManagedIdentityAuthRequest] Exception: {e.Message}");
+ exception = e;
+ message = MsalErrorMessage.CredentialEndpointNoResponseReceived;
+ }
+
+ MsalException msalException = MsalServiceExceptionFactory.CreateManagedIdentityException(
+ MsalError.CredentialRequestFailed,
+ message,
+ exception,
+ ManagedIdentitySource.SlcCredential,
+ null);
+
+ throw msalException;
+ }
+
+ private async Task GetCredentialAssertionAsync(
+ IKeyMaterialManager keyMaterial,
+ ILoggerAdapter logger,
+ CancellationToken cancellationToken
+ )
+ {
+ var msiCredentialService = new ManagedIdentityCredentialService(
+ _credentialEndpoint,
+ keyMaterial.BindingCertificate,
+ AuthenticationRequestParameters.RequestContext,
+ cancellationToken);
+
+ SlcCredentialResponse credentialResponse = await msiCredentialService.GetCredentialAsync().ConfigureAwait(false);
+
+ logger.Verbose(() => "[SlcManagedIdentityAuthRequest] A credential was successfully fetched.");
+
+ return credentialResponse;
+ }
+
+ ///
+ /// Creates an OAuth2 client request for fetching the managed identity credential.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private OAuth2Client CreateClientRequest(
+ IKeyMaterialManager keyMaterial,
+ IHttpManager httpManager,
+ SlcCredentialResponse credentialResponse)
+ {
+ // Initialize an OAuth2 client with logger, HTTP manager, and binding certificate.
+ var client = new OAuth2Client(
+ AuthenticationRequestParameters.RequestContext.Logger,
+ httpManager,
+ keyMaterial.BindingCertificate);
+
+ // Convert overridden scopes to a single string.
+ string scopes = GetOverriddenScopes(AuthenticationRequestParameters.Scope).AsSingleString();
+
+ //credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI)
+ scopes += "/.default";
+
+ // Add required parameters for client credentials grant request.
+ client.AddBodyParameter(OAuth2Parameter.GrantType, OAuth2GrantType.ClientCredentials);
+ client.AddBodyParameter(OAuth2Parameter.Scope, scopes);
+ client.AddBodyParameter(OAuth2Parameter.ClientId, credentialResponse.ClientId);
+ client.AddBodyParameter(OAuth2Parameter.ClientAssertion, credentialResponse.Credential);
+ client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer);
+
+ // Add optional claims and client capabilities parameter if provided.
+ if (!string.IsNullOrWhiteSpace(AuthenticationRequestParameters.ClaimsAndClientCapabilities))
+ {
+ client.AddBodyParameter(OAuth2Parameter.Claims, AuthenticationRequestParameters.ClaimsAndClientCapabilities);
+ }
+
+ // Return the configured OAuth2 client.
+ return client;
+ }
+
+ // Check if CredentialKeyType is set to a valid value for Managed Identity.
+ private static bool UseSlcManagedIdentity(
+ RequestContext requestContext,
+ out Uri credentialEndpointUri)
+ {
+ credentialEndpointUri = null;
+
+ CryptoKeyType credentialKeyType = requestContext.ServiceBundle.Config.ManagedIdentityCredentialKeyType;
+ bool isClaimsRequested = requestContext.ServiceBundle.Config.ClientCapabilities?.Any() == true;
+
+ // CredentialKeyType will be Undefined, if no keys are provisioned.
+ if (credentialKeyType == CryptoKeyType.Undefined)
+ {
+ // If new CAE APIs are used when no keys are defined, throw an exception.
+ if (isClaimsRequested)
+ {
+ requestContext.Logger.Verbose(() => "[SlcManagedIdentityAuthRequest] Claims-based authentication is not " +
+ "supported with Managed Identity on the current Azure Resource.");
+
+ throw new MsalClientException(
+ MsalError.ClaimsNotSupportedOnMiResource,
+ MsalErrorMessage.ResourceDoesNotSupportClaims);
+ }
+ // Log the unavailability of credential based managed identity for a basic request.
+ // Proceed to use Legacy MSI flow.
+ requestContext.Logger.Verbose(() => "[SlcManagedIdentityAuthRequest] Credential based managed identity is unavailable without specific client capabilities.");
+ return false;
+ }
+
+ // Initialize the credentialUri with the constant CredentialEndpoint and API version.
+ string credentialUri = Constants.CredentialEndpoint;
+
+ // Switch based on the type of Managed Identity ID provided.
+ switch (requestContext.ServiceBundle.Config.ManagedIdentityId.IdType)
+ {
+ // If the ID is of type ClientId, add user assigned client id to the request.
+ case ManagedIdentityIdType.ClientId:
+ requestContext.Logger.Info("[SlcManagedIdentityAuthRequest] Adding user assigned client id to the request.");
+ credentialUri += $"&{Constants.ManagedIdentityClientId}={requestContext.ServiceBundle.Config.ManagedIdentityId.UserAssignedId}";
+ break;
+
+ // If the ID is of type ResourceId, add user assigned resource id to the request.
+ case ManagedIdentityIdType.ResourceId:
+ requestContext.Logger.Info("[SlcManagedIdentityAuthRequest] Adding user assigned resource id to the request.");
+ credentialUri += $"&{Constants.ManagedIdentityResourceId}={requestContext.ServiceBundle.Config.ManagedIdentityId.UserAssignedId}";
+ break;
+
+ // If the ID is of type ObjectId, add user assigned object id to the request.
+ case ManagedIdentityIdType.ObjectId:
+ requestContext.Logger.Info("[SlcManagedIdentityAuthRequest] Adding user assigned object id to the request.");
+ credentialUri += $"&{Constants.ManagedIdentityObjectId}={requestContext.ServiceBundle.Config.ManagedIdentityId.UserAssignedId}";
+ break;
+ }
+
+ // Set the credentialEndpointUri with the constructed URI.
+ credentialEndpointUri = new Uri(credentialUri);
+
+ // Log information about creating Credential based managed identity.
+ requestContext.Logger.Info($"[SlcManagedIdentityAuthRequest] Creating Credential based managed identity.");
+ return true;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs b/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs
index 8072ffb692..cd71ffa85b 100644
--- a/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs
@@ -13,8 +13,13 @@
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
using Microsoft.Identity.Client.TelemetryCore;
using Microsoft.Identity.Client.TelemetryCore.Http;
-using Microsoft.Identity.Client.Utils;
using Microsoft.Identity.Client.WsTrust;
+#if NETSTANDARD
+using Microsoft.Identity.Client.Platforms.netstandard;
+#endif
+#if NET451_OR_GREATER
+using Microsoft.Identity.Client.Platforms.netdesktop;
+#endif
namespace Microsoft.Identity.Client.Internal
{
@@ -45,6 +50,7 @@ internal ServiceBundle(
WsTrustWebRequestManager = new WsTrustWebRequestManager(HttpManager);
ThrottlingManager = SingletonThrottlingManager.GetInstance();
DeviceAuthManager = config.DeviceAuthManagerForTest ?? PlatformProxy.CreateDeviceAuthManager();
+ KeyMaterialManager = config.KeyMaterialManagerForTest ?? PlatformProxy.GetKeyMaterialManager();
if (shouldClearCaches) // for test
{
@@ -74,6 +80,7 @@ internal ServiceBundle(
public ApplicationConfiguration Config { get; }
public IDeviceAuthManager DeviceAuthManager { get; }
+ public IKeyMaterialManager KeyMaterialManager { get; }
public IHttpTelemetryManager HttpTelemetryManager { get; }
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs
index 0b4adaf94b..98b898b205 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs
@@ -41,27 +41,47 @@ public virtual async Task AuthenticateAsync(
cancellationToken.ThrowIfCancellationRequested();
}
+ HttpResponse response;
+
// Convert the scopes to a resource string.
string resource = parameters.Resource;
ManagedIdentityRequest request = CreateRequest(resource);
+ _requestContext.Logger.Info("[Managed Identity] sending request to managed identity endpoints.");
+
try
{
- HttpResponse response =
- request.Method == HttpMethod.Get ?
- await _requestContext.ServiceBundle.HttpManager
- .SendGetForceResponseAsync(
- request.ComputeUri(),
- request.Headers,
- _requestContext.Logger,
- cancellationToken: cancellationToken).ConfigureAwait(false) :
- await _requestContext.ServiceBundle.HttpManager
- .SendPostForceResponseAsync(
- request.ComputeUri(),
- request.Headers,
- request.BodyParameters,
- _requestContext.Logger, cancellationToken: cancellationToken).ConfigureAwait(false);
+ if (request.Method == HttpMethod.Get)
+ {
+ response = await _requestContext.ServiceBundle.HttpManager
+ .SendRequestAsync(
+ request.ComputeUri(),
+ request.Headers,
+ body: null,
+ HttpMethod.Get,
+ logger: _requestContext.Logger,
+ doNotThrow: true,
+ retry: true,
+ mtlsCertificate: null,
+ cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ response = await _requestContext.ServiceBundle.HttpManager
+ .SendRequestAsync(
+ request.ComputeUri(),
+ request.Headers,
+ body: new FormUrlEncodedContent(request.BodyParameters),
+ HttpMethod.Post,
+ logger: _requestContext.Logger,
+ doNotThrow: true,
+ retry: true,
+ mtlsCertificate: null,
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ }
return await HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false);
}
@@ -103,11 +123,11 @@ protected ManagedIdentityResponse GetSuccessfulResponse(HttpResponse response)
{
ManagedIdentityResponse managedIdentityResponse = JsonHelper.DeserializeFromJson(response.Body);
- if (managedIdentityResponse == null || managedIdentityResponse.AccessToken.IsNullOrEmpty() || managedIdentityResponse.ExpiresOn.IsNullOrEmpty())
+ if (managedIdentityResponse == null || !managedIdentityResponse.IsValid())
{
_requestContext.Logger.Error("[Managed Identity] Response is either null or insufficient for authentication.");
- var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
+ MsalException exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.ManagedIdentityRequestFailed,
MsalErrorMessage.ManagedIdentityInvalidResponse,
null,
@@ -212,7 +232,7 @@ private void HandleException(Exception ex,
if (ex is HttpRequestException httpRequestException)
{
- CreateAndThrowException(MsalError.ManagedIdentityUnreachableNetwork, httpRequestException.Message, httpRequestException, source);
+ CreateAndThrowException(MsalError.CredentialUnreachableNetwork, httpRequestException.Message, httpRequestException, source);
}
else if (ex is TaskCanceledException)
{
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AppServiceManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AppServiceManagedIdentitySource.cs
index 378190d9de..6f0e2b5402 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AppServiceManagedIdentitySource.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AppServiceManagedIdentitySource.cs
@@ -31,7 +31,7 @@ public static AbstractManagedIdentity Create(RequestContext requestContext)
: null;
}
- private AppServiceManagedIdentitySource(RequestContext requestContext, Uri endpoint, string secret)
+ private AppServiceManagedIdentitySource(RequestContext requestContext, Uri endpoint, string secret)
: base(requestContext, ManagedIdentitySource.AppService)
{
_endpoint = endpoint;
@@ -57,7 +57,7 @@ private static bool TryValidateEnvVars(string msiEndpoint, ILoggerAdapter logger
var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.InvalidManagedIdentityEndpoint,
errorMessage,
- ex,
+ ex,
ManagedIdentitySource.AppService,
null); // statusCode is null in this case
@@ -71,7 +71,7 @@ private static bool TryValidateEnvVars(string msiEndpoint, ILoggerAdapter logger
protected override ManagedIdentityRequest CreateRequest(string resource)
{
ManagedIdentityRequest request = new(System.Net.Http.HttpMethod.Get, _endpoint);
-
+
request.Headers.Add(SecretHeaderName, _secret);
request.QueryParameters["api-version"] = AppServiceMsiApiVersion;
request.QueryParameters["resource"] = resource;
@@ -93,7 +93,7 @@ protected override ManagedIdentityRequest CreateRequest(string resource)
request.QueryParameters[Constants.ManagedIdentityObjectId] = _requestContext.ServiceBundle.Config.ManagedIdentityId.UserAssignedId;
break;
}
-
+
return request;
}
}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs
index 8389c97570..3733a4f444 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs
@@ -43,18 +43,18 @@ public static AbstractManagedIdentity Create(RequestContext requestContext)
var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.InvalidManagedIdentityEndpoint,
errorMessage,
- null,
+ null,
ManagedIdentitySource.AzureArc,
- null);
+ null);
throw exception;
}
- requestContext.Logger.Verbose(()=>"[Managed Identity] Creating Azure Arc managed identity. Endpoint URI: " + endpointUri);
+ requestContext.Logger.Verbose(() => "[Managed Identity] Creating Azure Arc managed identity. Endpoint URI: " + endpointUri);
return new AzureArcManagedIdentitySource(endpointUri, requestContext);
}
- private AzureArcManagedIdentitySource(Uri endpoint, RequestContext requestContext) :
+ private AzureArcManagedIdentitySource(Uri endpoint, RequestContext requestContext) :
base(requestContext, ManagedIdentitySource.AzureArc)
{
_endpoint = endpoint;
@@ -64,10 +64,10 @@ private AzureArcManagedIdentitySource(Uri endpoint, RequestContext requestContex
string errorMessage = string.Format(CultureInfo.InvariantCulture, MsalErrorMessage.ManagedIdentityUserAssignedNotSupported, AzureArc);
var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
- MsalError.UserAssignedManagedIdentityNotSupported,
- errorMessage,
- null,
- ManagedIdentitySource.AzureArc,
+ MsalError.UserAssignedManagedIdentityNotSupported,
+ errorMessage,
+ null,
+ ManagedIdentitySource.AzureArc,
null);
throw exception;
@@ -131,7 +131,17 @@ protected override async Task HandleResponseAsync(
_requestContext.Logger.Verbose(() => "[Managed Identity] Adding authorization header to the request.");
request.Headers.Add("Authorization", authHeaderValue);
- response = await _requestContext.ServiceBundle.HttpManager.SendGetAsync(request.ComputeUri(), request.Headers, _requestContext.Logger, cancellationToken: cancellationToken).ConfigureAwait(false);
+ response = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync(
+ request.ComputeUri(),
+ request.Headers,
+ body: null,
+ System.Net.Http.HttpMethod.Get,
+ logger: _requestContext.Logger,
+ doNotThrow: false,
+ retry: true,
+ mtlsCertificate: null,
+ cancellationToken)
+ .ConfigureAwait(false);
return await base.HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false);
}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/CloudShellManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/CloudShellManagedIdentitySource.cs
index 7282a67243..adb4f7b6f1 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/CloudShellManagedIdentitySource.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/CloudShellManagedIdentitySource.cs
@@ -42,18 +42,18 @@ public static AbstractManagedIdentity Create(RequestContext requestContext)
var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.InvalidManagedIdentityEndpoint,
errorMessage,
- ex,
+ ex,
ManagedIdentitySource.CloudShell,
- null);
+ null);
throw exception;
}
- requestContext.Logger.Verbose(()=>"[Managed Identity] Creating cloud shell managed identity. Endpoint URI: " + msiEndpoint);
+ requestContext.Logger.Verbose(() => "[Managed Identity] Creating cloud shell managed identity. Endpoint URI: " + msiEndpoint);
return new CloudShellManagedIdentitySource(endpointUri, requestContext);
}
- private CloudShellManagedIdentitySource(Uri endpoint, RequestContext requestContext) :
+ private CloudShellManagedIdentitySource(Uri endpoint, RequestContext requestContext) :
base(requestContext, ManagedIdentitySource.CloudShell)
{
_endpoint = endpoint;
@@ -61,8 +61,8 @@ private CloudShellManagedIdentitySource(Uri endpoint, RequestContext requestCont
if (requestContext.ServiceBundle.Config.ManagedIdentityId.IsUserAssigned)
{
string errorMessage = string.Format(
- CultureInfo.InvariantCulture,
- MsalErrorMessage.ManagedIdentityUserAssignedNotSupported,
+ CultureInfo.InvariantCulture,
+ MsalErrorMessage.ManagedIdentityUserAssignedNotSupported,
CloudShell);
var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/IManagedIdentityCredentialServices.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/IManagedIdentityCredentialServices.cs
new file mode 100644
index 0000000000..57dae4d161
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/IManagedIdentityCredentialServices.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Threading.Tasks;
+
+namespace Microsoft.Identity.Client.ManagedIdentity
+{
+ internal interface IManagedIdentityCredentialService
+ {
+ Task GetCredentialAsync();
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs
index fa40e10ae7..94f6ab42fa 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityClient.cs
@@ -61,7 +61,6 @@ private static ManagedIdentitySource GetManagedIdentitySource()
string imdsEndpoint = EnvironmentVariables.ImdsEndpoint;
string podIdentityEndpoint = EnvironmentVariables.PodIdentityEndpoint;
-
if (!string.IsNullOrEmpty(identityEndpoint) && !string.IsNullOrEmpty(identityHeader))
{
if (!string.IsNullOrEmpty(identityServerThumbprint))
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityCredentialServices.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityCredentialServices.cs
new file mode 100644
index 0000000000..6ce8e6edf5
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityCredentialServices.cs
@@ -0,0 +1,162 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client.Http;
+using Microsoft.Identity.Client.Internal;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.Utils;
+using Microsoft.Identity.Client.OAuth2;
+using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.Identity.Client.ManagedIdentity
+{
+ internal class ManagedIdentityCredentialService : IManagedIdentityCredentialService
+ {
+ private readonly Uri _uri;
+ private readonly X509Certificate2 _bindingCertificate;
+ private readonly RequestContext _requestContext;
+ private readonly CancellationToken _cancellationToken;
+ internal const string TimeoutError = "[Managed Identity] Authentication unavailable. The request to the managed identity endpoint timed out.";
+
+ public ManagedIdentityCredentialService(
+ Uri uri,
+ X509Certificate2 bindingCertificate,
+ RequestContext requestContext,
+ CancellationToken cancellationToken)
+ {
+ _uri = uri;
+ _bindingCertificate = bindingCertificate;
+ _requestContext = requestContext;
+ _cancellationToken = cancellationToken;
+ }
+
+ ///
+ /// Gets or fetches the Managed Identity credential from the cache or the service.
+ ///
+ ///
+ ///
+ public async Task GetCredentialAsync()
+ {
+ SlcCredentialResponse credentialResponse = await FetchFromServiceAsync(
+ _requestContext.ServiceBundle.HttpManager,
+ _cancellationToken)
+ .ConfigureAwait(false);
+
+ ValidateCredentialResponse(credentialResponse);
+
+ return credentialResponse;
+ }
+
+ ///
+ /// Validates the properties of a CredentialResponse to ensure it meets the necessary criteria for authentication.
+ ///
+ /// The CredentialResponse to be validated.
+ private void ValidateCredentialResponse(SlcCredentialResponse credentialResponse)
+ {
+ var errorMessages = new List();
+
+ // Check if the CredentialResponse is null or if any required property is missing or invalid
+ if (credentialResponse == null)
+ {
+ errorMessages.Add("CredentialResponse is null. ");
+ }
+ else
+ {
+ if (credentialResponse.Credential.IsNullOrEmpty())
+ {
+ errorMessages.Add("Credential is missing or empty. ");
+ }
+
+ if (credentialResponse.RegionalTokenUrl.IsNullOrEmpty())
+ {
+ errorMessages.Add("RegionalTokenUrl is missing or empty. ");
+ }
+
+ if (credentialResponse.ClientId.IsNullOrEmpty())
+ {
+ errorMessages.Add("ClientId is missing or empty. ");
+ }
+
+ if (credentialResponse.TenantId.IsNullOrEmpty())
+ {
+ errorMessages.Add("TenantId is missing or empty. ");
+ }
+ }
+
+ // Check if any error messages were added
+ if (errorMessages.Any())
+ {
+ // Log an error message indicating the missing or insufficient fields
+ _requestContext.Logger.Error("[Managed Identity] " + string.Join(" ", errorMessages) +
+ " and/or insufficient for authentication.");
+
+ // Throw an exception indicating that the CredentialResponse is invalid
+ MsalException exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
+ MsalError.CredentialRequestFailed,
+ MsalErrorMessage.ManagedIdentityInvalidResponse,
+ null,
+ ManagedIdentitySource.SlcCredential,
+ null);
+
+ throw exception;
+ }
+ }
+
+ ///
+ /// Fetches a new managed identity credential from the IMDS endpoint.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task FetchFromServiceAsync(
+ IHttpManager httpManager,
+ CancellationToken cancellationToken)
+ {
+ _requestContext.Logger.Info("[Managed Identity] Fetching new managed identity credential from IMDS endpoint.");
+
+ OAuth2Client client = CreateClientRequest(httpManager);
+
+ SlcCredentialResponse credentialResponse = await client
+ .GetCredentialResponseAsync(_uri, _requestContext)
+ .ConfigureAwait(false);
+
+ return credentialResponse;
+ }
+
+ ///
+ /// Creates an OAuth2 client request for fetching the managed identity credential.
+ ///
+ ///
+ ///
+ private OAuth2Client CreateClientRequest(IHttpManager httpManager)
+ {
+ var client = new OAuth2Client(_requestContext.Logger, httpManager, null);
+
+ client.AddHeader("Metadata", "true");
+ client.AddHeader("x-ms-client-request-id", _requestContext.CorrelationId.ToString("D"));
+ string jsonPayload = GetCredentialPayload();
+ client.AddBodyContent(new StringContent(jsonPayload, System.Text.Encoding.UTF8, "application/json"));
+
+ return client;
+ }
+
+ ///
+ /// Creates the payload for the managed identity credential request.
+ ///
+ private string GetCredentialPayload()
+ {
+ string certificateBase64 = Convert.ToBase64String(_bindingCertificate.Export(X509ContentType.Cert));
+
+ return @"{""cnf"":{""jwk"":{""kty"":""RSA"",""use"":""sig"",""alg"":""RS256"",""kid"":""" + _bindingCertificate.Thumbprint +
+ @""",""x5c"":[""" + certificateBase64 + @"""]}},""latch_key"":false}";
+ }
+
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs
index 6eb5a5bba0..89cbbfa034 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Utils;
@@ -17,19 +18,25 @@ internal class ManagedIdentityRequest
public HttpMethod Method { get; }
- public IDictionary Headers { get; }
+ public Dictionary Headers { get; }
- public IDictionary BodyParameters { get; }
+ public Dictionary BodyParameters { get; }
- public IDictionary QueryParameters { get; }
+ public Dictionary QueryParameters { get; }
- public ManagedIdentityRequest(HttpMethod method, Uri endpoint)
+ public X509Certificate2 BindingCertificate { get; internal set; }
+
+ public ManagedIdentityRequest(
+ HttpMethod method,
+ Uri endpoint,
+ X509Certificate2 bindingCertificate = null)
{
Method = method;
_baseEndpoint = endpoint;
Headers = new Dictionary();
BodyParameters = new Dictionary();
QueryParameters = new Dictionary();
+ BindingCertificate = bindingCertificate;
}
public Uri ComputeUri()
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityResponse.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityResponse.cs
index 3a8147b427..0b391cdba3 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityResponse.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityResponse.cs
@@ -55,5 +55,10 @@ internal class ManagedIdentityResponse
[JsonProperty("client_id")]
public string ClientId { get; set; }
+ // Method to check if the necessary properties are valid
+ public bool IsValid()
+ {
+ return !string.IsNullOrEmpty(AccessToken) && !string.IsNullOrEmpty(ExpiresOn);
+ }
}
}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySource.cs
index 8ae1181539..8421b4c2a0 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySource.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentitySource.cs
@@ -48,6 +48,11 @@ public enum ManagedIdentitySource
/// Indicates that the source is defaulted to IMDS since no environment variables are set.
/// This is used to detect the managed identity source.
///
- DefaultToImds
+ DefaultToImds,
+
+ ///
+ /// The source to acquire token for managed identity is Slc Credential Endpoint.
+ ///
+ SlcCredential
}
}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs
index 13e2a91c84..147a64513d 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs
@@ -34,9 +34,9 @@ public static AbstractManagedIdentity Create(RequestContext requestContext)
var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.InvalidManagedIdentityEndpoint,
errorMessage,
- null,
+ null,
ManagedIdentitySource.ServiceFabric,
- null);
+ null);
throw exception;
}
@@ -45,7 +45,7 @@ public static AbstractManagedIdentity Create(RequestContext requestContext)
return new ServiceFabricManagedIdentitySource(requestContext, endpointUri, EnvironmentVariables.IdentityHeader);
}
- private ServiceFabricManagedIdentitySource(RequestContext requestContext, Uri endpoint, string identityHeaderValue) :
+ private ServiceFabricManagedIdentitySource(RequestContext requestContext, Uri endpoint, string identityHeaderValue) :
base(requestContext, ManagedIdentitySource.ServiceFabric)
{
_endpoint = endpoint;
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/SlcCredentialResponse.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/SlcCredentialResponse.cs
new file mode 100644
index 0000000000..6dea45ddc6
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/SlcCredentialResponse.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+#if SUPPORTS_SYSTEM_TEXT_JSON
+using Microsoft.Identity.Client.Platforms.net6;
+using JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
+#else
+using Microsoft.Identity.Json;
+#endif
+
+namespace Microsoft.Identity.Client.ManagedIdentity
+{
+ [JsonObject]
+ [Preserve(AllMembers = true)]
+ internal class SlcCredentialResponse
+ {
+ [JsonProperty("client_id")]
+ public string ClientId { get; set; }
+
+ [JsonProperty("credential")]
+ public string Credential { get; set; }
+
+ [JsonProperty("expires_on")]
+ public long ExpiresOn { get; set; }
+
+ [JsonProperty("identity_type")]
+ public string IdentityType { get; set; }
+
+ [JsonProperty("refresh_in")]
+ public long RefreshIn { get; set; }
+
+ [JsonProperty("regional_token_url")]
+ public string RegionalTokenUrl { get; set; }
+
+ [JsonProperty("tenant_id")]
+ public string TenantId { get; set; }
+
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs b/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs
index 954b3fa669..3298d6cbfa 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentityApplication.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Executors;
@@ -12,6 +13,8 @@
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Internal.Requests;
using Microsoft.Identity.Client.ManagedIdentity;
+using Microsoft.Identity.Client.PlatformsCommon.Factories;
+using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
namespace Microsoft.Identity.Client
{
@@ -36,12 +39,18 @@ internal ManagedIdentityApplication(
AppTokenCacheInternal = configuration.AppTokenCacheInternalForTest ?? new TokenCache(ServiceBundle, true);
- this.ServiceBundle.ApplicationLogger.Verbose(()=>$"ManagedIdentityApplication {configuration.GetHashCode()} created");
+ KeyMaterialManager = configuration.KeyMaterialManagerForTest ?? ServiceBundle.PlatformProxy.GetKeyMaterialManager();
+ configuration.ManagedIdentityClientCertificate = KeyMaterialManager.BindingCertificate;
+ configuration.ManagedIdentityCredentialKeyType = KeyMaterialManager.CryptoKeyType;
+
+ ServiceBundle.ApplicationLogger.Verbose(() => $"ManagedIdentityApplication {configuration.GetHashCode()} created");
}
// Stores all app tokens
internal ITokenCacheInternal AppTokenCacheInternal { get; }
+ internal IKeyMaterialManager KeyMaterialManager { get; }
+
///
public AcquireTokenForManagedIdentityParameterBuilder AcquireTokenForManagedIdentity(string resource)
{
@@ -55,6 +64,38 @@ public AcquireTokenForManagedIdentityParameterBuilder AcquireTokenForManagedIden
resource);
}
+ ///
+ /// Used to determine if managed identity is able to handle claims.
+ ///
+ /// Boolean indicating if Claims is supported
+ public static bool IsClaimsSupportedByClient()
+ {
+ // Get the PlatformProxy instance
+ IPlatformProxy platformProxy = PlatformProxyFactory.CreatePlatformProxy(null);
+
+ // Get the KeyMaterialManager
+ IKeyMaterialManager keyMaterialManager = platformProxy.GetKeyMaterialManager();
+
+ // True if the key material manager has a crypto key type
+ return keyMaterialManager.CryptoKeyType != CryptoKeyType.Undefined;
+ }
+
+ ///
+ /// Retrieves the binding certificate for advanced managed identity scenarios.
+ ///
+ /// Binding certificate used for advanced scenarios
+ public static X509Certificate2 GetBindingCertificate()
+ {
+ // Get the PlatformProxy instance
+ IPlatformProxy platformProxy = PlatformProxyFactory.CreatePlatformProxy(null);
+
+ // Get the KeyMaterialManager
+ IKeyMaterialManager keyMaterialManager = platformProxy.GetKeyMaterialManager();
+
+ // Return the binding certificate
+ return keyMaterialManager.BindingCertificate;
+ }
+
///
/// Detects and returns the managed identity source available on the environment.
///
diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
index 5743aef132..1f186b6ca5 100644
--- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
+++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
@@ -2,6 +2,7 @@
net462
+ net472
netstandard2.0
net6.0
Debug;Release;Debug + MobileApps
@@ -21,7 +22,7 @@
- $(TargetFrameworkNetDesktop462);$(TargetFrameworkNetStandard);$(TargetFrameworkNet6Ios);$(TargetFrameworkNet6Android);$(TargetFrameworkNet6);
+ $(TargetFrameworkNetDesktop462);$(TargetFrameworkNetDesktop472);$(TargetFrameworkNetStandard);$(TargetFrameworkNet6Ios);$(TargetFrameworkNet6Android);$(TargetFrameworkNet6);
$(TargetFrameworkNetStandard);$(TargetFrameworkNet6Ios);$(TargetFrameworkNet6Android);$(TargetFrameworkNet6)
$(TargetFrameworkNetStandard);$(TargetFrameworkNet6)
@@ -34,10 +35,10 @@
Microsoft Authentication Library for .NET
-
+
README.md
-
+
@@ -61,7 +62,7 @@
false
-
+
$(DefineConstants);HAVE_ADO_NET;HAVE_APP_DOMAIN;HAVE_ASYNC;HAVE_BIG_INTEGER;HAVE_BINARY_FORMATTER;HAVE_BINARY_SERIALIZATION;HAVE_BINARY_EXCEPTION_SERIALIZATION;HAVE_CHAR_TO_LOWER_WITH_CULTURE;HAVE_CHAR_TO_STRING_WITH_CULTURE;HAVE_COM_ATTRIBUTES;HAVE_COMPONENT_MODEL;HAVE_CONCURRENT_COLLECTIONS;HAVE_COVARIANT_GENERICS;HAVE_DATA_CONTRACTS;HAVE_DATE_TIME_OFFSET;HAVE_DB_NULL_TYPE_CODE;HAVE_DYNAMIC;HAVE_EMPTY_TYPES;HAVE_ENTITY_FRAMEWORK;HAVE_EXPRESSIONS;HAVE_FAST_REVERSE;HAVE_FSHARP_TYPES;HAVE_FULL_REFLECTION;HAVE_GUID_TRY_PARSE;HAVE_HASH_SET;HAVE_ICLONEABLE;HAVE_ICONVERTIBLE;HAVE_IGNORE_DATA_MEMBER_ATTRIBUTE;HAVE_INOTIFY_COLLECTION_CHANGED;HAVE_INOTIFY_PROPERTY_CHANGING;HAVE_ISET;HAVE_LINQ;HAVE_MEMORY_BARRIER;HAVE_METHOD_IMPL_ATTRIBUTE;HAVE_NON_SERIALIZED_ATTRIBUTE;HAVE_READ_ONLY_COLLECTIONS;HAVE_REFLECTION_EMIT;HAVE_SECURITY_SAFE_CRITICAL_ATTRIBUTE;HAVE_SERIALIZATION_BINDER_BIND_TO_NAME;HAVE_STREAM_READER_WRITER_CLOSE;HAVE_STRING_JOIN_WITH_ENUMERABLE;HAVE_TIME_SPAN_PARSE_WITH_CULTURE;HAVE_TIME_SPAN_TO_STRING_WITH_CULTURE;HAVE_TIME_ZONE_INFO;HAVE_TRACE_WRITER;HAVE_TYPE_DESCRIPTOR;HAVE_UNICODE_SURROGATE_DETECTION;HAVE_VARIANT_TYPE_PARAMETERS;HAVE_VERSION_TRY_PARSE;HAVE_XLINQ;HAVE_XML_DOCUMENT;HAVE_XML_DOCUMENT_TYPE;HAVE_CONCURRENT_DICTIONARY;$(AdditionalConstants)
true
@@ -86,8 +87,16 @@
+
+
+
+
+
+
+
+
@@ -106,29 +115,47 @@
+
+
-
+
true
true
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -164,3 +191,4 @@
+
diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs
index 3ae5e404ed..46141d1c2a 100644
--- a/src/client/Microsoft.Identity.Client/MsalError.cs
+++ b/src/client/Microsoft.Identity.Client/MsalError.cs
@@ -865,6 +865,16 @@ public static class MsalError
///
public const string DeviceCertificateNotFound = "device_certificate_not_found";
+ ///
+ /// Managed Identity certificate creation failed.
+ ///
+ public const string CertificateCreationFailed = "certificate_creation_failed";
+
+ ///
+ /// Managed Identity certificate credential request failed.
+ ///
+ public const string CredentialRequestFailed = "credential_request_failed";
+
///
/// What happens?The ADAL cache is invalid as it contains multiple refresh token entries for one user.
/// MitigationDelete the ADAL cache. If you do not maintain an ADAL cache, this may be a bug in MSAL.
@@ -905,6 +915,13 @@ public static class MsalError
/// MitigationProvide a nonce when Proof-of-Possession is configured for public clients.
///
public const string NonceRequiredForPopOnPCA = "nonce_required_for_pop_on_pca";
+
+ ///
+ /// What happens?The request has client capabilities configured but the platform does not support claims.
+ /// MitigationRemove the Client Capabilities API from the request.
+ ///
+ public const string ClaimsNotSupportedOnMiResource = "managed_identity_claims_not_supported_platform";
+
#if iOS
///
/// Xamarin.iOS specific. This error indicates that keychain access has not been enabled for the application.
@@ -1110,6 +1127,11 @@ public static class MsalError
///
public const string ManagedIdentityUnreachableNetwork = "managed_identity_unreachable_network";
+ ///
+ /// Credential endpoint is not reachable.
+ ///
+ public const string CredentialUnreachableNetwork = "credential_endpoint_unreachable_network";
+
///
/// Unknown error response received.
///
diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs
index ab7477b920..4e80c0888c 100644
--- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs
+++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs
@@ -134,6 +134,8 @@ public static string iOSBrokerKeySaveFailed(string keyChainResult)
public const string BrokerRequiredForPop = "The request has Proof-of-Possession configured but does not have broker enabled. Broker is required to use Proof-of-Possession on public clients. Use IPublicClientApplication.IsProofOfPossessionSupportedByClient to ensure Proof-of-Possession can be performed before using WithProofOfPossession.";
public const string NonceRequiredForPop = "The request has Proof-of-Possession configured for public clients but does not have a nonce provided. A nonce is required for Proof-of-Possession on public clients.";
public const string AdfsNotSupportedWithBroker = "Broker does not support ADFS environments. If using Proof-of-Possession, use IPublicClientApplication.IsProofOfPossessionSupportedByClient to ensure Proof-of-Possession can be performed before calling WithProofOfPossession.";
+ public const string ResourceDoesNotSupportPop = "There is no support for Proof-of-Possession on the current platform or Azure Resource.";
+ public const string ResourceDoesNotSupportClaims = "There is no support for Client Capabilities on the current platform or Azure Resource.";
public const string NullIntentReturnedFromBroker = "Broker returned a null intent. Check the Android app settings and logs for more information. ";
public const string NoAccountForLoginHint = "You are trying to acquire a token silently using a login hint. No account was found in the token cache having this login hint. ";
@@ -411,16 +413,18 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName)
}
public const string ManagedIdentityNoResponseReceived = "[Managed Identity] Authentication unavailable. No response received from the managed identity endpoint.";
- public const string ManagedIdentityInvalidResponse = "[Managed Identity] Invalid response, the authentication response received did not contain the expected fields.";
+ public const string ManagedIdentityInvalidResponse = "[Managed Identity] Required Response fields are missing from the credential response and/or insufficient for authentication.";
public const string ManagedIdentityUnexpectedResponse = "[Managed Identity] Unexpected exception occurred when parsing the response. See the inner exception for details.";
public const string ManagedIdentityExactlyOneScopeExpected = "[Managed Identity] To acquire token for managed identity, exactly one scope must be passed.";
public const string ManagedIdentityUnexpectedErrorResponse = "[Managed Identity] The error response was either empty or could not be parsed.";
-
public const string ManagedIdentityEndpointInvalidUriError = "[Managed Identity] The environment variable {0} contains an invalid Uri {1} in {2} managed identity source.";
public const string ManagedIdentityNoChallengeError = "[Managed Identity] Did not receive expected WWW-Authenticate header in the response from Azure Arc Managed Identity Endpoint.";
public const string ManagedIdentityInvalidChallenge = "[Managed Identity] The WWW-Authenticate header in the response from Azure Arc Managed Identity Endpoint did not match the expected format.";
public const string ManagedIdentityUserAssignedNotSupported = "[Managed Identity] User assigned identity is not supported by the {0} Managed Identity. To authenticate with the system assigned identity omit the client id in ManagedIdentityApplicationBuilder.Create().";
public const string ManagedIdentityUserAssignedNotConfigurableAtRuntime = "[Managed Identity] Service Fabric user assigned managed identity ClientId or ResourceId is not configurable at runtime.";
+ public const string CredentialEndpointNoResponseReceived = "[Managed Identity] Authentication unavailable. No response received from the managed identity credential endpoint.";
+ public const string CredentialResponseMissingHeader = "[Managed Identity] Did not receive expected Server header in the response from Credential Endpoint.";
+
public const string CombinedUserAppCacheNotSupported = "Using a combined flat storage, like a file, to store both app and user tokens is not supported. Use a partitioned token cache (for ex. distributed cache like Redis) or separate files for app and user token caches. See https://aka.ms/msal-net-token-cache-serialization .";
public const string JsonParseErrorMessage = "There was an error parsing the response from the token endpoint, see inner exception for details. Verify that your app is configured correctly. If this is a B2C app, one possible cause is acquiring a token for Microsoft Graph, which is not supported. See https://aka.ms/msal-net-up";
public const string SetCiamAuthorityAtRequestLevelNotSupported = "Setting the CIAM authority (ex. \"{tenantName}.ciamlogin.com\") at the request level is not supported. The CIAM authority must be set during application creation";
diff --git a/src/client/Microsoft.Identity.Client/MsalException.cs b/src/client/Microsoft.Identity.Client/MsalException.cs
index eb540827a2..2af6e36272 100644
--- a/src/client/Microsoft.Identity.Client/MsalException.cs
+++ b/src/client/Microsoft.Identity.Client/MsalException.cs
@@ -228,29 +228,29 @@ internal virtual void PopulateJson(JObject jObject)
internal virtual void PopulateObjectFromJson(JObject jObject)
{
// Populate this exception instance with broker exception data from JSON
- var exceptionData = JsonHelper.ExtractInnerJsonAsDictionary(jObject, ExceptionSerializationKey.AdditionalExceptionData);
+ IDictionary exceptionData = JsonHelper.ExtractInnerJsonAsDictionary(jObject, ExceptionSerializationKey.AdditionalExceptionData);
- if (exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorContext, out string brokerErrorContext))
+ if(exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorContext, out string brokerErrorContext))
{
exceptionData[BrokerErrorContext] = brokerErrorContext;
exceptionData.Remove(ExceptionSerializationKey.BrokerErrorContext);
}
- if (exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorTag, out string brokerErrorTag))
+ if(exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorTag, out string brokerErrorTag))
{
exceptionData[BrokerErrorTag] = brokerErrorTag;
exceptionData.Remove(ExceptionSerializationKey.BrokerErrorTag);
}
- if (exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorStatus, out string brokerErrorStatus))
+ if(exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorStatus, out string brokerErrorStatus))
{
exceptionData[BrokerErrorStatus] = brokerErrorStatus;
exceptionData.Remove(ExceptionSerializationKey.BrokerErrorStatus);
}
- if (exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorCode, out string brokerErrorCode))
+ if(exceptionData.TryGetValue(ExceptionSerializationKey.BrokerErrorCode, out string brokerErrorCode))
{
exceptionData[BrokerErrorCode] = brokerErrorCode;
exceptionData.Remove(ExceptionSerializationKey.BrokerErrorCode);
}
- if (exceptionData.TryGetValue(ExceptionSerializationKey.BrokerTelemetry, out string brokerTelemetry))
+ if(exceptionData.TryGetValue(ExceptionSerializationKey.BrokerTelemetry, out string brokerTelemetry))
{
exceptionData[BrokerTelemetry] = brokerTelemetry;
exceptionData.Remove(ExceptionSerializationKey.BrokerTelemetry);
diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs
index 620ffadc84..59fa4b8731 100644
--- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs
+++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs
@@ -15,8 +15,9 @@
using Microsoft.Identity.Client.Instance.Oidc;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Utils;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.Identity.Client.Internal.Broker;
-
+using Microsoft.Identity.Client.ManagedIdentity;
#if SUPPORTS_SYSTEM_TEXT_JSON
using System.Text.Json;
#else
@@ -38,12 +39,15 @@ internal class OAuth2Client
private readonly Dictionary _headers;
private readonly Dictionary _queryParameters = new Dictionary();
private readonly IDictionary _bodyParameters = new Dictionary();
+ private StringContent _stringContent;
private readonly IHttpManager _httpManager;
+ private readonly X509Certificate2 _mtlsCertificate;
- public OAuth2Client(ILoggerAdapter logger, IHttpManager httpManager)
+ public OAuth2Client(ILoggerAdapter logger, IHttpManager httpManager, X509Certificate2 mtlsCertificate)
{
_headers = new Dictionary(MsalIdHelper.GetMsalIdParameters(logger));
_httpManager = httpManager ?? throw new ArgumentNullException(nameof(httpManager));
+ _mtlsCertificate = mtlsCertificate;
}
public void AddQueryParameter(string key, string value)
@@ -62,6 +66,14 @@ public void AddBodyParameter(string key, string value)
}
}
+ public void AddBodyContent(StringContent content)
+ {
+ if (content != null)
+ {
+ _stringContent = content;
+ }
+ }
+
internal void AddHeader(string key, string value)
{
_headers[key] = value;
@@ -82,6 +94,12 @@ public Task DiscoverOidcMetadataAsync(Uri endpoint, RequestContext
return ExecuteRequestAsync(endpoint, HttpMethod.Get, requestContext);
}
+ public async Task GetCredentialResponseAsync(Uri endpoint, RequestContext requestContext)
+ {
+ return await ExecuteRequestAsync(endpoint, HttpMethod.Post, requestContext)
+ .ConfigureAwait(false);
+ }
+
internal Task GetTokenAsync(
Uri endPoint,
RequestContext requestContext,
@@ -111,7 +129,7 @@ internal async Task ExecuteRequestAsync(
AddCommonHeaders(requestContext);
}
- HttpResponse response;
+ HttpResponse response = null;
Uri endpointUri = AddExtraQueryParams(endPoint);
using (requestContext.Logger.LogBlockDuration($"[Oauth2Client] Sending {method} request "))
@@ -122,27 +140,37 @@ internal async Task ExecuteRequestAsync(
{
if (onBeforePostRequestData != null)
{
+ requestContext.Logger.Verbose(() => "[Oauth2Client] Processing onBeforePostRequestData ");
var requestData = new OnBeforeTokenRequestData(_bodyParameters, _headers, endpointUri, requestContext.UserCancellationToken);
await onBeforePostRequestData(requestData).ConfigureAwait(false);
endpointUri = requestData.RequestUri;
}
- response = await _httpManager.SendPostAsync(
+ response = await _httpManager.SendRequestAsync(
endpointUri,
_headers,
- _bodyParameters,
- requestContext.Logger,
- cancellationToken: requestContext.UserCancellationToken)
- .ConfigureAwait(false);
+ body: _stringContent == null ? new FormUrlEncodedContent(_bodyParameters) : _stringContent,
+ HttpMethod.Post,
+ logger: requestContext.Logger,
+ doNotThrow: false,
+ retry: true,
+ mtlsCertificate: _mtlsCertificate,
+ requestContext.UserCancellationToken)
+ .ConfigureAwait(false);
}
else
{
- response = await _httpManager.SendGetAsync(
+ response = await _httpManager.SendRequestAsync(
endpointUri,
_headers,
- requestContext.Logger,
- cancellationToken: requestContext.UserCancellationToken)
- .ConfigureAwait(false);
+ body: null,
+ HttpMethod.Get,
+ logger: requestContext.Logger,
+ doNotThrow: false,
+ retry: true,
+ mtlsCertificate: null,
+ requestContext.UserCancellationToken)
+ .ConfigureAwait(false);
}
}
catch (Exception ex)
diff --git a/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs b/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs
index 9341a7381c..3ef33fd9cc 100644
--- a/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs
+++ b/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs
@@ -43,7 +43,8 @@ public TokenClient(AuthenticationRequestParameters requestParams)
_oAuth2Client = new OAuth2Client(
_serviceBundle.ApplicationLogger,
- _serviceBundle.HttpManager);
+ _serviceBundle.HttpManager,
+ requestParams.MtlsCertificate);
}
public async Task SendTokenRequestAsync(
@@ -141,6 +142,7 @@ private async Task AddBodyParamsAndHeadersAsync(
var tokenEndpoint = await _requestParams.Authority.GetTokenEndpointAsync(_requestParams.RequestContext).ConfigureAwait(false);
bool useSha2 = _requestParams.AuthorityManager.Authority.AuthorityInfo.IsSha2CredentialSupported;
+
await _serviceBundle.Config.ClientCredential.AddConfidentialClientParametersAsync(
_oAuth2Client,
_requestParams.RequestContext.Logger,
@@ -162,12 +164,12 @@ await _serviceBundle.Config.ClientCredential.AddConfidentialClientParametersAsyn
// It should not be included for authorize request.
AddClaims();
- foreach (var kvp in additionalBodyParameters)
+ foreach (KeyValuePair kvp in additionalBodyParameters)
{
_oAuth2Client.AddBodyParameter(kvp.Key, kvp.Value);
}
- foreach (var kvp in _requestParams.AuthenticationScheme.GetTokenRequestParams())
+ foreach (KeyValuePair kvp in _requestParams.AuthenticationScheme.GetTokenRequestParams())
{
_oAuth2Client.AddBodyParameter(kvp.Key, kvp.Value);
}
@@ -294,7 +296,8 @@ await _oAuth2Client
return await _oAuth2Client.GetTokenAsync(
tokenEndpointWithQueryParams,
_requestParams.RequestContext,
- false, _requestParams.OnBeforeTokenRequestHandler).ConfigureAwait(false);
+ false, _requestParams.OnBeforeTokenRequestHandler)
+ .ConfigureAwait(false);
}
}
diff --git a/src/client/Microsoft.Identity.Client/Platforms/Features/SLC/IKeyGuardProxy.cs b/src/client/Microsoft.Identity.Client/Platforms/Features/SLC/IKeyGuardProxy.cs
new file mode 100644
index 0000000000..da90b062b9
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Platforms/Features/SLC/IKeyGuardProxy.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Security.Cryptography.X509Certificates;
+using System.Security.Cryptography;
+
+namespace Microsoft.Identity.Client.ManagedIdentity
+{
+ ///
+ /// Platform / OS specific logic.
+ ///
+ internal interface IKeyGuardProxy
+ {
+ ///
+ /// Load a CngKey with the given key provider
+ ///
+ ///
+ ///
+ ECDsa LoadCngKeyWithProvider(string keyProvider);
+
+ ///
+ /// Check if the given Cng key is protected by KeyGuard
+ ///
+ bool IsKeyGuardProtectedKey(CngKey cngKey);
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/Platforms/Features/SLC/KeyGuardProxy.cs b/src/client/Microsoft.Identity.Client/Platforms/Features/SLC/KeyGuardProxy.cs
new file mode 100644
index 0000000000..7ff4457212
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Platforms/Features/SLC/KeyGuardProxy.cs
@@ -0,0 +1,217 @@
+using System;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.ManagedIdentity;
+using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
+
+namespace Microsoft.Identity.Client.Platforms.Features.SLC
+{
+ ///
+ /// Platform / OS specific logic to manage KeyGuard keys.
+ ///
+ internal class KeyGuardProxy : IKeyGuardProxy
+ {
+ // The name of the key guard isolation property
+ private const string IsKeyGuardEnabledProperty = "Virtual Iso";
+
+ // The flag for using virtual isolation with CNG keys
+ private const CngKeyCreationOptions NCryptUseVirtualIsolationFlag = (CngKeyCreationOptions)0x00020000;
+
+ // Constants specifying the names for the key storage provider and key names
+ private const string MachineKeyName = "ResourceBindingMachineCredentialKey";
+ private const string SoftwareKeyName = "ResourceBindingUserCredentialKey";
+
+ // Logger instance for capturing log information
+ private readonly ILoggerAdapter _logger;
+
+ ///
+ /// cryptographic key type
+ ///
+ public CryptoKeyType CryptoKeyType { get; private set; } = CryptoKeyType.Undefined;
+
+ internal KeyGuardProxy(ILoggerAdapter logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ /// Loads a CngKey with the given key provider.
+ ///
+ ///
+ ///
+ ///
+ public ECDsa LoadCngKeyWithProvider(string keyProvider)
+ {
+ try
+ {
+ _logger.Verbose(() => "[Managed Identity] Initializing Cng Key.");
+
+ // Try to get the key material from machine key
+ if (TryGetCryptoKey(keyProvider, MachineKeyName, CngKeyOpenOptions.MachineKey, out ECDsa ecdsaKey))
+ {
+ _logger.Verbose(() => $"[Managed Identity] A machine key was found. Key Name : {MachineKeyName}. ");
+ return ecdsaKey;
+ }
+
+ // If machine key is not available, fall back to software key
+ if (TryGetCryptoKey(keyProvider, SoftwareKeyName, CngKeyOpenOptions.None, out ecdsaKey))
+ {
+ _logger.Verbose(() => $"[Managed Identity] A software key was found. Key Name : {SoftwareKeyName}. ");
+ return ecdsaKey;
+ }
+
+ _logger.Info("[Managed Identity] Machine / Software keys are not setup. " +
+ "Attempting to create a new key for Managed Identity.");
+
+ // Attempt to create a new key if none are available
+ if (TryCreateKeyMaterial(SoftwareKeyName, out ecdsaKey))
+ {
+ return ecdsaKey;
+ }
+
+ // All attempts for getting keys failed
+ // Now we should follow the legacy managed identity flow
+ _logger.Info("[Managed Identity] Machine / Software keys are not setup. " +
+ "Proceed to check for legacy managed identity sources.");
+
+ return null;
+ }
+ catch (Exception ex)
+ {
+ // Log the exception or handle it according to your error policy
+ throw new InvalidOperationException("Failed to load CngKey.", ex);
+ }
+ }
+
+ ///
+ /// Attempts to retrieve cryptographic key material for a specified key name and provider.
+ ///
+ /// The name of the key provider.
+ /// The name of the key.
+ /// The options for opening the CNG key.
+ /// The resulting key material.
+ ///
+ /// true if the key material is successfully retrieved; otherwise, false.
+ ///
+ public bool TryGetCryptoKey(
+ string keyProviderName,
+ string keyName,
+ CngKeyOpenOptions cngKeyOpenOptions,
+ out ECDsa ecdsaKey)
+ {
+ try
+ {
+ // Specify the optional flags for opening the key
+ CngKeyOpenOptions options = cngKeyOpenOptions;
+ options |= CngKeyOpenOptions.Silent;
+
+ // Open the key with the specified options
+ var cngKey = CngKey.Open(keyName, new CngProvider(keyProviderName), options);
+ ecdsaKey = new ECDsaCng(cngKey);
+
+ //check if the key is protected by KeyGuard
+ if (IsKeyGuardProtectedKey(cngKey))
+ {
+ // Check if the key name indicates user-specific key
+ if (keyName.Equals(SoftwareKeyName, StringComparison.OrdinalIgnoreCase))
+ {
+ CryptoKeyType = CryptoKeyType.KeyGuardUser;
+ }
+ else
+ {
+ CryptoKeyType = CryptoKeyType.KeyGuardMachine;
+ }
+
+ return true;
+ }
+ }
+ catch (CryptographicException ex)
+ {
+ // Check if the error message contains "Keyset does not exist"
+ if (ex.Message.IndexOf("Keyset does not exist", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ _logger.Info($"[Managed Identity] Key with name : {keyName} does not exist.");
+ }
+ else
+ {
+ // Handle other cryptographic errors
+ _logger.Verbose(() => $"[Managed Identity] Exception caught during key operations. " +
+ $"Error Mesage : {ex.Message}.");
+ }
+ }
+
+ ecdsaKey = null;
+ return false;
+ }
+
+ ///
+ /// Checks if the specified CNG key is protected by KeyGuard.
+ ///
+ /// The CNG key to check for KeyGuard protection.
+ ///
+ /// true if the key is protected by KeyGuard; otherwise, false.
+ ///
+ public bool IsKeyGuardProtectedKey(CngKey cngKey)
+ {
+ //Check to see if the KeyGuard Isolation flag was set in the key
+ if (!cngKey.HasProperty(IsKeyGuardEnabledProperty, CngPropertyOptions.None))
+ {
+ return false;
+ }
+
+ //if key guard isolation flag exist, check for the key guard property value existence
+ CngProperty property = cngKey.GetProperty(IsKeyGuardEnabledProperty, CngPropertyOptions.None);
+
+ // Retrieve the key guard property value
+ var keyGuardProperty = property.GetValue();
+
+ // Check if the key guard property exists and has a non-zero value
+ if (keyGuardProperty != null && keyGuardProperty.Length > 0)
+ {
+ if (keyGuardProperty[0] != 0)
+ {
+ // KeyGuard key is available; set the cryptographic key type accordingly
+ _logger.Info("[Managed Identity] KeyGuard key is available. ");
+ return true;
+ }
+ }
+
+ // KeyGuard key is not available
+ return false;
+ }
+
+ ///
+ /// Attempts to create a new cryptographic key and load it into a CngKey with the specified options.
+ ///
+ /// The name of the key to create.
+ /// Output parameter that returns the created ECDsa key, if successful.
+ /// True if the key was created and loaded successfully, false otherwise.
+ public bool TryCreateKeyMaterial(string keyName, out ECDsa ecdsaKey)
+ {
+ ecdsaKey = null;
+
+ try
+ {
+ var keyParams = new CngKeyCreationParameters
+ {
+ KeyUsage = CngKeyUsages.AllUsages,
+ Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider,
+ KeyCreationOptions = NCryptUseVirtualIsolationFlag | CngKeyCreationOptions.OverwriteExistingKey,
+ ExportPolicy = CngExportPolicies.None
+ };
+
+ using var cngKey = CngKey.Create(CngAlgorithm.ECDsaP256, keyName, keyParams);
+ ecdsaKey = new ECDsaCng(cngKey);
+ _logger.Info($"[Managed Identity] Key '{keyName}' created successfully with Virtual Isolation.");
+ CryptoKeyType = CryptoKeyType.KeyGuardUser;
+ return true; // Key creation was successful
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"[Managed Identity] Failed to create user key '{keyName}': {ex.Message}");
+ return false; // Key creation failed
+ }
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/Platforms/Features/SLC/ManagedIdentityCertificateProvider.cs b/src/client/Microsoft.Identity.Client/Platforms/Features/SLC/ManagedIdentityCertificateProvider.cs
new file mode 100644
index 0000000000..85042c624f
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Platforms/Features/SLC/ManagedIdentityCertificateProvider.cs
@@ -0,0 +1,206 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
+
+namespace Microsoft.Identity.Client.Platforms.Features.SLC
+{
+ ///
+ /// Provides X509_2 certificates and cryptographic key information for a Credential based
+ /// Managed Identity-supported Azure resource.
+ /// This class handles the retrieval or creation of X.509_2 certificates for authentication purposes,
+ /// including the determination of the cryptographic key type.
+ /// For more details, see https://aka.ms/msal-net-managed-identity.
+ ///
+ internal class ManagedIdentityCertificateProvider : IKeyMaterialManager
+ {
+ // Field to store the current crypto key type
+ private static CryptoKeyType s_cryptoKeyType = CryptoKeyType.Undefined;
+
+ // Name for the key storage provider and key names on Windows
+ private const string KeyProviderName = "Microsoft Software Key Storage Provider";
+
+ // Subject name for the binding certificate
+ private const string CertSubjectname = "ManagedIdentitySlcCertificate";
+
+ // Cache the binding certificate across instances
+ private static X509Certificate2 s_bindingCertificate;
+
+ // Lock object for ensuring thread safety when accessing key information
+ private readonly object _keyInfoLock = new();
+
+ // Logger instance for capturing log information
+ private readonly ILoggerAdapter _logger;
+
+ private readonly KeyGuardProxy _keyGuardProxy; // Use KeyGuardManager
+
+ private static bool s_isInitialized = false;
+
+ // Property to get or create the binding certificate from crypto key information
+ public X509Certificate2 BindingCertificate
+ {
+ get
+ {
+ if (!s_isInitialized)
+ {
+ s_bindingCertificate = GetOrCreateCertificateFromCryptoKeyInfo();
+ s_isInitialized = true;
+ }
+
+ return s_bindingCertificate;
+ }
+ }
+
+ // Property to expose the current crypto key type
+ public CryptoKeyType CryptoKeyType
+ {
+ get
+ {
+ if (!s_isInitialized)
+ {
+ s_bindingCertificate = GetOrCreateCertificateFromCryptoKeyInfo();
+ s_isInitialized = true;
+ }
+
+ return s_cryptoKeyType;
+ }
+ }
+
+ public ManagedIdentityCertificateProvider(ILoggerAdapter logger)
+ {
+ _logger = logger;
+ _keyGuardProxy = new KeyGuardProxy(logger);
+ }
+
+ ///
+ /// Retrieves or creates an X.509 certificate from crypto key information.
+ ///
+ ///
+ /// The X.509 certificate if available and still valid in the cache; otherwise, a new certificate is created.
+ ///
+ public X509Certificate2 GetOrCreateCertificateFromCryptoKeyInfo()
+ {
+ if (s_bindingCertificate != null && !CertificateNeedsRotation(s_bindingCertificate))
+ {
+ _logger.Verbose(() => "[Managed Identity] A non-expired cached binding certificate is available.");
+ return s_bindingCertificate;
+ }
+
+ lock (_keyInfoLock) // Lock to ensure thread safety
+ {
+ if (s_bindingCertificate != null && !CertificateNeedsRotation(s_bindingCertificate))
+ {
+ _logger.Verbose(() => "[Managed Identity] Another thread created the certificate while waiting for the lock.");
+ s_isInitialized = true;
+ return s_bindingCertificate;
+ }
+
+ // The cached certificate needs to be rotated or does not exist
+ ECDsa eCDsaKey = _keyGuardProxy.LoadCngKeyWithProvider(KeyProviderName);
+ s_cryptoKeyType = _keyGuardProxy.CryptoKeyType;
+
+ if (eCDsaKey != null)
+ {
+ s_bindingCertificate = CreateBindingCertificate(eCDsaKey);
+ s_isInitialized = true;
+ return s_bindingCertificate;
+ }
+ }
+
+ s_isInitialized = false;
+ return null;
+ }
+
+ ///
+ /// Determines if a given X.509 certificate needs rotation based on a percentage threshold.
+ ///
+ /// The X.509 certificate to evaluate.
+ /// The threshold percentage for considering rotation (default is 70%).
+ ///
+ /// True if the certificate needs rotation, false otherwise.
+ ///
+ public static bool CertificateNeedsRotation(X509Certificate2 certificate, double rotationPercentageThreshold = 70)
+ {
+ DateTime now = DateTime.UtcNow;
+
+ // Calculate the total duration of the certificate's validity
+ TimeSpan certificateLifetime = certificate.NotAfter - certificate.NotBefore;
+
+ // Calculate how much time has passed since the certificate's issuance
+ TimeSpan elapsedTime = now - certificate.NotBefore;
+
+ // Calculate the current percentage of the certificate's lifetime that has passed
+ double percentageElapsed = (elapsedTime.TotalMilliseconds / certificateLifetime.TotalMilliseconds) * 100.0;
+
+ // Check if the percentage elapsed exceeds the rotation threshold
+ return percentageElapsed >= rotationPercentageThreshold;
+ }
+
+ ///
+ /// Creates a binding certificate with a the key material for use in Managed Identity scenarios.
+ ///
+ /// The key used for creating the certificate.
+ /// The created binding certificate.
+ private X509Certificate2 CreateBindingCertificate(ECDsa eCDsaKey)
+ {
+ try
+ {
+ lock (_keyInfoLock) // Lock to ensure thread safety
+ {
+ _logger.Verbose(() => "[Managed Identity] Creating binding certificate " +
+ "with CNG key for credential endpoint.");
+
+ // Create a certificate request
+ CertificateRequest request = CreateCertificateRequest(CertSubjectname, eCDsaKey);
+
+ // Create a self-signed X.509 certificate
+ DateTimeOffset startDate = DateTimeOffset.UtcNow;
+ DateTimeOffset endDate = startDate.AddYears(5); //expiry
+
+ //Create the self signed cert
+ X509Certificate2 selfSigned = request.CreateSelfSigned(startDate, endDate);
+
+ if (!selfSigned.HasPrivateKey)
+ {
+ _logger.Error("[Managed Identity] The Certificate is missing the private key.");
+ throw new InvalidOperationException("The MTLS Certificate must include a private key.");
+ }
+
+ _logger.Verbose(() => $"[Managed Identity] Binding certificate (with cng key) created successfully. Has Private Key ? : {selfSigned.HasPrivateKey}");
+
+ return selfSigned;
+ }
+ }
+ catch (CryptographicException ex)
+ {
+ // log the exception
+ _logger.Error($"Error generating binding certificate: {ex.Message}");
+
+ throw new MsalClientException(MsalError.CertificateCreationFailed,
+ $"Failed to create Managed Identity binding certificate. Error : {ex.Message}");
+ }
+ }
+
+ ///
+ /// Creates a certificate request for the binding certificate using the specified subject name and ECDsa key.
+ ///
+ /// The subject name for the certificate (e.g., Common Name).
+ /// The ECDsa key to be associated with the certificate request.
+ /// The certificate request for the binding certificate.
+ private CertificateRequest CreateCertificateRequest(string subjectName, ECDsa ecdsaKey)
+ {
+ CertificateRequest certificateRequest = null;
+
+ _logger.Verbose(() => "[Managed Identity] Creating certificate request for the binding certificate.");
+
+ return certificateRequest = new CertificateRequest(
+ $"CN={subjectName}", // Common Name
+ ecdsaKey, // ECDsa key
+ HashAlgorithmName.SHA256); // Hash algorithm for the certificate
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/Platforms/net6/MsalJsonSerializerContext.cs b/src/client/Microsoft.Identity.Client/Platforms/net6/MsalJsonSerializerContext.cs
index 2dc4fa1fb2..f17ea7767f 100644
--- a/src/client/Microsoft.Identity.Client/Platforms/net6/MsalJsonSerializerContext.cs
+++ b/src/client/Microsoft.Identity.Client/Platforms/net6/MsalJsonSerializerContext.cs
@@ -40,6 +40,7 @@ namespace Microsoft.Identity.Client.Platforms.net6
[JsonSerializable(typeof(ManagedIdentityResponse))]
[JsonSerializable(typeof(ManagedIdentityErrorResponse))]
[JsonSerializable(typeof(OidcMetadata))]
+ [JsonSerializable(typeof(SlcCredentialResponse))]
[JsonSourceGenerationOptions]
internal partial class MsalJsonSerializerContext : JsonSerializerContext
{
diff --git a/src/client/Microsoft.Identity.Client/Platforms/netcore/NetCorePlatformProxy.cs b/src/client/Microsoft.Identity.Client/Platforms/netcore/NetCorePlatformProxy.cs
index efa145598d..3cbaf3330d 100644
--- a/src/client/Microsoft.Identity.Client/Platforms/netcore/NetCorePlatformProxy.cs
+++ b/src/client/Microsoft.Identity.Client/Platforms/netcore/NetCorePlatformProxy.cs
@@ -15,6 +15,9 @@
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Platforms.Features.DesktopOs;
+#if !NETSTANDARD
+using Microsoft.Identity.Client.Platforms.Features.SLC;
+#endif
using Microsoft.Identity.Client.Platforms.Shared.NetStdCore;
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
using Microsoft.Identity.Client.PlatformsCommon.Shared;
@@ -297,5 +300,14 @@ public override IDeviceAuthManager CreateDeviceAuthManager()
{
return new DeviceAuthManager(CryptographyManager);
}
+
+ public override IKeyMaterialManager GetKeyMaterialManager()
+ {
+#if NETSTANDARD
+ return NullKeyMaterialManager.Instance;
+#else
+ return new ManagedIdentityCertificateProvider(Logger);
+#endif
+ }
}
}
diff --git a/src/client/Microsoft.Identity.Client/Platforms/netdesktop/NetDesktopPlatformProxy.cs b/src/client/Microsoft.Identity.Client/Platforms/netdesktop/NetDesktopPlatformProxy.cs
index 152301fb11..e4f3732f1c 100644
--- a/src/client/Microsoft.Identity.Client/Platforms/netdesktop/NetDesktopPlatformProxy.cs
+++ b/src/client/Microsoft.Identity.Client/Platforms/netdesktop/NetDesktopPlatformProxy.cs
@@ -19,6 +19,9 @@
using Microsoft.Identity.Client.PlatformsCommon.Shared;
using Microsoft.Identity.Client.UI;
using Microsoft.Win32;
+#if NET472_OR_GREATER
+using Microsoft.Identity.Client.Platforms.Features.SLC;
+#endif
namespace Microsoft.Identity.Client.Platforms.netdesktop
{
@@ -248,5 +251,14 @@ public override IDeviceAuthManager CreateDeviceAuthManager()
}
public override bool BrokerSupportsWamAccounts => true;
+
+ public override IKeyMaterialManager GetKeyMaterialManager()
+ {
+#if NET472_OR_GREATER
+ return new ManagedIdentityCertificateProvider(Logger);
+#else
+ return NullKeyMaterialManager.Instance;
+#endif
+ }
}
}
diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/CryptoKeyType.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/CryptoKeyType.cs
new file mode 100644
index 0000000000..b37d7ff918
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/CryptoKeyType.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Microsoft.Identity.Client.PlatformsCommon.Interfaces
+{
+ ///
+ /// Defines an enumeration of different types of cryptographic keys that can be used within the application.
+ /// Each member of this enum represents a specific type of cryptographic key with a unique purpose.
+ ///
+ internal enum CryptoKeyType
+ {
+ ///
+ /// Represents an undefined cryptographic key type when MSAL is not able to identify the key type.
+ /// Used as a default value when no specific key type is applicable.
+ ///
+ Undefined = 0,
+
+ ///
+ /// Represents a cryptographic machine key protected by KeyGuard. This key is typically used for operations
+ /// requiring higher security enforced by the system hardware (e.g., to acquire Proof-of-Possession tokens).
+ ///
+ KeyGuardMachine = 1,
+
+ ///
+ /// Represents a cryptographic user key protected by KeyGuard. This key is user-specific and provides
+ /// the same security measures like a machine key, but is only used to acquire Continuous Access Evaluation tokens.
+ ///
+ KeyGuardUser = 2,
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IKeyMaterialManager.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IKeyMaterialManager.cs
new file mode 100644
index 0000000000..bc0d480acf
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IKeyMaterialManager.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.Identity.Client.PlatformsCommon.Interfaces
+{
+ internal interface IKeyMaterialManager
+ {
+ CryptoKeyType CryptoKeyType { get; }
+
+ X509Certificate2 BindingCertificate { get; }
+
+ X509Certificate2 GetOrCreateCertificateFromCryptoKeyInfo();
+
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IPlatformProxy.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IPlatformProxy.cs
index 00b1a446bb..8e1ce585f7 100644
--- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IPlatformProxy.cs
+++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Interfaces/IPlatformProxy.cs
@@ -110,5 +110,7 @@ internal interface IPlatformProxy
bool BrokerSupportsWamAccounts { get; }
IMsalHttpClientFactory CreateDefaultHttpClientFactory();
+
+ IKeyMaterialManager GetKeyMaterialManager();
}
}
diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/AbstractPlatformProxy.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/AbstractPlatformProxy.cs
index 8f2301896c..e57279af53 100644
--- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/AbstractPlatformProxy.cs
+++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/AbstractPlatformProxy.cs
@@ -154,6 +154,11 @@ public virtual ITokenCacheAccessor CreateTokenCacheAccessor(CacheOptions tokenCa
}
}
+ public virtual IKeyMaterialManager GetKeyMaterialManager()
+ {
+ return NullKeyMaterialManager.Instance;
+ }
+
///
public ICryptographyManager CryptographyManager => _cryptographyManager.Value;
diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/NullKeyMaterialManager.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/NullKeyMaterialManager.cs
new file mode 100644
index 0000000000..e7848ecbe6
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/NullKeyMaterialManager.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
+
+namespace Microsoft.Identity.Client.PlatformsCommon.Shared
+{
+ ///
+ /// Class to store crypto key information for a Managed Identity supported Azure resource.
+ /// For more details see https://aka.ms/msal-net-managed-identity
+ ///
+ internal class NullKeyMaterialManager : IKeyMaterialManager
+ {
+ // Singleton pattern
+ private static readonly Lazy s_lazy = new(() => new NullKeyMaterialManager());
+
+ public static NullKeyMaterialManager Instance { get { return s_lazy.Value; } }
+
+ private NullKeyMaterialManager()
+ {
+ }
+
+ public X509Certificate2 BindingCertificate => null;
+ CryptoKeyType IKeyMaterialManager.CryptoKeyType => CryptoKeyType.Undefined;
+
+ public X509Certificate2 GetOrCreateCertificateFromCryptoKeyInfo()
+ {
+ return null;
+ }
+
+ public ECDsa GetCngKey()
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs
index 64c6a56c4c..cc7470bfb7 100644
--- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs
+++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs
@@ -2,7 +2,9 @@
// Licensed under the MIT License.
using System;
+using System.Collections.Concurrent;
using System.Net.Http;
+using System.Security.Cryptography.X509Certificates;
using Microsoft.Identity.Client.Http;
namespace Microsoft.Identity.Client.PlatformsCommon.Shared
@@ -14,15 +16,36 @@ namespace Microsoft.Identity.Client.PlatformsCommon.Shared
/// .NET should use the IHttpClientFactory, but MSAL cannot take a dependency on it.
/// .NET should use SocketHandler, but UseDefaultCredentials doesn't work with it
///
- internal class SimpleHttpClientFactory : IMsalHttpClientFactory
+ internal class SimpleHttpClientFactory : IMsalMtlsHttpClientFactory
{
//Please see (https://aka.ms/msal-httpclient-info) for important information regarding the HttpClient.
- private static readonly Lazy s_httpClient = new Lazy(InitializeClient);
+ private static readonly ConcurrentDictionary s_httpClient = new ConcurrentDictionary();
+ private static readonly object s_cacheLock = new object();
- private static HttpClient InitializeClient()
+ private static HttpClient CreateNonMtlsClient()
{
- var httpClient = new HttpClient(new HttpClientHandler() {
- /* important for IWA */ UseDefaultCredentials = true });
+ var httpClient = new HttpClient(new HttpClientHandler()
+ {
+ /* important for IWA */
+ UseDefaultCredentials = true
+ });
+ HttpClientConfig.ConfigureRequestHeadersAndSize(httpClient);
+
+ return httpClient;
+ }
+
+ private static HttpClient CreateMtlsHttpClient(X509Certificate2 bindingCertificate)
+ {
+ if (s_httpClient.Count > 1000)
+ CheckAndClearCache();
+
+ //Create an HttpClientHandler and configure it to use the client certificate
+ HttpClientHandler handler = new();
+
+#if SUPPORTS_MTLS
+ handler.ClientCertificates.Add(bindingCertificate);
+#endif
+ var httpClient = new HttpClient(handler);
HttpClientConfig.ConfigureRequestHeadersAndSize(httpClient);
return httpClient;
@@ -30,7 +53,29 @@ private static HttpClient InitializeClient()
public HttpClient GetHttpClient()
{
- return s_httpClient.Value;
+ return s_httpClient.GetOrAdd("non_mtls", CreateNonMtlsClient());
+ }
+
+ public HttpClient GetHttpClient(X509Certificate2 x509Certificate2)
+ {
+ if (x509Certificate2 == null)
+ {
+ return GetHttpClient();
+ }
+
+ string key = x509Certificate2.Thumbprint;
+ return s_httpClient.GetOrAdd(key, CreateMtlsHttpClient(x509Certificate2));
+ }
+
+ private static void CheckAndClearCache()
+ {
+ lock (s_cacheLock)
+ {
+ if (s_httpClient.Count > 1000)
+ {
+ s_httpClient.Clear();
+ }
+ }
}
}
}
diff --git a/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs b/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs
index 59ab567fc1..4a5835409d 100644
--- a/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs
+++ b/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs
@@ -30,7 +30,7 @@ public enum ApiIds
// MSAL 4.51.0+
RemoveOboTokens = 1014,
- // The API IDs for managed identity will not be found in HTTP telemetry,
+ // The API IDs for legacy managed identity will not be found in HTTP telemetry,
// as we don't hit eSTS for managed identity calls.
AcquireTokenForSystemAssignedManagedIdentity = 1015,
AcquireTokenForUserAssignedManagedIdentity = 1016,
@@ -135,7 +135,7 @@ public string TokenTypeString
public CacheLevel CacheLevel { get; set; }
public string MsalRuntimeTelemetry { get; set; }
-
+
public static bool IsLongRunningObo(ApiIds apiId) => apiId == ApiIds.InitiateLongRunningObo || apiId == ApiIds.AcquireTokenInLongRunningObo;
public static bool IsOnBehalfOfRequest(ApiIds apiId) => apiId == ApiIds.AcquireTokenOnBehalfOf || IsLongRunningObo(apiId);
diff --git a/src/client/Microsoft.Identity.Client/Utils/ScopeHelper.cs b/src/client/Microsoft.Identity.Client/Utils/ScopeHelper.cs
index 741bae812c..3909c046ef 100644
--- a/src/client/Microsoft.Identity.Client/Utils/ScopeHelper.cs
+++ b/src/client/Microsoft.Identity.Client/Utils/ScopeHelper.cs
@@ -21,7 +21,6 @@ public static string OrderScopesAlphabetically(string originalScopes)
return string.Join(" ", split);
}
-
public static bool ScopeContains(ISet outerSet, IEnumerable possibleContainedSet)
{
foreach (string key in possibleContainedSet)
diff --git a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs
index 323d429869..160509f821 100644
--- a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs
+++ b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -15,51 +14,51 @@
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.TelemetryCore;
using Microsoft.Identity.Client.Utils;
-
namespace Microsoft.Identity.Client.WsTrust
{
internal class WsTrustWebRequestManager : IWsTrustWebRequestManager
{
private readonly IHttpManager _httpManager;
-
public WsTrustWebRequestManager(IHttpManager httpManager)
{
_httpManager = httpManager;
}
-
///
public async Task GetMexDocumentAsync(string federationMetadataUrl, RequestContext requestContext, string federationMetadata = null)
{
MexDocument mexDoc;
-
if (!string.IsNullOrEmpty(federationMetadata))
{
mexDoc = new MexDocument(federationMetadata);
requestContext.Logger.Info(() => $"MEX document fetched and parsed from provided federation metadata");
return mexDoc;
}
+ Dictionary msalIdParams = MsalIdHelper.GetMsalIdParameters(requestContext.Logger);
+ var uri = new UriBuilder(federationMetadataUrl);
- IDictionary msalIdParams = MsalIdHelper.GetMsalIdParameters(requestContext.Logger);
-
- var uri = new UriBuilder(federationMetadataUrl);
- HttpResponse httpResponse = await _httpManager.SendGetAsync(
- uri.Uri,
- msalIdParams,
- requestContext.Logger,
- cancellationToken: requestContext.UserCancellationToken).ConfigureAwait(false);
+ HttpResponse httpResponse = await _httpManager.SendRequestAsync(
+ uri.Uri,
+ msalIdParams,
+ body: null,
+ HttpMethod.Get,
+ logger: requestContext.Logger,
+ doNotThrow: false,
+ retry: true,
+ mtlsCertificate: null,
+ requestContext.UserCancellationToken)
+ .ConfigureAwait(false);
if (httpResponse.StatusCode != System.Net.HttpStatusCode.OK)
{
string message = string.Format(CultureInfo.CurrentCulture,
MsalErrorMessage.HttpRequestUnsuccessful + "See https://aka.ms/msal-net-ropc for more information. ",
(int)httpResponse.StatusCode, httpResponse.StatusCode);
-
requestContext.Logger.ErrorPii(
string.Format(MsalErrorMessage.RequestFailureErrorMessagePii,
requestContext.ApiEvent?.ApiIdString,
requestContext.ServiceBundle.Config.Authority.AuthorityInfo.CanonicalAuthority,
requestContext.ServiceBundle.Config.ClientId),
string.Format(MsalErrorMessage.RequestFailureErrorMessage,
- requestContext.ApiEvent?.ApiIdString,
+ requestContext.ApiEvent?.ApiIdString,
requestContext.ServiceBundle.Config.Authority.AuthorityInfo.Host));
throw MsalServiceExceptionFactory.FromHttpResponse(
MsalError.AccessingWsMetadataExchangeFailed,
@@ -91,11 +90,16 @@ public async Task GetWsTrustResponseAsync(
wsTrustRequest,
Encoding.UTF8, "application/soap+xml");
- HttpResponse resp = await _httpManager.SendPostForceResponseAsync(wsTrustEndpoint.Uri,
- headers,
- body,
- requestContext.Logger,
- cancellationToken: requestContext.UserCancellationToken).ConfigureAwait(false);
+ HttpResponse resp = await _httpManager.SendRequestAsync(
+ wsTrustEndpoint.Uri,
+ headers,
+ body: body,
+ HttpMethod.Post,
+ logger: requestContext.Logger,
+ doNotThrow: true,
+ retry: true,
+ mtlsCertificate: null,
+ requestContext.UserCancellationToken).ConfigureAwait(false);
if (resp.StatusCode != System.Net.HttpStatusCode.OK)
{
@@ -108,32 +112,26 @@ public async Task GetWsTrustResponseAsync(
{
errorMessage = resp.Body;
}
-
- requestContext.Logger.ErrorPii(LogMessages.WsTrustRequestFailed + $"Status code: {resp.StatusCode} \nError message: {errorMessage}",
+ requestContext.Logger.ErrorPii(LogMessages.WsTrustRequestFailed + $"Status code: {resp.StatusCode} \nError message: {errorMessage}",
LogMessages.WsTrustRequestFailed + $"Status code: {resp.StatusCode}");
-
string message = string.Format(
CultureInfo.CurrentCulture,
MsalErrorMessage.FederatedServiceReturnedErrorTemplate,
wsTrustEndpoint.Uri,
errorMessage);
-
throw MsalServiceExceptionFactory.FromHttpResponse(
MsalError.FederatedServiceReturnedError,
message,
resp);
}
-
try
{
var wsTrustResponse = WsTrustResponse.CreateFromResponse(resp.Body, wsTrustEndpoint.Version);
-
- if (wsTrustResponse == null)
+ if (wsTrustResponse == null)
{
requestContext.Logger.ErrorPii("Token not found in the ws trust response. See response for more details: \n" + resp.Body, "Token not found in WS-Trust response.");
throw new MsalClientException(MsalError.ParsingWsTrustResponseFailed, MsalErrorMessage.ParsingWsTrustResponseFailedDueToConfiguration);
}
-
return wsTrustResponse;
}
catch (System.Xml.XmlException ex)
@@ -143,45 +141,44 @@ public async Task GetWsTrustResponseAsync(
MsalErrorMessage.ParsingWsTrustResponseFailedErrorTemplate,
wsTrustEndpoint.Uri,
resp.Body);
-
throw new MsalClientException(
MsalError.ParsingWsTrustResponseFailed, message, ex);
}
}
-
public async Task GetUserRealmAsync(
string userRealmUriPrefix,
string userName,
RequestContext requestContext)
{
requestContext.Logger.Info("Sending request to userrealm endpoint. ");
-
- IDictionary msalIdParams = MsalIdHelper.GetMsalIdParameters(requestContext.Logger);
-
+ Dictionary msalIdParams = MsalIdHelper.GetMsalIdParameters(requestContext.Logger);
var uri = new UriBuilder(userRealmUriPrefix + userName + "?api-version=1.0").Uri;
-
- var httpResponse = await _httpManager.SendGetAsync(
- uri,
- msalIdParams,
- requestContext.Logger,
- cancellationToken: requestContext.UserCancellationToken).ConfigureAwait(false);
-
+
+ var httpResponse = await _httpManager.SendRequestAsync(
+ uri,
+ msalIdParams,
+ body: null,
+ HttpMethod.Get,
+ logger: requestContext.Logger,
+ doNotThrow: false,
+ retry: true,
+ mtlsCertificate: null,
+ requestContext.UserCancellationToken)
+ .ConfigureAwait(false);
if (httpResponse.StatusCode == System.Net.HttpStatusCode.OK)
{
return JsonHelper.DeserializeFromJson(httpResponse.Body);
}
-
string message = string.Format(CultureInfo.CurrentCulture,
MsalErrorMessage.HttpRequestUnsuccessful,
(int)httpResponse.StatusCode, httpResponse.StatusCode);
-
requestContext.Logger.ErrorPii(
string.Format(MsalErrorMessage.RequestFailureErrorMessagePii,
requestContext.ApiEvent?.ApiIdString,
requestContext.ServiceBundle.Config.Authority.AuthorityInfo.CanonicalAuthority,
requestContext.ServiceBundle.Config.ClientId),
string.Format(MsalErrorMessage.RequestFailureErrorMessage,
- requestContext.ApiEvent?.ApiIdString,
+ requestContext.ApiEvent?.ApiIdString,
requestContext.ServiceBundle.Config.Authority.AuthorityInfo.Host));
throw MsalServiceExceptionFactory.FromHttpResponse(
MsalError.UserRealmDiscoveryFailed,
diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/KeyMaterialManagerMock.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/KeyMaterialManagerMock.cs
new file mode 100644
index 0000000000..185a4a4373
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/KeyMaterialManagerMock.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
+
+namespace Microsoft.Identity.Test.Common.Core.Mocks
+{
+ ///
+ /// Mock implementation of the IKeyMaterialManager interface for testing purposes.
+ ///
+ internal class KeyMaterialManagerMock : IKeyMaterialManager
+ {
+ ///
+ /// Initializes a new instance of the KeyMaterialManagerMock class.
+ ///
+ /// The X509 certificate used for binding.
+ /// The type of cryptographic key used.
+ public KeyMaterialManagerMock(X509Certificate2 bindingCertificate, CryptoKeyType cryptoKeyType)
+ {
+ BindingCertificate = bindingCertificate;
+ CryptoKeyType = cryptoKeyType;
+ }
+
+ ///
+ /// Gets the X509 certificate used for binding.
+ ///
+ public X509Certificate2 BindingCertificate { get; }
+
+ ///
+ /// Gets the type of cryptographic key used.
+ ///
+ public CryptoKeyType CryptoKeyType { get; }
+
+ public ECDsa GetCngKey()
+ {
+ return null;
+ }
+
+ public X509Certificate2 GetOrCreateCertificateFromCryptoKeyInfo()
+ {
+ return null;
+ }
+ }
+}
diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs
index a42e055e84..1d6411df46 100644
--- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs
+++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs
@@ -11,7 +11,7 @@
using Microsoft.Identity.Test.Unit;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.OAuth2;
-using Microsoft.Identity.Client.ManagedIdentity;
+using Microsoft.Identity.Client.AppConfig;
namespace Microsoft.Identity.Test.Common.Core.Mocks
{
@@ -70,12 +70,12 @@ public static string GetTokenResponseWithNoOidClaim()
public static string GetDefaultTokenResponse(string accessToken = TestConstants.ATSecret, string refreshToken = TestConstants.RTSecret)
{
- return
- "{\"token_type\":\"Bearer\",\"expires_in\":\"3599\",\"refresh_in\":\"2400\",\"scope\":" +
- "\"r1/scope1 r1/scope2\",\"access_token\":\"" + accessToken + "\"" +
- ",\"refresh_token\":\"" + refreshToken + "\",\"client_info\"" +
- ":\"" + CreateClientInfo() + "\",\"id_token\"" +
- ":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) + "\"}";
+ return
+ "{\"token_type\":\"Bearer\",\"expires_in\":\"3599\",\"refresh_in\":\"2400\",\"scope\":" +
+ "\"r1/scope1 r1/scope2\",\"access_token\":\"" + accessToken + "\"" +
+ ",\"refresh_token\":\"" + refreshToken + "\",\"client_info\"" +
+ ":\"" + CreateClientInfo() + "\",\"id_token\"" +
+ ":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) + "\"}";
}
public static string GetPopTokenResponse()
@@ -130,24 +130,66 @@ public static string GetMsiImdsSuccessfulResponse()
"\"ext_expires_in\":\"12345\",\"token_type\":\"Bearer\"}";
}
- public static string GetMsiErrorResponse(ManagedIdentitySource source)
+ public static string GetMsiErrorResponse()
+ {
+ return "{\"statusCode\":\"500\",\"message\":\"An unexpected error occured while fetching the AAD Token.\",\"correlationId\":\"7d0c9763-ff1d-4842-a3f3-6d49e64f4513\"}";
+ }
+
+ public static string GetSuccessfulCredentialResponse(
+ string credential = "managed-identity-credential",
+ ManagedIdentityIdType identityType = ManagedIdentityIdType.SystemAssigned,
+ string client_id = "2d0d13ad-3a4d-4cfd-98f8-f20621d55ded",
+ long expires_on = 0,
+ string regional_token_url = "https://centraluseuap.mtlsauth.microsoft.com",
+ string tenant_id = "72f988bf-86f1-41af-91ab-2d7cd011db47")
{
- switch (source)
+ var identityTypeString = identityType.ToString();
+
+ if (expires_on == 0)
{
- case ManagedIdentitySource.AppService:
- return "{\"statusCode\":500,\"message\":\"An unexpected error occured while fetching the AAD Token.\",\"correlationId\":\"4ce26535-1769-4001-96e3-9019ce00922d\"}";
+ long currentUnixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ expires_on = currentUnixTimestamp + 3600; // Add one hour (3600 seconds) for example
+ }
- case ManagedIdentitySource.Imds:
- case ManagedIdentitySource.AzureArc:
- case ManagedIdentitySource.ServiceFabric:
- return "{\"error\":\"invalid_resource\",\"error_description\":\"AADSTS500011: The resource principal named scope was not found in the tenant named Microsoft. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.\\r\\nTrace ID: GUID\\r\\nCorrelation ID: GUID\\r\\nTimestamp: 2024-02-14 23:11:50Z\",\"error_codes\":\"[500011]\",\"timestamp\":\"2022-11-10 23:11:50Z\",\"trace_id\":\"GUID\",\"correlation_id\":\"GUID\",\"error_uri\":\"errorUri\"}";
+ long refresh_in = expires_on / 2;
- case ManagedIdentitySource.CloudShell:
- return "{\"error\":{\"code\":\"AudienceNotSupported\",\"message\":\"Audience scope is not a supported MSI token audience.Supported audiences:https://management.core.windows.net/,https://management.azure.com/,https://graph.windows.net/,https://vault.azure.net,https://datalake.azure.net/,https://outlook.office365.com/,https://graph.microsoft.com/,https://batch.core.windows.net/,https://analysis.windows.net/powerbi/api,https://storage.azure.com/,https://rest.media.azure.net,https://api.loganalytics.io,https://ossrdbms-aad.database.windows.net,https://www.yammer.com,https://digitaltwins.azure.net,0b07f429-9f4b-4714-9392-cc5e8e80c8b0,822c8694-ad95-4735-9c55-256f7db2f9b4,https://dev.azuresynapse.net,https://database.windows.net,https://quantum.microsoft.com,https://iothubs.azure.net,2ff814a6-3304-4ab8-85cb-cd0e6f879c1d,https://azuredatabricks.net/,ce34e7e5-485f-4d76-964f-b3d2b16d1e4f,https://azure-devices-provisioning.net,https://managedhsm.azure.net,499b84ac-1321-427f-aa17-267ca6975798,https://api.adu.microsoft.com/,https://purview.azure.net/,6dae42f8-4368-4678-94ff-3960e28e3630\"}}";
+ return "{\"client_id\":\"" + client_id + "\",\"credential\":\"" + credential + "\",\"expires_on\":" + expires_on + ",\"identity_type\":\"" + identityTypeString + "\",\"refresh_in\":" + refresh_in + ",\"regional_token_url\":\"" + regional_token_url + "\",\"tenant_id\":\"" + tenant_id + "\"}";
+ }
- default:
- return "";
- }
+ public static string GetSuccessfulMtlsResponse()
+ {
+ return "{\"token_type\":\"Bearer\",\"expires_in\":86399,\"ext_expires_in\":86399,\"access_token\":\"some-token\"}";
+ }
+
+ public static string GetMtlsInvalidResourceError()
+ {
+ return @"{""error"":""invalid_resource"",
+ ""error_description"":""AADSTS500011: The resource principal named https://graph.microsoft.com/user.read was not found in the tenant named Cross Cloud B2B Test Tenant. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant. Trace ID: 9d8cb0bf-7e34-40fd-babc-f6ff018a1800 Correlation ID: 42186e1b-17eb-46fb-b5b7-4c43cae4d336 Timestamp: 2023-12-08 22:20:25Z"",
+ ""error_codes"":[500011],
+ ""timestamp"":""2023-12-08 22:20:25Z"",
+ ""trace_id"":""9d8cb0bf-7e34-40fd-babc-f6ff018a1800"",
+ ""correlation_id"":""42186e1b-17eb-46fb-b5b7-4c43cae4d336"",
+ ""error_uri"":""https://eastus2euap.mtlsauth.microsoft.com/error?code=500011""}";
+ }
+
+ public static string GetMtlsInvalidScopeError70011()
+ {
+ return @"{""error"":""invalid_scope"",
+ ""error_description"":""AADSTS70011: The provided request must include a 'scope' input parameter. The provided value for the input parameter 'scope' is not valid. The scope user.read/.default is not valid. Trace ID: 9e8a0bd6-fb1b-45cf-8e00-95c2c73e1400 Correlation ID: 6ce4a5ab-87a1-4985-b06d-5ab08b5fa924 Timestamp: 2023-12-08 21:56:44Z"",
+ ""error_codes"":[70011],
+ ""timestamp"":""2023-12-08 21:56:44Z"",
+ ""trace_id"":""9e8a0bd6-fb1b-45cf-8e00-95c2c73e1400"",
+ ""correlation_id"":""6ce4a5ab-87a1-4985-b06d-5ab08b5fa924""}";
+ }
+
+ public static string GetMtlsInvalidScopeError1002012()
+ {
+ return @"{""error"":""invalid_scope"",
+ ""error_description"":""AADSTS1002012: The provided value for scope user.read is not valid. Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI). Trace ID: 8575f1d5-0144-4d71-87c8-2df9f1e30000 Correlation ID: a5469466-6c01-40e0-abf8-302d09c991e3 Timestamp: 2023-12-08 22:11:08Z"",
+ ""error_codes"":[1002012],
+ ""timestamp"":""2023-12-08 22:11:08Z"",
+ ""trace_id"":""8575f1d5-0144-4d71-87c8-2df9f1e30000"",
+ ""correlation_id"":""a5469466-6c01-40e0-abf8-302d09c991e3""}";
}
public static string GetMsiImdsErrorResponse()
@@ -155,12 +197,179 @@ public static string GetMsiImdsErrorResponse()
return "{\"error\":\"invalid_resource\"," +
"\"error_description\":\"AADSTS500011: The resource principal named user.read was not found in the tenant named Microsoft. " +
"This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. " +
- "You might have sent your authentication request to the wrong tenant.\\r\\nTrace ID: 2dff494a-0226-4f41-8859-d9f560ca8903" +
- "\\r\\nCorrelation ID: 77145480-bc5a-4ebe-ae4d-e4a8b7d727cf\\r\\nTimestamp: 2022-11-10 23:12:37Z\"," +
+ "You might have sent your authentication request to the wrong tenant.\r\nTrace ID: 2dff494a-0226-4f41-8859-d9f560ca8903" +
+ "\r\nCorrelation ID: 77145480-bc5a-4ebe-ae4d-e4a8b7d727cf\r\nTimestamp: 2022-11-10 23:12:37Z\"," +
"\"error_codes\":[500011],\"timestamp\":\"2022-11-10 23:12:37Z\",\"trace_id\":\"2dff494a-0226-4f41-8859-d9f560ca8903\"," +
"\"correlation_id\":\"77145480-bc5a-4ebe-ae4d-e4a8b7d727cf\",\"error_uri\":\"https://westus2.login.microsoft.com/error?code=500011\"}";
}
+ public static string InvalidTenantError900023()
+ {
+ return @"{
+ ""error"":""invalid_request"",
+ ""error_description"":""AADSTS900023: Specified tenant identifier 'invalid_tenant' is neither a valid DNS name, nor a valid external domain. Trace ID: f38df5f2-84c4-4195-bad6-8eca059b0b00 Correlation ID: e318f766-8581-445a-97fb-419f80d98d8b Timestamp: 2023-12-11 22:52:53Z"",
+ ""error_codes"":[900023],
+ ""timestamp"":""2023-12-11 22:52:53Z"",
+ ""trace_id"":""f38df5f2-84c4-4195-bad6-8eca059b0b00"",
+ ""correlation_id"":""e318f766-8581-445a-97fb-419f80d98d8b"",
+ ""error_uri"":""https://centraluseuap.mtlsauth.microsoft.com/error?code=900023""
+ }";
+ }
+
+ public static string WrongTenantError700016()
+ {
+ return @"{
+ ""error"":""unauthorized_client"",
+ ""error_description"":""AADSTS700016: Application with identifier '833aa854-2811-4f90-9620-c38070f595d7' was not found in the directory 'MSIDLAB4'. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant. Trace ID: 68b0d98d-52e8-4e45-9282-9b3b09fc1800 Correlation ID: 75673189-3db2-408b-8384-16860ee0c0f0 Timestamp: 2023-12-11 22:54:25Z"",
+ ""error_codes"":[700016],
+ ""timestamp"":""2023-12-11 22:54:25Z"",
+ ""trace_id"":""68b0d98d-52e8-4e45-9282-9b3b09fc1800"",
+ ""correlation_id"":""75673189-3db2-408b-8384-16860ee0c0f0"",
+ ""error_uri"":""https://centraluseuap.mtlsauth.microsoft.com/error?code=700016""
+ }";
+ }
+
+ public static string WrongMtlsUrlError50171()
+ {
+ return @"{
+ ""error"":""invalid_client"",
+ ""error_description"":""AADSTS50171: The given audience can only be used in Mutual-TLS token calls. Trace ID: e350f752-0a39-43c2-a9a2-cbd7ff4a6f00 Correlation ID: 26bb13de-d2cf-4f8f-9f36-d7611c00fecb Timestamp: 2023-12-11 22:58:32Z"",
+ ""error_codes"":[50171],
+ ""timestamp"":""2023-12-11 22:58:32Z"",
+ ""trace_id"":""e350f752-0a39-43c2-a9a2-cbd7ff4a6f00"",
+ ""correlation_id"":""26bb13de-d2cf-4f8f-9f36-d7611c00fecb""
+ }";
+ }
+
+ public static string SendTenantIdInCredentialValueError50027()
+ {
+ return @"{
+ ""error"":""invalid_request"",
+ ""error_description"":""AADSTS50027: JWT token is invalid or malformed. Trace ID: 6ca706cd-c0a1-4ec2-acb1-541b5a579a00 Correlation ID: 52955596-2fe6-43c6-b087-6038942c8254 Timestamp: 2023-12-11 23:02:08Z"",
+ ""error_codes"":[50027],
+ ""timestamp"":""2023-12-11 23:02:08Z"",
+ ""trace_id"":""6ca706cd-c0a1-4ec2-acb1-541b5a579a00"",
+ ""correlation_id"":""52955596-2fe6-43c6-b087-6038942c8254"",
+ ""error_uri"":""https://mtlsauth.microsoft.com/error?code=50027""
+ }";
+ }
+
+ public static string BadCredNoIssError90014()
+ {
+ return @"{
+ ""error"":""invalid_request"",
+ ""error_description"":""AADSTS90014: The required field 'iss' is missing from the credential. Ensure that you have all the necessary parameters for the login request. Trace ID: 605439e8-8f0e-43f5-9887-5281a05a5200 Correlation ID: abc63349-b90e-4b15-8fb7-edc9326ed3c8 Timestamp: 2023-12-11 23:14:38Z"",
+ ""error_codes"":[90014],
+ ""timestamp"":""2023-12-11 23:14:38Z"",
+ ""trace_id"":""605439e8-8f0e-43f5-9887-5281a05a5200"",
+ ""correlation_id"":""abc63349-b90e-4b15-8fb7-edc9326ed3c8"",
+ ""error_uri"":""https://mtlsauth.microsoft.com/error?code=90014""
+ }";
+ }
+
+ public static string BadCredNoAudError90014()
+ {
+ return @"{
+ ""error"":""invalid_request"",
+ ""error_description"":""AADSTS90014: The required field 'aud' is missing from the credential. Ensure that you have all the necessary parameters for the login request. Trace ID: 0b1cc102-98b7-4fa5-a11a-82520fa85a00 Correlation ID: 23811f20-96bb-4900-a1a3-6368ef8890b2 Timestamp: 2023-12-11 23:16:15Z"",
+ ""error_codes"":[90014],
+ ""timestamp"":""2023-12-11 23:16:15Z"",
+ ""trace_id"":""0b1cc102-98b7-4fa5-a11a-82520fa85a00"",
+ ""correlation_id"":""23811f20-96bb-4900-a1a3-6368ef8890b2"",
+ ""error_uri"":""https://mtlsauth.microsoft.com/error?code=90014""
+ }";
+ }
+
+ public static string BadCredBadAlgError5002738()
+ {
+ return @"{
+ ""error"":""invalid_client"",
+ ""error_description"":""AADSTS5002738: Invalid JWT token. 'HS256' is not a supported signature algorithm. Supported signing algorithms are: 'RS256,RS384,RS512' Trace ID: 2ed12465-8044-44af-bd27-b73b27e04a00 Correlation ID: bc26e294-ed13-4e6f-a225-28cdec2cc519 Timestamp: 2023-12-11 23:18:06Z"",
+ ""error_codes"":[5002738],
+ ""timestamp"":""2023-12-11 23:18:06Z"",
+ ""trace_id"":""2ed12465-8044-44af-bd27-b73b27e04a00"",
+ ""correlation_id"":""bc26e294-ed13-4e6f-a225-28cdec2cc519"",
+ ""error_uri"":""https://mtlsauth.microsoft.com/error?code=5002738""
+ }";
+ }
+
+ public static string BadCredMissingSha1Error5002723()
+ {
+ return @"{
+ ""error"":""invalid_client"",
+ ""error_description"":""AADSTS5002723: Invalid JWT token. No certificate SHA-1 thumbprint, certificate SHA-256 thumbprint, nor keyId specified in token header. Trace ID: 3ce71c90-8d35-4413-bedb-73337ec40c00 Correlation ID: 540e9fb1-db53-4b10-a0ca-047d03b97d10 Timestamp: 2023-12-11 23:51:16Z"",
+ ""error_codes"":[5002723],
+ ""timestamp"":""2023-12-11 23:51:16Z"",
+ ""trace_id"":""3ce71c90-8d35-4413-bedb-73337ec40c00"",
+ ""correlation_id"":""540e9fb1-db53-4b10-a0ca-047d03b97d10"",
+ ""error_uri"":""https://mtlsauth.microsoft.com/error?code=5002723""
+ }";
+ }
+
+ public static string BadTimeRangeError700024()
+ {
+ return @"{
+ ""error"":""invalid_client"",
+ ""error_description"":""AADSTS700024: Client assertion is not within its valid time range. Current time: 2023-12-11T23:52:19.6223401Z, assertion valid from 2018-01-18T01:30:22.0000000Z, expiry time of assertion 1970-01-01T00:00:00.0000000Z. Review the documentation at https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials . Trace ID: 2486d2c5-63a7-44f5-bb09-05e4c5494000 Correlation ID: fc5f1331-e3ef-44cb-b478-909a171010ab Timestamp: 2023-12-11 23:52:19Z"",
+ ""error_codes"":[700024],
+ ""timestamp"":""2023-12-11 23:52:19Z"",
+ ""trace_id"":""2486d2c5-63a7-44f5-bb09-05e4c5494000"",
+ ""correlation_id"":""fc5f1331-e3ef-44cb-b478-909a171010ab"",
+ ""error_uri"":""https://mtlsauth.microsoft.com/error?code=700024""
+ }";
+ }
+
+ public static string IdentifierMismatchError700021()
+ {
+ return @"{
+ ""error"":""invalid_client"",
+ ""error_description"":""AADSTS700021: Client assertion application identifier doesn't match 'client_id' parameter. Review the documentation at https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials . Trace ID: 1180e895-2f6b-4504-b0cf-f49632647100 Correlation ID: 88c237d8-7867-4e68-89e4-bc5a6d3b2159 Timestamp: 2023-12-11 23:55:14Z"",
+ ""error_codes"":[700021],
+ ""timestamp"":""2023-12-11 23:55:14Z"",
+ ""trace_id"":""1180e895-2f6b-4504-b0cf-f49632647100"",
+ ""correlation_id"":""88c237d8-7867-4e68-89e4-bc5a6d3b2159"",
+ ""error_uri"":""https://mtlsauth.microsoft.com/error?code=700021""
+ }";
+ }
+
+ public static string MissingCertError392200()
+ {
+ return @"{
+ ""error"":""invalid_request"",
+ ""error_description"":""AADSTS392200: Client certificate is missing from the request. Trace ID: 35f8d355-5be8-4028-83e5-aeb609b8d500 Correlation ID: e10c5bea-3b7e-42a2-a251-705d6e7aa48d Timestamp: 2023-12-12 00:11:34Z"",
+ ""error_codes"":[392200],
+ ""timestamp"":""2023-12-12 00:11:34Z"",
+ ""trace_id"":""35f8d355-5be8-4028-83e5-aeb609b8d500"",
+ ""correlation_id"":""e10c5bea-3b7e-42a2-a251-705d6e7aa48d"",
+ ""error_uri"":""https://mtlsauth.microsoft.com/error?code=392200""
+ }";
+ }
+
+ public static string ExpiredCertError392204()
+ {
+ return @"{
+ ""error"":""invalid_client"",
+ ""error_description"":""AADSTS392204: The provided client certificate has expired. Trace ID: 44b6984d-e6bd-4374-a9c7-5738ea6b6800 Correlation ID: 7279f188-cd3a-4f09-8236-fc7044d2080a Timestamp: 2023-12-12 00:18:55Z"",
+ ""error_codes"":[392204],
+ ""timestamp"":""2023-12-12 00:18:55Z"",
+ ""trace_id"":""44b6984d-e6bd-4374-a9c7-5738ea6b6800"",
+ ""correlation_id"":""7279f188-cd3a-4f09-8236-fc7044d2080a"",
+ ""error_uri"":""https://mtlsauth.microsoft.com/error?code=392204""
+ }";
+ }
+
+ public static string CertMismatchError500181()
+ {
+ return @"{
+ ""error"":""invalid_request"",
+ ""error_description"":""AADSTS500181: The TLS certificate provided does not match the certificate in the assertion. Trace ID: 2781e26e-d4ed-4947-9d95-11dfa81a5900 Correlation ID: e19df97b-3909-4c41-a439-91dc4ec8355b Timestamp: 2023-12-12 00:27:10Z"",
+ ""error_codes"":[500181],
+ ""timestamp"":""2023-12-12 00:27:10Z"",
+ ""trace_id"":""2781e26e-d4ed-4947-9d95-11dfa81a5900"",
+ ""correlation_id"":""e19df97b-3909-4c41-a439-91dc4ec8355b""
+ }";
+ }
+
public static string CreateClientInfo(string uid = TestConstants.Uid, string utid = TestConstants.Utid)
{
return Base64UrlHelpers.Encode("{\"uid\":\"" + uid + "\",\"utid\":\"" + utid + "\"}");
@@ -375,7 +584,7 @@ public static string CreateSuccessTokenResponseString(string uniqueId,
idToken +
(foci ? "\",\"foci\":\"1" : "") +
"\",\"id_token_expires_in\":\"3600\",\"client_info\":\"" + CreateClientInfo(uniqueId, utid) + "\"}";
-
+
return stringContent;
}
@@ -522,6 +731,9 @@ public static HttpResponseMessage CreateOpenIdConfigurationResponse(string autho
public static HttpResponseMessage CreateAdfsOpenIdConfigurationResponse(string authority, string qp = "")
{
+ var authorityUri = new Uri(authority);
+ string path = authorityUri.AbsolutePath.Substring(1);
+
if (!string.IsNullOrEmpty(qp))
{
qp = "?" + qp;
diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs
index b9994eeeac..9a2c9956bc 100644
--- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs
+++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs
@@ -21,12 +21,12 @@ public MockHttpAndServiceBundle(
LogCallback logCallback = null,
bool isExtendedTokenLifetimeEnabled = false,
string authority = ClientApplicationBase.DefaultAuthority,
- TestContext testContext = null,
+ string testName = null,
bool isMultiCloudSupportEnabled = false,
bool isInstanceDiscoveryEnabled = true,
IPlatformProxy platformProxy = null)
{
- HttpManager = new MockHttpManager(testContext);
+ HttpManager = new MockHttpManager(testName);
ServiceBundle = TestCommon.CreateServiceBundleWithCustomHttpManager(
HttpManager,
logCallback: logCallback,
@@ -55,8 +55,8 @@ public AuthenticationRequestParameters CreateAuthenticationRequestParameters(
ApiEvent.ApiIds apiId = ApiEvent.ApiIds.None,
bool validateAuthority = false)
{
- scopes ??= TestConstants.s_scope;
- tokenCache ??= new TokenCache(ServiceBundle, false);
+ scopes = scopes ?? TestConstants.s_scope;
+ tokenCache = tokenCache ?? new TokenCache(ServiceBundle, false);
var commonParameters = new AcquireTokenCommonParameters
{
diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpClientFactory.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpClientFactory.cs
new file mode 100644
index 0000000000..e6d006f365
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpClientFactory.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.Http;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Identity.Test.Common.Core.Mocks
+{
+ internal sealed class MockHttpClientFactoryForTest : IMsalHttpClientFactory, IDisposable
+ {
+ ///
+ public void Dispose()
+ {
+ // This ensures we only check the mock queue on dispose when we're not in the middle of an
+ // exception flow. Otherwise, any early assertion will cause this to likely fail
+ // even though it's not the root cause.
+#pragma warning disable CS0618 // Type or member is obsolete - this is non-production code so it's fine
+ if (Marshal.GetExceptionCode() == 0)
+#pragma warning restore CS0618 // Type or member is obsolete
+ {
+ string remainingMocks = string.Join(
+ " ",
+ _httpMessageHandlerQueue.Select(
+ h => (h as MockHttpMessageHandler)?.ExpectedUrl ?? string.Empty));
+
+ Assert.IsNotNull(_httpMessageHandlerQueue);
+ }
+ }
+
+ public MockHttpMessageHandler AddMockHandler(MockHttpMessageHandler handler)
+ {
+ _httpMessageHandlerQueue.Enqueue(handler);
+ return handler;
+ }
+
+ private Queue _httpMessageHandlerQueue = new Queue();
+
+ public HttpClient GetHttpClient()
+ {
+ HttpMessageHandler messageHandler;
+
+ Assert.IsNotNull(_httpMessageHandlerQueue);
+ messageHandler = _httpMessageHandlerQueue.Dequeue();
+
+ var httpClient = new HttpClient(messageHandler);
+
+ httpClient.DefaultRequestHeaders.Accept.Clear();
+ httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+
+ return httpClient;
+ }
+ }
+}
diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpCreator.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpCreator.cs
new file mode 100644
index 0000000000..ed583b0734
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpCreator.cs
@@ -0,0 +1,131 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net.Http;
+using System.Net;
+using System.Net.Http.Headers;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.Http;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.Identity.Test.Unit;
+
+namespace Microsoft.Identity.Test.Common.Core.Mocks
+{
+ internal static class MockHttpCreator
+ {
+ public static HttpResponseMessage GetSuccessfulCredentialResponse()
+ {
+ string successResponse = "{\"client_id\":\"2d0d13ad-3a4d-4cfd-98f8-f20621d55ded\"," +
+ "\"credential\":\"accesstoken\"," +
+ "\"expires_on\":" + (DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 3600) + "," +
+ "\"identity_type\":\"SystemAssigned\"," +
+ "\"refresh_in\":" + ((DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 3600) / 2) + "," +
+ "\"regional_token_url\":\"https://centraluseuap.mtlsauth.microsoft.com\"," +
+ "\"tenant_id\":\"72f988bf-86f1-41af-91ab-2d7cd011db47\"}";
+
+ return CreateSuccessResponseMessage(successResponse);
+ }
+
+ public static HttpResponseMessage GetMsiSuccessfulResponse(int expiresInHours = 1)
+ {
+ string expiresOn = Client.Utils.DateTimeHelpers.DateTimeToUnixTimestamp(DateTime.UtcNow.AddHours(expiresInHours));
+ string msiSuccessResponse = "{\"access_token\":\"" + TestConstants.ATSecret + "\",\"expires_on\":\"" + expiresOn + "\",\"resource\":\"https://management.azure.com/\",\"token_type\":" +
+ "\"Bearer\",\"client_id\":\"client_id\"}";
+
+ return CreateSuccessResponseMessage(msiSuccessResponse);
+ }
+
+ public static HttpResponseMessage GetSuccessfulMtlsResponse()
+ {
+ string mtlsResponse = "{\"token_type\":\"Bearer\",\"expires_in\":86399,\"ext_expires_in\":86399,\"access_token\":\"some-token\"}";
+
+ return CreateSuccessResponseMessage(mtlsResponse);
+ }
+
+
+ public static HttpResponseMessage CreateSuccessResponseMessage(string successResponse)
+ {
+ HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
+ HttpContent content =
+ new StringContent(successResponse);
+ responseMessage.Content = content;
+ return responseMessage;
+ }
+
+ public static MockHttpMessageHandler CreateManagedIdentityCredentialHandler()
+ {
+ var handler = new MockHttpMessageHandler()
+ {
+ ExpectedMethod = HttpMethod.Post,
+ ResponseMessage = GetSuccessfulCredentialResponse(),
+ };
+
+ return handler;
+ }
+
+ public static MockHttpMessageHandler CreateManagedIdentityMsiTokenHandler()
+ {
+ var handler = new MockHttpMessageHandler()
+ {
+ ExpectedMethod = HttpMethod.Get,
+ ResponseMessage = GetMsiSuccessfulResponse(),
+ };
+
+ return handler;
+ }
+
+ public static MockHttpMessageHandler CreateMtlsCredentialHandler(X509Certificate2 mtlsBindingCert)
+ {
+ var handler = new MockHttpMessageHandler()
+ {
+ ExpectedMethod = HttpMethod.Post,
+ ResponseMessage = GetSuccessfulCredentialResponse(),
+ };
+
+ // Add the certificate to the handler if provided
+ if (mtlsBindingCert != null)
+ {
+ handler.AddClientCertificate(mtlsBindingCert);
+ }
+
+ return handler;
+ }
+
+ public static MockHttpMessageHandler CreateMtlsTokenHandler()
+ {
+ var handler = new MockHttpMessageHandler()
+ {
+ ExpectedMethod = HttpMethod.Post,
+ ResponseMessage = GetSuccessfulMtlsResponse(),
+ };
+
+ return handler;
+ }
+
+ public static MockHttpMessageHandler CreateCredentialTokenHandler()
+ {
+ var handler = new MockHttpMessageHandler()
+ {
+ ExpectedMethod = HttpMethod.Post,
+ ResponseMessage = GetSuccessfulCredentialResponse(),
+ };
+
+ return handler;
+ }
+
+ public static void AddClientCertificate(this MockHttpMessageHandler handler, X509Certificate2 certificate)
+ {
+ handler.ClientCertificates.Add(certificate);
+ }
+ }
+}
diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs
index 2668459d5c..42514167ed 100644
--- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs
+++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs
@@ -9,40 +9,55 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Http;
using Microsoft.VisualStudio.TestTools.UnitTesting;
-using NSubstitute;
namespace Microsoft.Identity.Test.Common.Core.Mocks
{
internal sealed class MockHttpManager : IHttpManager,
IDisposable
{
- private readonly TestContext _testContext;
+ private readonly string _testName;
private readonly IHttpManager _httpManager;
- public MockHttpManager(TestContext testContext = null, bool isManagedIdentity = false, Func messageHandlerFunc = null) :
- this(true, testContext, isManagedIdentity, messageHandlerFunc)
+ public MockHttpManager(string testName = null,
+ bool isManagedIdentity = false,
+ Func messageHandlerFunc = null,
+ bool invokeNonMtlsHttpManagerFactory = false) :
+ this(true, testName, isManagedIdentity, messageHandlerFunc, invokeNonMtlsHttpManagerFactory)
{ }
- public MockHttpManager(bool retryOnce, TestContext testContext = null, bool isManagedIdentity = false, Func messageHandlerFunc = null)
+ public MockHttpManager(
+ bool retryOnce,
+ string testName = null,
+ bool isManagedIdentity = false,
+ Func messageHandlerFunc = null,
+ bool invokeNonMtlsHttpManagerFactory = false)
{
- _httpManager = HttpManagerFactory.GetHttpManager(new MockHttpClientFactory(messageHandlerFunc,
- _httpMessageHandlerQueue, testContext), retryOnce, isManagedIdentity);
-
- _testContext = testContext;
+ _httpManager = invokeNonMtlsHttpManagerFactory
+ ? HttpManagerFactory.GetHttpManager(
+ new MockNonMtlsHttpClientFactory(messageHandlerFunc, _httpMessageHandlerQueue, testName),
+ retryOnce,
+ isManagedIdentity)
+ : HttpManagerFactory.GetHttpManager(
+ new MockHttpClientFactory(messageHandlerFunc, _httpMessageHandlerQueue, testName),
+ retryOnce,
+ isManagedIdentity);
+
+ _testName = testName;
}
- private ConcurrentQueue _httpMessageHandlerQueue
+ private ConcurrentQueue _httpMessageHandlerQueue
{
get;
set;
- } = new ConcurrentQueue();
+ } = new ConcurrentQueue();
///
public void Dispose()
@@ -55,7 +70,7 @@ public void Dispose()
#pragma warning restore CS0618 // Type or member is obsolete
{
string remainingMocks = string.Join(" ",
- _httpMessageHandlerQueue.Select(GetExpectedUrlFromHandler));
+ _httpMessageHandlerQueue.Select(m => GetExpectedUrlFromHandler(m)));
Assert.AreEqual(0, _httpMessageHandlerQueue.Count,
"All mocks should have been consumed. Remaining mocks are for: " + remainingMocks);
}
@@ -63,9 +78,8 @@ public void Dispose()
public MockHttpMessageHandler AddMockHandler(MockHttpMessageHandler handler)
{
- string testName = _testContext?.TestName ?? "";
- Trace.WriteLine($"Test {testName} adds an HttpMessageHandler for { GetExpectedUrlFromHandler(handler) }");
- _httpMessageHandlerQueue.Enqueue(handler);
+ Trace.WriteLine($"Test {_testName} adds an HttpMessageHandler for {GetExpectedUrlFromHandler(handler)}");
+ _httpMessageHandlerQueue.Enqueue(handler);
return handler;
}
@@ -77,67 +91,59 @@ public MockHttpMessageHandler AddMockHandler(MockHttpMessageHandler handler)
///
public void ClearQueue()
{
- while (_httpMessageHandlerQueue.TryDequeue(out _))
- ;
+ while (_httpMessageHandlerQueue.TryDequeue(out _));
}
public long LastRequestDurationInMs => 3000;
-
-
private string GetExpectedUrlFromHandler(HttpMessageHandler handler)
{
return (handler as MockHttpMessageHandler)?.ExpectedUrl ?? "";
}
- public async Task SendPostAsync(Uri endpoint, IDictionary headers, IDictionary bodyParameters, ILoggerAdapter logger, CancellationToken cancellationToken = default)
- {
- return await _httpManager.SendPostAsync(endpoint, headers, bodyParameters, logger, cancellationToken).ConfigureAwait(false);
- }
-
- public async Task SendPostAsync(Uri endpoint, IDictionary headers, HttpContent body, ILoggerAdapter logger, CancellationToken cancellationToken = default)
- {
- return await _httpManager.SendPostAsync(endpoint, headers, body, logger, cancellationToken).ConfigureAwait(false);
- }
-
- public Task SendGetAsync(Uri endpoint, IDictionary headers, ILoggerAdapter logger, bool retry = true, CancellationToken cancellationToken = default)
- {
- return _httpManager.SendGetAsync(endpoint, headers, logger, retry, cancellationToken);
- }
-
- public async Task SendPostForceResponseAsync(Uri uri, IDictionary headers, StringContent body, ILoggerAdapter logger, CancellationToken cancellationToken = default)
+ public Task SendRequestAsync(
+ Uri endpoint,
+ Dictionary headers,
+ HttpContent body,
+ HttpMethod method,
+ ILoggerAdapter logger,
+ bool doNotThrow,
+ bool retry,
+ X509Certificate2 mtlsCertificate,
+ CancellationToken cancellationToken)
{
- return await _httpManager.SendPostForceResponseAsync(uri, headers, body, logger, cancellationToken).ConfigureAwait(false);
- }
-
- public async Task SendPostForceResponseAsync(Uri uri, IDictionary headers, IDictionary bodyParameters, ILoggerAdapter logger, CancellationToken cancellationToken = default)
- {
- return await _httpManager.SendPostForceResponseAsync(uri, headers, bodyParameters, logger, cancellationToken).ConfigureAwait(false);
- }
-
- public async Task SendGetForceResponseAsync(Uri endpoint, IDictionary headers, ILoggerAdapter logger, bool retry = true, CancellationToken cancellationToken = default)
- {
- return await _httpManager.SendGetForceResponseAsync(endpoint, headers, logger, retry, cancellationToken).ConfigureAwait(false);
+ return _httpManager.SendRequestAsync(
+ endpoint,
+ headers,
+ body,
+ method,
+ logger,
+ doNotThrow,
+ retry,
+ mtlsCertificate,
+ cancellationToken);
}
}
- internal class MockHttpClientFactory : IMsalHttpClientFactory
+ internal class MockHttpClientFactoryBase
{
- Func MessageHandlerFunc;
- ConcurrentQueue HttpMessageHandlerQueue;
- TestContext TestContext;
-
- public MockHttpClientFactory(Func messageHandlerFunc,
- ConcurrentQueue httpMessageHandlerQueue, TestContext testContext)
+ protected Func MessageHandlerFunc { get; set; }
+ protected ConcurrentQueue HttpMessageHandlerQueue { get; set; }
+ protected string _testName { get; set; }
+
+ protected MockHttpClientFactoryBase(
+ Func messageHandlerFunc,
+ ConcurrentQueue httpMessageHandlerQueue,
+ string testName)
{
MessageHandlerFunc = messageHandlerFunc;
HttpMessageHandlerQueue = httpMessageHandlerQueue;
- TestContext = testContext;
+ _testName = testName;
}
- public HttpClient GetHttpClient()
+ protected HttpClient GetHttpClientInternal(X509Certificate2 mtlsBindingCert)
{
- HttpMessageHandler messageHandler;
+ HttpClientHandler messageHandler;
if (MessageHandlerFunc != null)
{
@@ -151,7 +157,12 @@ public HttpClient GetHttpClient()
}
}
- Trace.WriteLine($"Test {TestContext?.TestName ?? ""} dequeued a mock handler for {GetExpectedUrlFromHandler(messageHandler)}");
+ Trace.WriteLine($"Test {_testName} dequeued a mock handler for {GetExpectedUrlFromHandler(messageHandler)}");
+
+ if (mtlsBindingCert != null)
+ {
+ messageHandler.ClientCertificates.Add(mtlsBindingCert);
+ }
var httpClient = new HttpClient(messageHandler)
{
@@ -169,4 +180,42 @@ private string GetExpectedUrlFromHandler(HttpMessageHandler handler)
return (handler as MockHttpMessageHandler)?.ExpectedUrl ?? "";
}
}
+
+ internal class MockHttpClientFactory : MockHttpClientFactoryBase, IMsalMtlsHttpClientFactory
+ {
+ public MockHttpClientFactory(
+ Func messageHandlerFunc,
+ ConcurrentQueue httpMessageHandlerQueue,
+ string testName)
+ : base(messageHandlerFunc, httpMessageHandlerQueue, testName)
+ {
+ }
+
+ public HttpClient GetHttpClient()
+ {
+ return GetHttpClientInternal(null);
+ }
+
+ public HttpClient GetHttpClient(X509Certificate2 mtlsBindingCert)
+ {
+ return GetHttpClientInternal(mtlsBindingCert);
+ }
+ }
+
+ internal class MockNonMtlsHttpClientFactory : MockHttpClientFactoryBase, IMsalHttpClientFactory
+ {
+ public MockNonMtlsHttpClientFactory(
+ Func messageHandlerFunc,
+ ConcurrentQueue httpMessageHandlerQueue,
+ string testName)
+ : base(messageHandlerFunc, httpMessageHandlerQueue, testName)
+ {
+ }
+
+ public HttpClient GetHttpClient()
+ {
+ return GetHttpClientInternal(null);
+ }
+ }
+
}
diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs
index 7a7bd8b109..e73a13fa3c 100644
--- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs
+++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs
@@ -7,6 +7,7 @@
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text;
using System.Threading.Tasks;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Client.Instance;
@@ -23,9 +24,9 @@ namespace Microsoft.Identity.Test.Common.Core.Mocks
internal static class MockHttpManagerExtensions
{
public static MockHttpMessageHandler AddInstanceDiscoveryMockHandler(
- this MockHttpManager httpManager,
- string authority = TestConstants.AuthorityCommonTenant,
- Uri customDiscoveryEndpoint = null,
+ this MockHttpManager httpManager,
+ string authority = TestConstants.AuthorityCommonTenant,
+ Uri customDiscoveryEndpoint = null,
string instanceMetadataContent = null)
{
Uri authorityURI = new Uri(authority);
@@ -47,7 +48,7 @@ public static MockHttpMessageHandler AddInstanceDiscoveryMockHandler(
return httpManager.AddMockHandler(
MockHelpers.CreateInstanceDiscoveryMockHandler(
- discoveryEndpoint,
+ discoveryEndpoint,
instanceMetadataContent ?? TestConstants.DiscoveryJsonResponse));
}
@@ -85,7 +86,7 @@ public static MockHttpMessageHandler AddResponseMockHandlerForPost(
public static MockHttpMessageHandler AddFailureTokenEndpointResponse(
this MockHttpManager httpManager,
string error,
- string authority = TestConstants.AuthorityCommonTenant,
+ string authority = TestConstants.AuthorityCommonTenant,
string correlationId = null)
{
var handler = new MockHttpMessageHandler()
@@ -93,7 +94,7 @@ public static MockHttpMessageHandler AddFailureTokenEndpointResponse(
ExpectedUrl = authority + "oauth2/v2.0/token",
ExpectedMethod = HttpMethod.Post,
ResponseMessage = MockHelpers.CreateFailureTokenResponseMessage(
- error,
+ error,
correlationId: correlationId)
};
httpManager.AddMockHandler(handler);
@@ -105,7 +106,7 @@ public static MockHttpMessageHandler AddSuccessTokenResponseMockHandlerForPost(
string authority = TestConstants.AuthorityCommonTenant,
IDictionary bodyParameters = null,
IDictionary queryParameters = null,
- bool foci = false,
+ bool foci = false,
HttpResponseMessage responseMessage = null,
IDictionary expectedHttpHeaders = null)
{
@@ -140,7 +141,7 @@ public static void AddSuccessTokenResponseMockHandlerForGet(
public static HttpResponseMessage AddResiliencyMessageMockHandler(
this MockHttpManager httpManager,
HttpMethod httpMethod,
- HttpStatusCode httpStatusCode,
+ HttpStatusCode httpStatusCode,
int? retryAfter = null)
{
var response = MockHelpers.CreateServerErrorMessage(httpStatusCode, retryAfter);
@@ -179,8 +180,8 @@ public static void AddMockHandlerContentNotFound(this MockHttpManager httpManage
}
public static MockHttpMessageHandler AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(
- this MockHttpManager httpManager,
- string token = "header.payload.signature",
+ this MockHttpManager httpManager,
+ string token = "header.payload.signature",
string expiresIn = "3599",
string tokenType = "Bearer",
IList unexpectedHttpHeaders = null)
@@ -247,7 +248,7 @@ public static void AddAdfs2019MockHandler(this MockHttpManager httpManager)
});
}
-
+
public static MockHttpMessageHandler AddAllMocks(this MockHttpManager httpManager, TokenResponseType aadResponse)
{
@@ -256,8 +257,8 @@ public static MockHttpMessageHandler AddAllMocks(this MockHttpManager httpManage
}
public static MockHttpMessageHandler AddTokenResponse(
- this MockHttpManager httpManager,
- TokenResponseType responseType,
+ this MockHttpManager httpManager,
+ TokenResponseType responseType,
IDictionary expectedRequestHeaders = null)
{
HttpResponseMessage responseMessage;
@@ -269,7 +270,7 @@ public static MockHttpMessageHandler AddTokenResponse(
TestConstants.Uid,
TestConstants.DisplayableId,
TestConstants.s_scope.ToArray());
-
+
break;
case TokenResponseType.Valid_ClientCredentials:
responseMessage = MockHelpers.CreateSuccessfulClientCredentialTokenResponseMessage();
@@ -278,12 +279,12 @@ public static MockHttpMessageHandler AddTokenResponse(
case TokenResponseType.Invalid_AADUnavailable503:
responseMessage = MockHelpers.CreateFailureMessage(
System.Net.HttpStatusCode.ServiceUnavailable, "service down");
-
+
break;
case TokenResponseType.InvalidGrant:
- responseMessage = MockHelpers.CreateInvalidGrantTokenResponseMessage();
+ responseMessage = MockHelpers.CreateInvalidGrantTokenResponseMessage();
break;
- case TokenResponseType.InvalidClient:
+ case TokenResponseType.InvalidClient:
responseMessage = MockHelpers.CreateInvalidClientResponseMessage();
break;
@@ -295,7 +296,7 @@ public static MockHttpMessageHandler AddTokenResponse(
{
ExpectedMethod = HttpMethod.Post,
ExpectedRequestHeaders = expectedRequestHeaders,
- ResponseMessage = responseMessage,
+ ResponseMessage = responseMessage,
};
httpManager.AddMockHandler(responseHandler);
@@ -303,8 +304,8 @@ public static MockHttpMessageHandler AddTokenResponse(
}
public static HttpResponseMessage AddTokenErrorResponse(
- this MockHttpManager httpManager,
- string error,
+ this MockHttpManager httpManager,
+ string error,
HttpStatusCode? customStatusCode)
{
var responseMessage = MockHelpers.CreateFailureTokenResponseMessage(error, customStatusCode: customStatusCode);
@@ -372,7 +373,7 @@ public static void AddManagedIdentityMockHandler(
httpManager.AddMockHandler(httpMessageHandler);
}
-
+
private static MockHttpMessageHandler BuildMockHandlerForManagedIdentitySource(ManagedIdentitySource managedIdentitySourceType, string resource)
{
MockHttpMessageHandler httpMessageHandler = new MockHttpMessageHandler();
@@ -411,21 +412,26 @@ private static MockHttpMessageHandler BuildMockHandlerForManagedIdentitySource(M
expectedQueryParams.Add("api-version", "2019-07-01-preview");
expectedQueryParams.Add("resource", resource);
break;
+ case ManagedIdentitySource.SlcCredential:
+ httpMessageHandler.ExpectedMethod = HttpMethod.Post;
+ expectedRequestHeaders.Add("Server", "IMDS");
+ expectedQueryParams.Add("cred-api-version", "1.0");
+ break;
}
if (managedIdentitySourceType != ManagedIdentitySource.CloudShell)
{
httpMessageHandler.ExpectedQueryParams = expectedQueryParams;
}
-
+
httpMessageHandler.ExpectedRequestHeaders = expectedRequestHeaders;
return httpMessageHandler;
}
public static void AddManagedIdentityWSTrustMockHandler(
- this MockHttpManager httpManager,
- string expectedUrl,
+ this MockHttpManager httpManager,
+ string expectedUrl,
string filePath = null)
{
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized);
@@ -433,7 +439,7 @@ public static void AddManagedIdentityWSTrustMockHandler(
{
responseMessage.Headers.Add("WWW-Authenticate", $"Basic realm={filePath}");
}
-
+
httpManager.AddMockHandler(
new MockHttpMessageHandler
{
@@ -443,6 +449,85 @@ public static void AddManagedIdentityWSTrustMockHandler(
});
}
+ public static void AddManagedIdentityCredentialMockHandler(
+ this MockHttpManager httpManager,
+ string expectedUrl,
+ string response = null,
+ string userAssignedId = null,
+ UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.None,
+ HttpStatusCode statusCode = HttpStatusCode.OK)
+ {
+ HttpResponseMessage responseMessage = new HttpResponseMessage(statusCode);
+ IDictionary expectedQueryParams = new Dictionary();
+ IDictionary expectedHeaders = new Dictionary();
+ MockHttpMessageHandler httpMessageHandler = new MockHttpMessageHandler();
+
+ HttpContent content = new StringContent(response);
+ responseMessage.Content = content;
+
+ httpMessageHandler.ExpectedMethod = HttpMethod.Post;
+
+ expectedHeaders.Add("Metadata", "true");
+ expectedQueryParams.Add("cred-api-version", "1.0");
+
+ if (userAssignedIdentityId == UserAssignedIdentityId.ClientId)
+ {
+ expectedQueryParams.Add(Constants.ManagedIdentityClientId, userAssignedId);
+ }
+
+ if (userAssignedIdentityId == UserAssignedIdentityId.ResourceId)
+ {
+ expectedQueryParams.Add(Constants.ManagedIdentityResourceId, userAssignedId);
+ }
+
+ if (userAssignedIdentityId == UserAssignedIdentityId.ObjectId)
+ {
+ expectedQueryParams.Add(Constants.ManagedIdentityObjectId, userAssignedId);
+ }
+
+ httpMessageHandler.ExpectedQueryParams = expectedQueryParams;
+
+ httpMessageHandler.ResponseMessage = responseMessage;
+ httpMessageHandler.ExpectedUrl = expectedUrl;
+
+ httpMessageHandler.ExpectedRequestHeaders = expectedHeaders;
+
+ httpManager.AddMockHandler(httpMessageHandler);
+ }
+
+ public static void AddManagedIdentityMtlsMockHandler(
+ this MockHttpManager httpManager,
+ string expectedUrl,
+ string resource,
+ string client_id = TestConstants.SystemAssignedClientId,
+ string response = null,
+ HttpStatusCode statusCode = HttpStatusCode.OK)
+ {
+ HttpResponseMessage responseMessage = new HttpResponseMessage(statusCode);
+ IDictionary expectedBodyParams = new Dictionary();
+ IDictionary expectedRequestHeaders = new Dictionary();
+ MockHttpMessageHandler httpMessageHandler = new MockHttpMessageHandler();
+ Guid correlationId = Guid.NewGuid();
+
+ HttpContent content = new StringContent(response);
+ responseMessage.Content = content;
+
+ httpMessageHandler.ExpectedMethod = HttpMethod.Post;
+ //expectedRequestHeaders.Add("client-request-id", correlationId.ToString("D"));
+ httpMessageHandler.ResponseMessage = responseMessage;
+ httpMessageHandler.ExpectedUrl = expectedUrl;
+
+ expectedBodyParams.Add("grant_type", "client_credentials");
+ expectedBodyParams.Add("scope", resource + "/.default");
+ expectedBodyParams.Add("client_id", client_id);
+ expectedBodyParams.Add("client_assertion", "managed-identity-credential");
+ expectedBodyParams.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+
+ httpMessageHandler.ExpectedPostData = expectedBodyParams;
+ httpMessageHandler.ExpectedRequestHeaders = expectedRequestHeaders;
+ httpManager.AddMockHandler(httpMessageHandler);
+ }
+
public static void AddRegionDiscoveryMockHandlerNotFound(
this MockHttpManager httpManager)
{
@@ -468,11 +553,11 @@ public enum TokenResponseType
///
/// Results in a UI Required Exception
///
- InvalidGrant,
+ InvalidGrant,
///
/// Normal server exception
///
InvalidClient
}
- }
+}
diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpMessageHandler.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpMessageHandler.cs
index fa5116512e..08ceeebf88 100644
--- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpMessageHandler.cs
+++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpMessageHandler.cs
@@ -7,6 +7,7 @@
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
+using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.OAuth2;
@@ -16,17 +17,14 @@
namespace Microsoft.Identity.Test.Common.Core.Mocks
{
- internal class MockHttpMessageHandler : HttpMessageHandler
+ internal class MockHttpMessageHandler : HttpClientHandler
{
public HttpResponseMessage ResponseMessage { get; set; }
-
- // no query params
public string ExpectedUrl { get; set; }
public IDictionary ExpectedQueryParams { get; set; }
public IDictionary ExpectedPostData { get; set; }
public IDictionary ExpectedRequestHeaders { get; set; }
public IList UnexpectedRequestHeaders { get; set; }
-
public HttpMethod ExpectedMethod { get; set; }
public Exception ExceptionToThrow { get; set; }
@@ -36,9 +34,9 @@ internal class MockHttpMessageHandler : HttpMessageHandler
/// Once the http message is executed, this property holds the request message
///
public HttpRequestMessage ActualRequestMessage { get; private set; }
-
public Dictionary ActualRequestPostData { get; private set; }
public HttpRequestHeaders ActualRequestHeaders { get; private set; }
+ public X509Certificate2 ExpectedMtlsBindingCertificate { get; set; }
protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
@@ -50,6 +48,7 @@ protected override Task SendAsync(HttpRequestMessage reques
}
var uri = request.RequestUri;
+
if (!string.IsNullOrEmpty(ExpectedUrl))
{
Assert.AreEqual(
@@ -57,35 +56,45 @@ protected override Task SendAsync(HttpRequestMessage reques
uri.AbsoluteUri.Split('?')[0]);
}
+ if (ExpectedMtlsBindingCertificate != null)
+ {
+ Assert.AreEqual(1, base.ClientCertificates.Count);
+ Assert.AreEqual(ExpectedMtlsBindingCertificate, base.ClientCertificates[0]);
+ }
+
Assert.AreEqual(ExpectedMethod, request.Method);
- // Match QP passed in for validation.
- if (ExpectedQueryParams != null)
+ ValidateQueryParams(uri);
+
+ ValidatePostDataAsync(request);
+
+ ValidateHeaders(request);
+
+ AdditionalRequestValidation?.Invoke(request);
+
+ return new TaskFactory().StartNew(() => ResponseMessage, cancellationToken);
+ }
+
+ private void ValidateQueryParams(Uri uri)
+ {
+ if (ExpectedQueryParams != null && ExpectedQueryParams.Any())
{
- Assert.IsFalse(
- string.IsNullOrEmpty(uri.Query),
- string.Format(
- CultureInfo.InvariantCulture,
- "Provided url ({0}) does not contain query parameters, as expected",
- uri.AbsolutePath));
- IDictionary inputQp = CoreHelpers.ParseKeyValueList(uri.Query.Substring(1), '&', false, null);
- Assert.AreEqual(ExpectedQueryParams.Count, inputQp.Count, "Different number of query params`");
- foreach (string key in ExpectedQueryParams.Keys)
+ Assert.IsFalse(string.IsNullOrEmpty(uri.Query), $"Provided url ({uri.AbsoluteUri}) does not contain query parameters as expected.");
+ var inputQp = CoreHelpers.ParseKeyValueList(uri.Query.Substring(1), '&', false, null);
+ Assert.AreEqual(ExpectedQueryParams.Count, inputQp.Count, "Different number of query params.");
+ foreach (var key in ExpectedQueryParams.Keys)
{
- Assert.IsTrue(
- inputQp.ContainsKey(key),
- string.Format(
- CultureInfo.InvariantCulture,
- "Expected query parameter ({0}) not found in the url ({1})",
- key,
- uri.AbsolutePath));
- Assert.AreEqual(ExpectedQueryParams[key], inputQp[key]);
+ Assert.IsTrue(inputQp.ContainsKey(key), $"Expected query parameter ({key}) not found in the url ({uri.AbsoluteUri}).");
+ Assert.AreEqual(ExpectedQueryParams[key], inputQp[key], $"Value mismatch for query parameter: {key}.");
}
}
+ }
+ private async Task ValidatePostDataAsync(HttpRequestMessage request)
+ {
if (request.Method != HttpMethod.Get && request.Content != null)
{
- string postData = request.Content.ReadAsStringAsync().Result;
+ string postData = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
ActualRequestPostData = CoreHelpers.ParseKeyValueList(postData, '&', true, null);
}
@@ -104,18 +113,18 @@ protected override Task SendAsync(HttpRequestMessage reques
}
}
}
+ }
+ private void ValidateHeaders(HttpRequestMessage request)
+ {
ActualRequestHeaders = request.Headers;
-
- if (ExpectedRequestHeaders != null )
+ if (ExpectedRequestHeaders != null)
{
foreach (var kvp in ExpectedRequestHeaders)
{
- Assert.IsTrue(
- request.Headers.Any(h =>
- string.Equals(h.Key, kvp.Key, StringComparison.OrdinalIgnoreCase) &&
- string.Equals(h.Value.AsSingleString(), kvp.Value, StringComparison.OrdinalIgnoreCase))
- , $"Expecting a request header {kvp.Key}: {kvp.Value} but did not find in the actual request: {request}");
+ Assert.IsTrue(request.Headers.Contains(kvp.Key), $"Expected request header not found: {kvp.Key}.");
+ var headerValue = request.Headers.GetValues(kvp.Key).FirstOrDefault();
+ Assert.AreEqual(kvp.Value, headerValue, $"Value mismatch for request header {kvp.Key}.");
}
}
@@ -123,15 +132,9 @@ protected override Task SendAsync(HttpRequestMessage reques
{
foreach (var item in UnexpectedRequestHeaders)
{
- Assert.IsTrue(
- !request.Headers.Any(h => string.Equals(h.Key, item, StringComparison.OrdinalIgnoreCase))
- , $"Not expecting a request header with key={item} but it was found in the actual request: {request}");
+ Assert.IsFalse(request.Headers.Contains(item), $"Not expecting a request header with key={item} but it was found.");
}
}
-
- AdditionalRequestValidation?.Invoke(request);
-
- return new TaskFactory().StartNew(() => ResponseMessage, cancellationToken);
}
}
}
diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockMtlsHttpClientFactory.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockMtlsHttpClientFactory.cs
new file mode 100644
index 0000000000..d395c6226f
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockMtlsHttpClientFactory.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client;
+using Microsoft.Identity.Client.Core;
+using Microsoft.Identity.Client.Http;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Identity.Test.Common.Core.Mocks
+{
+ internal sealed class MockMtlsHttpClientFactory : IMsalMtlsHttpClientFactory, IDisposable
+ {
+ ///
+ public void Dispose()
+ {
+ // This ensures we only check the mock queue on dispose when we're not in the middle of an
+ // exception flow. Otherwise, any early assertion will cause this to likely fail
+ // even though it's not the root cause.
+#pragma warning disable CS0618 // Type or member is obsolete - this is non-production code so it's fine
+ if (Marshal.GetExceptionCode() == 0)
+#pragma warning restore CS0618 // Type or member is obsolete
+ {
+ string remainingMocks = string.Join(
+ " ",
+ _httpMessageHandlerQueue.Select(
+ h => (h as MockHttpMessageHandler)?.ExpectedUrl ?? string.Empty));
+
+ Assert.IsNotNull(_httpMessageHandlerQueue);
+ }
+ }
+
+ public MockHttpMessageHandler AddMockHandler(MockHttpMessageHandler handler)
+ {
+ _httpMessageHandlerQueue.Enqueue(handler);
+ return handler;
+ }
+
+ private Queue _httpMessageHandlerQueue = new Queue();
+
+ public HttpClient GetHttpClient(X509Certificate2 x509Certificate2)
+ {
+ return GetHttpClientInternal(x509Certificate2);
+ }
+
+ public HttpClient GetHttpClient()
+ {
+ return GetHttpClientInternal(null);
+ }
+
+ public HttpClient GetHttpClientInternal(X509Certificate2 mtlsBindingCert)
+ {
+ HttpClientHandler messageHandler;
+
+ Assert.IsNotNull(_httpMessageHandlerQueue);
+
+ if (!_httpMessageHandlerQueue.Any() || !(_httpMessageHandlerQueue.Dequeue() is HttpClientHandler))
+ {
+ Assert.Fail("The MockHttpManager's queue is empty or does not contain the expected handler type. Cannot serve another response");
+ }
+
+ messageHandler = (HttpClientHandler)_httpMessageHandlerQueue.Dequeue();
+
+ var httpClient = new HttpClient(messageHandler);
+
+ if (mtlsBindingCert != null)
+ {
+ messageHandler.ClientCertificates.Add(mtlsBindingCert);
+ }
+
+ httpClient.DefaultRequestHeaders.Accept.Clear();
+ httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+
+ return httpClient;
+ }
+ }
+}
diff --git a/tests/Microsoft.Identity.Test.Common/Microsoft.Identity.Test.Common.csproj b/tests/Microsoft.Identity.Test.Common/Microsoft.Identity.Test.Common.csproj
index 7a295b51ab..088240506e 100644
--- a/tests/Microsoft.Identity.Test.Common/Microsoft.Identity.Test.Common.csproj
+++ b/tests/Microsoft.Identity.Test.Common/Microsoft.Identity.Test.Common.csproj
@@ -24,4 +24,4 @@
-
+
\ No newline at end of file
diff --git a/tests/Microsoft.Identity.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Test.Common/TestConstants.cs
index dfd7211c79..db1059c00c 100644
--- a/tests/Microsoft.Identity.Test.Common/TestConstants.cs
+++ b/tests/Microsoft.Identity.Test.Common/TestConstants.cs
@@ -144,6 +144,8 @@ public static HashSet s_scope
public const string ClientId = "d3adb33f-c0de-ed0c-c0de-deadb33fc0d3";
public const string ClientId2 = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
public const string ObjectId = "593b2662-5af7-4a90-a9cb-5a9de615b82f";
+ public const string SystemAssignedClientId = "2d0d13ad-3a4d-4cfd-98f8-f20621d55ded";
+ public const string CredentialIdentityDefaultClientId = "d3adb33f-c0de-ed0c-c0de-deadb33fc0d3";
public const string FamilyId = "1";
public const string UniqueId = "unique_id";
public const string IdentityProvider = "my-idp";
@@ -228,7 +230,6 @@ public static IDictionary ExtraQueryParameters
}
}
-
public const string MsalCCAKeyVaultUri = "https://buildautomation.vault.azure.net/secrets/AzureADIdentityDivisionTestAgentSecret/";
public const string MsalCCAKeyVaultSecretName = "MSIDLAB4-IDLABS-APP-AzureADMyOrg-CC";
public const string MsalOBOKeyVaultUri = "https://buildautomation.vault.azure.net/secrets/IdentityDivisionDotNetOBOServiceSecret/";
@@ -351,6 +352,38 @@ public static MsalTokenResponse CreateMsalTokenResponseWithTokenSource()
]
}";
+ public const string InvalidResourceError = "AADSTS500011: The resource principal named https://graph.microsoft.com/user.read was not found in the tenant named Cross Cloud B2B Test Tenant. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant. Trace ID: 9d8cb0bf-7e34-40fd-babc-f6ff018a1800 Correlation ID: 42186e1b-17eb-46fb-b5b7-4c43cae4d336 Timestamp: 2023-12-08 22:20:25Z";
+
+ public const string InvalidScopeError70011 = "AADSTS70011: The provided request must include a 'scope' input parameter. The provided value for the input parameter 'scope' is not valid. The scope user.read/.default is not valid. Trace ID: 9e8a0bd6-fb1b-45cf-8e00-95c2c73e1400 Correlation ID: 6ce4a5ab-87a1-4985-b06d-5ab08b5fa924 Timestamp: 2023-12-08 21:56:44Z";
+
+ public const string InvalidScopeError1002012 = "AADSTS1002012: The provided value for scope user.read is not valid. Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI). Trace ID: 8575f1d5-0144-4d71-87c8-2df9f1e30000 Correlation ID: a5469466-6c01-40e0-abf8-302d09c991e3 Timestamp: 2023-12-08 22:11:08Z";
+
+ public const string InvalidTenantError900023 = "AADSTS900023: Specified tenant identifier 'invalid_tenant' is neither a valid DNS name, nor a valid external domain. Trace ID: f38df5f2-84c4-4195-bad6-8eca059b0b00 Correlation ID: e318f766-8581-445a-97fb-419f80d98d8b Timestamp: 2023-12-11 22:52:53Z";
+
+ public const string WrongTenantError700016 = "AADSTS700016: Application with identifier '833aa854-2811-4f90-9620-c38070f595d7' was not found in the directory 'MSIDLAB4'. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant. Trace ID: 68b0d98d-52e8-4e45-9282-9b3b09fc1800 Correlation ID: 75673189-3db2-408b-8384-16860ee0c0f0 Timestamp: 2023-12-11 22:54:25Z";
+
+ public const string WrongMtlsUrlError50171 = "A configuration issue is preventing authentication - check the error message from the server for details. You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS50171: The given audience can only be used in Mutual-TLS token calls. Trace ID: e350f752-0a39-43c2-a9a2-cbd7ff4a6f00 Correlation ID: 26bb13de-d2cf-4f8f-9f36-d7611c00fecb Timestamp: 2023-12-11 22:58:32Z";
+
+ public const string SendTenantIdInCredentialValueError50027 = "AADSTS50027: JWT token is invalid or malformed. Trace ID: 6ca706cd-c0a1-4ec2-acb1-541b5a579a00 Correlation ID: 52955596-2fe6-43c6-b087-6038942c8254 Timestamp: 2023-12-11 23:02:08Z";
+
+ public const string BadCredNoIssError90014 = "AADSTS90014: The required field 'iss' is missing from the credential. Ensure that you have all the necessary parameters for the login request. Trace ID: 605439e8-8f0e-43f5-9887-5281a05a5200 Correlation ID: abc63349-b90e-4b15-8fb7-edc9326ed3c8 Timestamp: 2023-12-11 23:14:38Z";
+
+ public const string BadCredNoAudError90014 = "AADSTS90014: The required field 'aud' is missing from the credential. Ensure that you have all the necessary parameters for the login request. Trace ID: 0b1cc102-98b7-4fa5-a11a-82520fa85a00 Correlation ID: 23811f20-96bb-4900-a1a3-6368ef8890b2 Timestamp: 2023-12-11 23:16:15Z";
+
+ public const string BadCredBadAlgError5002738 = "A configuration issue is preventing authentication - check the error message from the server for details. You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS5002738: Invalid JWT token. 'HS256' is not a supported signature algorithm. Supported signing algorithms are: 'RS256,RS384,RS512' Trace ID: 2ed12465-8044-44af-bd27-b73b27e04a00 Correlation ID: bc26e294-ed13-4e6f-a225-28cdec2cc519 Timestamp: 2023-12-11 23:18:06Z";
+
+ public const string BadCredMissingSha1Error5002723 = "A configuration issue is preventing authentication - check the error message from the server for details. You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS5002723: Invalid JWT token. No certificate SHA-1 thumbprint, certificate SHA-256 thumbprint, nor keyId specified in token header. Trace ID: 3ce71c90-8d35-4413-bedb-73337ec40c00 Correlation ID: 540e9fb1-db53-4b10-a0ca-047d03b97d10 Timestamp: 2023-12-11 23:51:16Z";
+
+ public const string BadTimeRangeError700024 = "A configuration issue is preventing authentication - check the error message from the server for details. You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS700024: Client assertion is not within its valid time range. Current time: 2023-12-11T23:52:19.6223401Z, assertion valid from 2018-01-18T01:30:22.0000000Z, expiry time of assertion 1970-01-01T00:00:00.0000000Z. Review the documentation at https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials . Trace ID: 2486d2c5-63a7-44f5-bb09-05e4c5494000 Correlation ID: fc5f1331-e3ef-44cb-b478-909a171010ab Timestamp: 2023-12-11 23:52:19Z";
+
+ public const string IdentifierMismatchError700021 = "A configuration issue is preventing authentication - check the error message from the server for details. You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS700021: Client assertion application identifier doesn't match 'client_id' parameter. Review the documentation at https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials . Trace ID: 1180e895-2f6b-4504-b0cf-f49632647100 Correlation ID: 88c237d8-7867-4e68-89e4-bc5a6d3b2159 Timestamp: 2023-12-11 23:55:14Z";
+
+ public const string MissingCertError392200 = "AADSTS392200: Client certificate is missing from the request. Trace ID: 35f8d355-5be8-4028-83e5-aeb609b8d500 Correlation ID: e10c5bea-3b7e-42a2-a251-705d6e7aa48d Timestamp: 2023-12-12 00:11:34Z";
+
+ public const string ExpiredCertError392204 = "A configuration issue is preventing authentication - check the error message from the server for details. You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS392204: The provided client certificate has expired. Trace ID: 44b6984d-e6bd-4374-a9c7-5738ea6b6800 Correlation ID: 7279f188-cd3a-4f09-8236-fc7044d2080a Timestamp: 2023-12-12 00:18:55Z";
+
+ public const string CertMismatchError500181 = "AADSTS500181: The TLS certificate provided does not match the certificate in the assertion. Trace ID: 2781e26e-d4ed-4947-9d95-11dfa81a5900 Correlation ID: e19df97b-3909-4c41-a439-91dc4ec8355b Timestamp: 2023-12-12 00:27:10Z";
+
public const string DiscoveryFailedResponse =
@"{""error"":""invalid_instance"",
""error_description"":""AADSTS50049: Unknown or invalid instance.\r\nTrace ID: 82e709b9-f0b3-431d-99cd-f3c2ca3d4b00\r\nCorrelation ID: e7619cf4-53ea-443c-b76a-194c032e9840\r\nTimestamp: 2021-04-14 11:27:26Z"",
diff --git a/tests/Microsoft.Identity.Test.Common/TestData.cs b/tests/Microsoft.Identity.Test.Common/TestData.cs
index 63127b8574..a9d92f4a23 100644
--- a/tests/Microsoft.Identity.Test.Common/TestData.cs
+++ b/tests/Microsoft.Identity.Test.Common/TestData.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using Microsoft.Identity.Test.Common.Core.Mocks;
using Microsoft.Identity.Test.Unit;
namespace Microsoft.Identity.Test.Common
@@ -81,5 +82,128 @@ public static IEnumerable
+
+
+
+
diff --git a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs
index 427b7ca149..b0beec42c6 100644
--- a/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs
+++ b/tests/devapps/Managed Identity apps/ManagedIdentityAppVM/Program.cs
@@ -3,24 +3,62 @@
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
+using Microsoft.Identity.Client.Extensibility;
using Microsoft.IdentityModel.Abstractions;
IIdentityLogger identityLogger = new IdentityLogger();
-IManagedIdentityApplication mi = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned)
+string claims = @"{""access_token"":{""nbf"":{""essential"":true, ""value"":""1701477303""}}}";
+
+Console.WriteLine($"Binding Certificate - {ManagedIdentityApplication.GetBindingCertificate()}");
+
+Console.WriteLine($"Claims supported in MI ? - {ManagedIdentityApplication.IsClaimsSupportedByClient()}");
+
+IManagedIdentityApplication mi = ManagedIdentityApplicationBuilder
+ //.Create(ManagedIdentityId.SystemAssigned)
+ //.Create(ManagedIdentityId.WithUserAssignedClientId("8a7c2bc8-7041-4eb9-b49a-a70aeb68fdae")) // CAE VM
+ .Create(ManagedIdentityId.WithUserAssignedClientId("3b57c42c-3201-4295-ae27-d6baec5b7027")) //MSAL SLC VM
+ .WithExperimentalFeatures(true)
+ .WithClientCapabilities(new string[] { "CP1" })
.WithLogging(identityLogger, true)
.Build();
string? scope = "https://management.azure.com";
+string? resource = "api://AzureAdTokenExchange";
do
{
Console.WriteLine($"Acquiring token with scope {scope}");
try
{
- var result = await mi.AcquireTokenForManagedIdentity(scope)
- .ExecuteAsync().ConfigureAwait(false);
+ var result = await mi.AcquireTokenForManagedIdentity(resource)
+ .OnBeforeTokenRequest((OnBeforeTokenRequestData data) =>
+ {
+ Console.WriteLine("OnBeforeTokenRequest");
+ Console.WriteLine($"RequestUri: {data.RequestUri}");
+ Console.WriteLine($"BodyParameters: {string.Join(", ", data.BodyParameters)}");
+ Console.WriteLine($"Headers: {string.Join(", ", data.Headers)}");
+
+ // Adding query parameters directly to the request URI
+ if (!data.RequestUri.AbsoluteUri.Contains('?'))
+ {
+ data.RequestUri = new Uri(data.RequestUri.AbsoluteUri + "?dc=ESTS-PUB-WUS2-AZ1-FD000-TEST1");
+ }
+ else
+ {
+ data.RequestUri = new Uri(data.RequestUri.AbsoluteUri + "&dc=ESTS-PUB-WUS2-AZ1-FD000-TEST1");
+ }
+
+ // Adding additional headers
+ data.Headers.Add("header1", "hval1");
+ data.Headers.Add("header2", "hval2");
+
+ return Task.CompletedTask;
+ })
+ .WithClaims(claims)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
Console.WriteLine("Success");
Console.ReadLine();