From 3c32188f336a6ba76b6169d06ac2944c4cceeff5 Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Fri, 7 Nov 2025 15:15:16 -0800
Subject: [PATCH 01/12] Add extensibility APIs
---
.../AppConfig/ApplicationConfiguration.cs | 19 +
.../ConfidentialClientApplicationBuilder.cs | 10 +
...ntialClientApplicationBuilderExtensions.cs | 99 ++++
.../Extensibility/ExecutionResult.cs | 43 ++
.../CertificateAndClaimsClientCredential.cs | 81 +++-
.../Microsoft.Identity.Client.csproj | 5 +
.../Microsoft.Identity.Client/MsalError.cs | 6 +
.../PublicApi/net462/PublicAPI.Unshipped.txt | 8 +
.../PublicApi/net472/PublicAPI.Unshipped.txt | 8 +
.../PublicApi/net8.0/PublicAPI.Unshipped.txt | 8 +
.../netstandard2.0/PublicAPI.Unshipped.txt | 8 +
...tialClientApplicationExtensibilityTests.cs | 429 ++++++++++++++++++
12 files changed, 719 insertions(+), 5 deletions(-)
create mode 100644 src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs
create mode 100644 tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityTests.cs
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
index 4e22a2855c..c163d8bce2 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
@@ -131,6 +131,25 @@ public string ClientVersion
internal IRetryPolicyFactory RetryPolicyFactory { get; set; }
internal ICsrFactory CsrFactory { get; set; }
+ #region Extensibility Callbacks
+
+ ///
+ /// Dynamic certificate provider callback for client credential flows.
+ ///
+ public Func ClientCredentialCertificateProvider { get; set; }
+
+ ///
+ /// Retry policy callback that determines whether to retry after a token acquisition failure.
+ ///
+ public Func RetryPolicy { get; set; }
+
+ ///
+ /// Execution observer callback that receives the final result of token acquisition attempts.
+ ///
+ public Action ExecutionObserver { get; set; }
+
+ #endregion
+
#region ClientCredentials
// Indicates if claims or assertions are used within the configuration
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs
index 54234eacfd..cf68483b40 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs
@@ -407,6 +407,16 @@ internal override void Validate()
throw new InvalidOperationException(MsalErrorMessage.InvalidRedirectUriReceived(Config.RedirectUri));
}
+ // Validate mutual exclusivity between static certificate and dynamic certificate provider
+ if (Config.ClientCredential is CertificateClientCredential &&
+ Config.ClientCredentialCertificateProvider != null)
+ {
+ throw new MsalClientException(
+ MsalError.InvalidClientCredentialConfiguration,
+ "Cannot use both WithCertificate(X509Certificate2) and WithCertificate(Func). " +
+ "Choose one approach for providing client credentials.");
+ }
+
ValidateAndUpdateRegion();
}
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
index 7328b69ad3..9043612e24 100644
--- a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System;
+using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
namespace Microsoft.Identity.Client.Extensibility
@@ -29,5 +30,103 @@ public static ConfidentialClientApplicationBuilder WithAppTokenProvider(
builder.Config.AppTokenProvider = appTokenProvider ?? throw new ArgumentNullException(nameof(appTokenProvider));
return builder;
}
+
+ ///
+ /// Configures a callback to provide the client credential certificate dynamically.
+ /// The callback is invoked before each token acquisition request to the identity provider (including retries).
+ /// This enables scenarios such as certificate rotation and dynamic certificate selection based on application context.
+ ///
+ /// The confidential client application builder.
+ ///
+ /// A callback that provides the certificate based on the application configuration.
+ /// Called before each network request to acquire a token.
+ /// Must return a valid with a private key.
+ ///
+ /// The builder to chain additional configuration calls.
+ /// Thrown when is null.
+ ///
+ /// Thrown at build time if both
+ /// and this method are configured.
+ ///
+ ///
+ /// This method cannot be used together with .
+ /// The callback is not invoked when tokens are retrieved from cache, only for network calls.
+ /// The certificate returned by the callback will be used to sign the client assertion (JWT) for that token request.
+ /// See https://aka.ms/msal-net-client-credentials for more details on client credentials.
+ ///
+ public static ConfidentialClientApplicationBuilder WithCertificate(
+ this ConfidentialClientApplicationBuilder builder,
+ Func certificateProvider)
+ {
+ if (certificateProvider == null)
+ {
+ throw new ArgumentNullException(nameof(certificateProvider));
+ }
+
+ builder.Config.ClientCredentialCertificateProvider = certificateProvider;
+ return builder;
+ }
+
+ ///
+ /// Configures a retry policy for token acquisition failures.
+ /// The policy is invoked after each failed token request to determine whether a retry should be attempted.
+ /// MSAL will respect throttling hints from the identity provider and apply appropriate delays between retries.
+ ///
+ /// The confidential client application builder.
+ ///
+ /// A callback that determines whether to retry after a failure.
+ /// Receives the application configuration and the exception that occurred.
+ /// Returns true to retry the request, or false to stop retrying and throw the exception.
+ /// The callback will be invoked repeatedly after each failure until it returns false.
+ ///
+ /// The builder to chain additional configuration calls.
+ /// Thrown when is null.
+ ///
+ /// The retry policy is only invoked for network failures, not for cached token retrievals.
+ /// When the policy returns true, MSAL will invoke the certificate provider callback again (if configured)
+ /// before making another token request, enabling certificate rotation scenarios.
+ /// MSAL's internal throttling and retry mechanisms will still apply, including respecting Retry-After headers.
+ /// To prevent infinite loops, ensure your retry policy has appropriate termination conditions.
+ ///
+ public static ConfidentialClientApplicationBuilder WithRetry(
+ this ConfidentialClientApplicationBuilder builder,
+ Func retryPolicy)
+ {
+ if (retryPolicy == null)
+ throw new ArgumentNullException(nameof(retryPolicy));
+
+ builder.Config.RetryPolicy = retryPolicy;
+ return builder;
+ }
+
+ ///
+ /// Configures an observer callback that receives the final result of token acquisition.
+ /// The observer is invoked once at the completion of ExecuteAsync, with either a success or failure result.
+ /// This enables scenarios such as telemetry, logging, and custom error handling.
+ ///
+ /// The confidential client application builder.
+ ///
+ /// A callback that receives the application configuration and the execution result.
+ /// The result contains either the successful or the that occurred.
+ /// This callback is invoked after all retries have been exhausted (if retry policy is configured).
+ ///
+ /// The builder to chain additional configuration calls.
+ /// Thrown when is null.
+ ///
+ /// The observer is only invoked for network token acquisition attempts, not for cached token retrievals.
+ /// If multiple calls to WithObserver are made, only the last configured observer will be used.
+ /// Exceptions thrown by the observer callback will be caught and logged internally to prevent disruption of the authentication flow.
+ /// The observer is called on the same thread as the token acquisition request.
+ ///
+ public static ConfidentialClientApplicationBuilder WithObserver(
+ this ConfidentialClientApplicationBuilder builder,
+ Action observer)
+ {
+ if (observer == null)
+ throw new ArgumentNullException(nameof(observer));
+
+ builder.Config.ExecutionObserver = observer;
+ return builder;
+ }
}
}
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs b/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs
new file mode 100644
index 0000000000..779febf2f6
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Identity.Client.Extensibility
+{
+ ///
+ /// Represents the result of a token acquisition attempt.
+ /// Used by the execution observer configured via .
+ ///
+ public class ExecutionResult
+ {
+ ///
+ /// Internal constructor for ExecutionResult.
+ ///
+ internal ExecutionResult() { }
+
+ ///
+ /// Indicates whether the token acquisition was successful.
+ ///
+///
+ /// true if the token was successfully acquired; otherwise, false.
+ ///
+ public bool Successful { get; internal set; }
+
+ ///
+ /// The authentication result if the token acquisition was successful.
+///
+ ///
+ /// An containing the access token and related metadata if is true;
+ /// otherwise, null.
+ ///
+ public AuthenticationResult Result { get; internal set; }
+
+ ///
+/// The exception that occurred if the token acquisition failed.
+ ///
+ ///
+ /// An describing the failure if is false;
+ /// otherwise, null.
+ ///
+public MsalException Exception { get; internal set; }
+}
+}
diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
index 035571f7f2..8cde7a5d70 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;
@@ -35,7 +32,12 @@ public CertificateAndClaimsClientCredential(
Certificate = certificate;
_claimsToSign = claimsToSign;
_appendDefaultClaims = appendDefaultClaims;
- _base64EncodedThumbprint = Base64UrlHelpers.Encode(certificate.GetCertHash());
+
+ // Certificate can be null when using dynamic certificate provider
+ if (certificate != null)
+ {
+ _base64EncodedThumbprint = Base64UrlHelpers.Encode(certificate.GetCertHash());
+ }
}
public Task AddConfidentialClientParametersAsync(
@@ -54,6 +56,9 @@ public Task AddConfidentialClientParametersAsync(
{
requestParameters.RequestContext.Logger.Verbose(() => "Proceeding with JWT token creation and adding client assertion.");
+ // Resolve the certificate - either from static config or dynamic provider
+ X509Certificate2 effectiveCertificate = ResolveCertificate(requestParameters);
+
bool useSha2 = requestParameters.AuthorityManager.Authority.AuthorityInfo.IsSha2CredentialSupported;
var jwtToken = new JsonWebToken(
@@ -63,7 +68,7 @@ public Task AddConfidentialClientParametersAsync(
_claimsToSign,
_appendDefaultClaims);
- string assertion = jwtToken.Sign(Certificate, requestParameters.SendX5C, useSha2);
+ string assertion = jwtToken.Sign(effectiveCertificate, requestParameters.SendX5C, useSha2);
oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer);
oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, assertion);
@@ -76,5 +81,71 @@ public Task AddConfidentialClientParametersAsync(
return Task.CompletedTask;
}
+
+ ///
+ /// Resolves the certificate to use for signing the client assertion.
+ /// If a dynamic certificate provider is configured, it will be invoked to get the certificate.
+ /// Otherwise, the static certificate configured at build time is used.
+ ///
+ /// The authentication request parameters containing app config
+ /// The X509Certificate2 to use for signing
+ /// Thrown if the certificate provider returns null or an invalid certificate
+ private X509Certificate2 ResolveCertificate(AuthenticationRequestParameters requestParameters)
+ {
+ // Check if dynamic certificate provider is configured
+ if (requestParameters.AppConfig.ClientCredentialCertificateProvider != null)
+ {
+ requestParameters.RequestContext.Logger.Verbose(
+ () => "[CertificateAndClaimsClientCredential] Resolving certificate from dynamic provider.");
+
+ // Invoke the provider to get the certificate
+ X509Certificate2 providedCertificate = requestParameters.AppConfig.ClientCredentialCertificateProvider(
+ requestParameters.AppConfig);
+
+ // Validate the certificate returned by the provider
+ if (providedCertificate == null)
+ {
+ requestParameters.RequestContext.Logger.Error(
+ "[CertificateAndClaimsClientCredential] Certificate provider returned null.");
+
+ throw new MsalClientException(
+ MsalError.InvalidClientAssertion,
+ "The certificate provider callback returned null. Ensure the callback returns a valid X509Certificate2 instance.");
+ }
+
+ if (!providedCertificate.HasPrivateKey)
+ {
+ requestParameters.RequestContext.Logger.Error(
+ "[CertificateAndClaimsClientCredential] Certificate from provider does not have a private key.");
+
+ throw new MsalClientException(
+ MsalError.CertWithoutPrivateKey,
+ "The certificate returned by the provider does not have a private key. " +
+ "Ensure the certificate has a private key for signing operations.");
+ }
+
+ requestParameters.RequestContext.Logger.Info(
+ () => $"[CertificateAndClaimsClientCredential] Successfully resolved certificate from provider. " +
+ $"Thumbprint: {providedCertificate.Thumbprint}");
+
+ return providedCertificate;
+ }
+
+ // Use the static certificate configured at build time
+ if (Certificate == null)
+ {
+ requestParameters.RequestContext.Logger.Error(
+ "[CertificateAndClaimsClientCredential] No certificate configured (static or dynamic).");
+
+ throw new MsalClientException(
+ MsalError.InvalidClientAssertion,
+ "No certificate is configured. Use WithCertificate() to provide a certificate.");
+ }
+
+ requestParameters.RequestContext.Logger.Verbose(
+ () => $"[CertificateAndClaimsClientCredential] Using static certificate. Thumbprint: {Certificate.Thumbprint}");
+
+ return Certificate;
+ }
}
}
diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
index 3279f0338a..08925db453 100644
--- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
+++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
@@ -80,6 +80,7 @@
+
@@ -161,4 +162,8 @@
+
+
+
+
diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs
index 526718e7df..90d12e9116 100644
--- a/src/client/Microsoft.Identity.Client/MsalError.cs
+++ b/src/client/Microsoft.Identity.Client/MsalError.cs
@@ -702,6 +702,12 @@ public static class MsalError
///
public const string ClientCredentialAuthenticationTypeMustBeDefined = "Client_Credentials_Required_In_Confidential_Client_Application";
+ ///
+ /// What happens?You configured both a static certificate (WithCertificate(X509Certificate2)) and a dynamic certificate provider (WithCertificate(Func)).
+ /// MitigationChoose one approach for providing the client certificate.
+ ///
+ public const string InvalidClientCredentialConfiguration = "invalid_client_credential_configuration";
+
#region InvalidGrant suberrors
///
/// Issue can be resolved by user interaction during the interactive authentication flow.
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
index 407f3cfb56..9b7afbe438 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
@@ -1,5 +1,6 @@
const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
+const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
@@ -8,3 +9,10 @@ Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsy
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithObserver(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Action observer) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithRetry(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func retryPolicy) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
index 407f3cfb56..9b7afbe438 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
@@ -1,5 +1,6 @@
const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
+const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
@@ -8,3 +9,10 @@ Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsy
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithObserver(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Action observer) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithRetry(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func retryPolicy) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
index 407f3cfb56..0f039f9f0e 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
@@ -1,10 +1,18 @@
const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
+const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
+Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithObserver(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Action observer) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithRetry(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func retryPolicy) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
index 407f3cfb56..0f039f9f0e 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
@@ -1,10 +1,18 @@
const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
+const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
+Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithObserver(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Action observer) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithRetry(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func retryPolicy) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityTests.cs
new file mode 100644
index 0000000000..b7ce29daad
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityTests.cs
@@ -0,0 +1,429 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client;
+using Microsoft.Identity.Client.Extensibility;
+using Microsoft.Identity.Client.Internal.ClientCredential;
+using Microsoft.Identity.Test.Common;
+using Microsoft.Identity.Test.Common.Core.Helpers;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Identity.Test.Unit.AppConfigTests
+{
+ [TestClass]
+ [TestCategory(TestCategories.BuilderTests)]
+ public class ConfidentialClientApplicationExtensibilityTests
+ {
+ private X509Certificate2 _certificate;
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ ApplicationBase.ResetStateForTest();
+ }
+
+ [TestCleanup]
+ public void TestCleanup()
+ {
+ _certificate?.Dispose();
+ }
+
+ #region WithCertificate Tests
+
+ [TestMethod]
+ public void WithCertificate_CallbackIsStored()
+ {
+ // Arrange
+ bool callbackInvoked = false;
+ Func certificateProvider = (config) =>
+ {
+ callbackInvoked = true;
+ return GetTestCertificate();
+ };
+
+ // Act
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithCertificate(certificateProvider)
+ .BuildConcrete();
+
+ // Assert
+ Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ClientCredentialCertificateProvider);
+ Assert.IsFalse(callbackInvoked, "Certificate provider callback is not yet invoked.");
+ }
+
+ [TestMethod]
+ public void WithCertificate_ThrowsOnNullCallback()
+ {
+ // Act & Assert
+ var ex = Assert.ThrowsException(() =>
+ ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithCertificate((Func)null)
+ .Build());
+
+ Assert.AreEqual("certificateProvider", ex.ParamName);
+ }
+
+ [TestMethod]
+ [DeploymentItem(@"Resources\testCert.crtfile")]
+ public void WithCertificate_ThrowsWhenBothStaticAndDynamicCertificateConfigured()
+ {
+ // Arrange
+ var staticCert = GetTestCertificate();
+ Func certificateProvider = (config) => GetTestCertificate();
+
+ // Act & Assert
+ var ex = Assert.ThrowsException(() =>
+ ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithCertificate(staticCert)
+ .WithCertificate(certificateProvider)
+ .Build());
+
+ Assert.AreEqual(MsalError.InvalidClientCredentialConfiguration, ex.ErrorCode);
+ Assert.IsTrue(ex.Message.Contains("Choose one approach"));
+ }
+
+ [TestMethod]
+ [DeploymentItem(@"Resources\testCert.crtfile")]
+ public void WithCertificate_ThrowsWhenDynamicAndThenStaticCertificateConfigured()
+ {
+ // Arrange
+ var staticCert = GetTestCertificate();
+ Func certificateProvider = (config) => GetTestCertificate();
+
+ // Act & Assert
+ var ex = Assert.ThrowsException(() =>
+ ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithCertificate(certificateProvider)
+ .WithCertificate(staticCert)
+ .Build());
+
+ Assert.AreEqual(MsalError.InvalidClientCredentialConfiguration, ex.ErrorCode);
+ }
+
+ [TestMethod]
+ public void WithCertificate_AllowsMultipleCallbackRegistrations_LastOneWins()
+ {
+ // Arrange
+ int firstCallbackInvoked = 0;
+ int secondCallbackInvoked = 0;
+
+ Func firstProvider = (config) =>
+ {
+ firstCallbackInvoked++;
+ return GetTestCertificate();
+ };
+
+ Func secondProvider = (config) =>
+ {
+ secondCallbackInvoked++;
+ return GetTestCertificate();
+ };
+
+ // Act
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithCertificate(firstProvider)
+ .WithCertificate(secondProvider)
+ .BuildConcrete();
+
+ // Assert - last one should be stored
+ var config = app.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config.ClientCredentialCertificateProvider);
+ Assert.AreNotSame(firstProvider, config.ClientCredentialCertificateProvider);
+ }
+
+ #endregion
+
+ #region WithRetry Tests
+
+ [TestMethod]
+ public void WithRetry_CallbackIsStored()
+ {
+ // Arrange
+ Func retryPolicy = (config, ex) => false;
+
+ // Act
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithRetry(retryPolicy)
+ .BuildConcrete();
+
+ // Assert
+ Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.RetryPolicy);
+ }
+
+ [TestMethod]
+ public void WithRetry_ThrowsOnNullCallback()
+ {
+ // Act & Assert
+ var ex = Assert.ThrowsException(() =>
+ ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithRetry(null)
+ .Build());
+
+ Assert.AreEqual("retryPolicy", ex.ParamName);
+ }
+
+ [TestMethod]
+ public void WithRetry_AllowsMultipleRegistrations_LastOneWins()
+ {
+ // Arrange
+ Func firstPolicy = (config, ex) => true;
+ Func secondPolicy = (config, ex) => false;
+
+ // Act
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithRetry(firstPolicy)
+ .WithRetry(secondPolicy)
+ .BuildConcrete();
+
+ // Assert
+ var config = app.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config.RetryPolicy);
+ Assert.AreSame(secondPolicy, config.RetryPolicy);
+ }
+
+ #endregion
+
+ #region WithObserver Tests
+
+ [TestMethod]
+ public void WithObserver_CallbackIsStored()
+ {
+ // Arrange
+ Action observer = (config, result) => { };
+
+ // Act
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithObserver(observer)
+ .BuildConcrete();
+
+ // Assert
+ Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ExecutionObserver);
+ }
+
+ [TestMethod]
+ public void WithObserver_ThrowsOnNullCallback()
+ {
+ // Act & Assert
+ var ex = Assert.ThrowsException(() =>
+ ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithObserver(null)
+ .Build());
+
+ Assert.AreEqual("observer", ex.ParamName);
+ }
+
+ [TestMethod]
+ public void WithObserver_AllowsMultipleRegistrations_LastOneWins()
+ {
+ // Arrange
+ Action firstObserver = (config, result) => { };
+ Action secondObserver = (config, result) => { };
+
+ // Act
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithObserver(firstObserver)
+ .WithObserver(secondObserver)
+ .BuildConcrete();
+
+ // Assert
+ var config = app.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config.ExecutionObserver);
+ Assert.AreSame(secondObserver, config.ExecutionObserver);
+ }
+
+ #endregion
+
+ #region ExecutionResult Tests
+
+ [TestMethod]
+ public void ExecutionResult_CanBeCreated()
+ {
+ // Act
+ var result = new ExecutionResult();
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.IsFalse(result.Successful);
+ Assert.IsNull(result.Result);
+ Assert.IsNull(result.Exception);
+ }
+
+ [TestMethod]
+ public void ExecutionResult_PropertiesCanBeSet()
+ {
+ // Arrange
+ var authResult = new AuthenticationResult(
+ accessToken: "token",
+ isExtendedLifeTimeToken: false,
+ uniqueId: "unique_id",
+ expiresOn: DateTimeOffset.UtcNow.AddHours(1),
+ extendedExpiresOn: DateTimeOffset.UtcNow.AddHours(2),
+ tenantId: TestConstants.TenantId,
+ account: null,
+ idToken: "id_token",
+ scopes: new[] { "scope1" },
+ correlationId: Guid.NewGuid(),
+ tokenType: "Bearer",
+ authenticationResultMetadata: null);
+
+ var msalException = new MsalServiceException("error_code", "error_message");
+
+ // Act - Success case
+ var successResult = new ExecutionResult
+ {
+ Successful = true,
+ Result = authResult,
+ Exception = null
+ };
+
+ // Assert
+ Assert.IsTrue(successResult.Successful);
+ Assert.AreSame(authResult, successResult.Result);
+ Assert.IsNull(successResult.Exception);
+
+ // Act - Failure case
+ var failureResult = new ExecutionResult
+ {
+ Successful = false,
+ Result = null,
+ Exception = msalException
+ };
+
+ // Assert
+ Assert.IsFalse(failureResult.Successful);
+ Assert.IsNull(failureResult.Result);
+ Assert.AreSame(msalException, failureResult.Exception);
+ }
+
+ #endregion
+
+ #region Integration Tests
+
+ [TestMethod]
+ public void AllThreeExtensibilityPoints_CanBeConfiguredTogether()
+ {
+ // Arrange
+ Func certificateProvider = (config) => GetTestCertificate();
+ Func retryPolicy = (config, ex) => false;
+ Action observer = (config, result) => { };
+
+ // Act
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithCertificate(certificateProvider)
+ .WithRetry(retryPolicy)
+ .WithObserver(observer)
+ .BuildConcrete();
+
+ // Assert
+ var config = app.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config.ClientCredentialCertificateProvider);
+ Assert.IsNotNull(config.RetryPolicy);
+ Assert.IsNotNull(config.ExecutionObserver);
+ }
+
+ [TestMethod]
+ public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
+ {
+ // Arrange
+ Func certificateProvider = (config) => GetTestCertificate();
+ Func retryPolicy = (config, ex) => false;
+ Action observer = (config, result) => { };
+
+ // Act - Order: Observer, Retry, Certificate
+ var app1 = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithObserver(observer)
+ .WithRetry(retryPolicy)
+ .WithCertificate(certificateProvider)
+ .BuildConcrete();
+
+ // Act - Order: Retry, Certificate, Observer
+ var app2 = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithRetry(retryPolicy)
+ .WithCertificate(certificateProvider)
+ .WithObserver(observer)
+ .BuildConcrete();
+
+ // Assert
+ var config1 = app1.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config1.ClientCredentialCertificateProvider);
+ Assert.IsNotNull(config1.RetryPolicy);
+ Assert.IsNotNull(config1.ExecutionObserver);
+
+ var config2 = app2.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config2.ClientCredentialCertificateProvider);
+ Assert.IsNotNull(config2.RetryPolicy);
+ Assert.IsNotNull(config2.ExecutionObserver);
+ }
+
+ [TestMethod]
+ public void WithCertificate_WorksWithOtherConfidentialClientOptions()
+ {
+ // Arrange
+ Func certificateProvider = (config) =>
+ {
+ Assert.AreEqual(TestConstants.ClientId, config.ClientId);
+ Assert.AreEqual(TestConstants.TenantId, config.TenantId);
+ return GetTestCertificate();
+ };
+
+ // Act
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs)
+ .WithRedirectUri("https://localhost")
+ .WithClientName("TestApp")
+ .WithClientVersion("1.0.0")
+ .WithCertificate(certificateProvider)
+ .BuildConcrete();
+
+ // Assert
+ Assert.IsNotNull(app);
+ Assert.AreEqual(TestConstants.ClientId, app.AppConfig.ClientId);
+ Assert.AreEqual(TestConstants.TenantId, app.AppConfig.TenantId);
+ Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ClientCredentialCertificateProvider);
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Internal.Analyzers", "IA5352:DoNotMisuseCryptographicApi",
+ Justification = "Test code only")]
+ private X509Certificate2 GetTestCertificate()
+ {
+ if (_certificate == null)
+ {
+ _certificate = new X509Certificate2(
+ ResourceHelper.GetTestResourceRelativePath("testCert.crtfile"),
+ TestConstants.TestCertPassword);
+ }
+ return _certificate;
+ }
+
+ #endregion
+ }
+}
From dac7c01c5bdeaff299b701ef4f3737d942c2fc86 Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Fri, 14 Nov 2025 09:37:19 -0800
Subject: [PATCH 02/12] Add functionality for retry and onSuccess.
---
...ntialClientAcquireTokenParameterBuilder.cs | 3 +-
.../AppConfig/ApplicationConfiguration.cs | 11 +-
.../ConfidentialClientApplicationBuilder.cs | 10 -
.../ClientCredentialExtensionParameters.cs | 42 ++
...ntialClientApplicationBuilderExtensions.cs | 130 +++--
.../Extensibility/ExecutionResult.cs | 26 +-
.../CertificateAndClaimsClientCredential.cs | 20 +-
.../Requests/ClientCredentialRequest.cs | 146 +++++-
.../AbstractManagedIdentity.cs | 2 +-
.../Microsoft.Identity.Client.csproj | 2 +
.../PublicApi/net462/PublicAPI.Unshipped.txt | 18 +-
.../PublicApi/net472/PublicAPI.Unshipped.txt | 18 +-
.../PublicApi/net8.0/PublicAPI.Unshipped.txt | 10 +-
.../netstandard2.0/PublicAPI.Unshipped.txt | 10 +-
...ClientApplicationExtensibilityApiTests.cs} | 233 ++++-----
...tialClientApplicationExtensibilityTests.cs | 476 ++++++++++++++++++
16 files changed, 917 insertions(+), 240 deletions(-)
create mode 100644 src/client/Microsoft.Identity.Client/Extensibility/ClientCredentialExtensionParameters.cs
rename tests/Microsoft.Identity.Test.Unit/AppConfigTests/{ConfidentialClientApplicationExtensibilityTests.cs => ConfidentialClientApplicationExtensibilityApiTests.cs} (56%)
create mode 100644 tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs
index 25d64952be..fcb67f34c6 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs
@@ -48,7 +48,8 @@ protected override void Validate()
// Confidential client must have a credential
if (ServiceBundle?.Config.ClientCredential == null &&
CommonParameters.OnBeforeTokenRequestHandler == null &&
- ServiceBundle?.Config.AppTokenProvider == null
+ ServiceBundle?.Config.AppTokenProvider == null &&
+ ServiceBundle?.Config.ClientCredentialCertificateProvider == null
)
{
throw new MsalClientException(
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
index c163d8bce2..8776b1a043 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
@@ -136,17 +136,18 @@ public string ClientVersion
///
/// Dynamic certificate provider callback for client credential flows.
///
- public Func ClientCredentialCertificateProvider { get; set; }
+ public Func> ClientCredentialCertificateProvider { get; set; }
///
- /// Retry policy callback that determines whether to retry after a token acquisition failure.
+ /// MSAL service failure callback that determines whether to retry after a token acquisition failure from the identity provider.
+ /// Only invoked for MsalServiceException (errors from the Security Token Service).
///
- public Func RetryPolicy { get; set; }
+ public Func> OnMsalServiceFailureCallback { get; set; }
///
- /// Execution observer callback that receives the final result of token acquisition attempts.
+ /// Success callback that receives the result of token acquisition attempts (typically successful, but can include failures after retries are exhausted).
///
- public Action ExecutionObserver { get; set; }
+ public Func OnSuccessCallback { get; set; }
#endregion
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs
index cf68483b40..54234eacfd 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs
@@ -407,16 +407,6 @@ internal override void Validate()
throw new InvalidOperationException(MsalErrorMessage.InvalidRedirectUriReceived(Config.RedirectUri));
}
- // Validate mutual exclusivity between static certificate and dynamic certificate provider
- if (Config.ClientCredential is CertificateClientCredential &&
- Config.ClientCredentialCertificateProvider != null)
- {
- throw new MsalClientException(
- MsalError.InvalidClientCredentialConfiguration,
- "Cannot use both WithCertificate(X509Certificate2) and WithCertificate(Func). " +
- "Choose one approach for providing client credentials.");
- }
-
ValidateAndUpdateRegion();
}
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ClientCredentialExtensionParameters.cs b/src/client/Microsoft.Identity.Client/Extensibility/ClientCredentialExtensionParameters.cs
new file mode 100644
index 0000000000..10ddd89cb0
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ClientCredentialExtensionParameters.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Identity.Client.Extensibility
+{
+ ///
+ /// Provides application configuration context to client credential extensibility callbacks.
+ /// Contains read-only information about the confidential client application.
+ ///
+#if !SUPPORTS_CONFIDENTIAL_CLIENT
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile
+#endif
+ public class ClientCredentialExtensionParameters
+ {
+ ///
+ /// Internal constructor - only MSAL can create instances of this class.
+ ///
+ /// The application configuration.
+ internal ClientCredentialExtensionParameters(ApplicationConfiguration config)
+ {
+ ClientId = config.ClientId;
+ TenantId = config.TenantId;
+ Authority = config.Authority?.AuthorityInfo?.CanonicalAuthority?.ToString();
+ }
+
+ ///
+ /// The application (client) ID as registered in the Azure portal or application registration portal.
+ ///
+ public string ClientId { get; }
+
+ ///
+ /// The tenant ID if the application is configured for a specific tenant.
+ /// Will be null for multi-tenant applications.
+ ///
+ public string TenantId { get; }
+
+ ///
+ /// The authority URL used for authentication (e.g., https://login.microsoftonline.com/common).
+ ///
+ public string Authority { get; }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
index 9043612e24..bcc8d7624a 100644
--- a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
@@ -32,100 +32,150 @@ public static ConfidentialClientApplicationBuilder WithAppTokenProvider(
}
///
- /// Configures a callback to provide the client credential certificate dynamically.
+ /// Configures an async callback to provide the client credential certificate dynamically.
/// The callback is invoked before each token acquisition request to the identity provider (including retries).
/// This enables scenarios such as certificate rotation and dynamic certificate selection based on application context.
///
/// The confidential client application builder.
///
- /// A callback that provides the certificate based on the application configuration.
+ /// An async callback that provides the certificate based on the application configuration.
/// Called before each network request to acquire a token.
/// Must return a valid with a private key.
///
/// The builder to chain additional configuration calls.
/// Thrown when is null.
///
- /// Thrown at build time if both
- /// and this method are configured.
+ /// Thrown if a static certificate is already configured via .
///
///
/// This method cannot be used together with .
/// The callback is not invoked when tokens are retrieved from cache, only for network calls.
/// The certificate returned by the callback will be used to sign the client assertion (JWT) for that token request.
+ /// The callback can perform async operations such as fetching certificates from Azure Key Vault or other secret management systems.
/// See https://aka.ms/msal-net-client-credentials for more details on client credentials.
///
public static ConfidentialClientApplicationBuilder WithCertificate(
this ConfidentialClientApplicationBuilder builder,
- Func certificateProvider)
+ Func> certificateProvider)
{
if (certificateProvider == null)
{
throw new ArgumentNullException(nameof(certificateProvider));
}
-
+
builder.Config.ClientCredentialCertificateProvider = certificateProvider;
+
+ // Create a CertificateAndClaimsClientCredential with null certificate
+ // The certificate will be resolved dynamically via the provider in ResolveCertificateAsync
+ builder.Config.ClientCredential = new Microsoft.Identity.Client.Internal.ClientCredential.CertificateAndClaimsClientCredential(
+ certificate: null,
+ claimsToSign: null,
+ appendDefaultClaims: true);
+
return builder;
}
///
- /// Configures a retry policy for token acquisition failures.
- /// The policy is invoked after each failed token request to determine whether a retry should be attempted.
- /// MSAL will respect throttling hints from the identity provider and apply appropriate delays between retries.
+ /// Configures an async callback that is invoked when MSAL receives an error response from the identity provider (Security Token Service).
+ /// The callback determines whether MSAL should retry the token request or propagate the exception.
+ /// This callback is invoked after each service failure and can be called multiple times until it returns false or the request succeeds.
///
/// The confidential client application builder.
- ///
- /// A callback that determines whether to retry after a failure.
- /// Receives the application configuration and the exception that occurred.
- /// Returns true to retry the request, or false to stop retrying and throw the exception.
- /// The callback will be invoked repeatedly after each failure until it returns false.
+ ///
+ /// An async callback that determines whether to retry after a service failure.
+ /// Receives the application configuration parameters and the that occurred.
+ /// Returns true to retry the request, or false to stop retrying and propagate the exception.
+ /// The callback will be invoked repeatedly after each service failure until it returns false or the request succeeds.
///
/// The builder to chain additional configuration calls.
- /// Thrown when is null.
+ /// Thrown when is null.
///
- /// The retry policy is only invoked for network failures, not for cached token retrievals.
- /// When the policy returns true, MSAL will invoke the certificate provider callback again (if configured)
+ /// This callback is ONLY triggered for - errors returned by the identity provider (e.g., HTTP 500, 503, throttling).
+ /// This callback is NOT triggered for client-side errors () or network failures handled internally by MSAL.
+ /// This callback is only invoked for network token acquisition attempts, not when tokens are retrieved from cache.
+ /// When the callback returns true, MSAL will invoke the certificate provider (if configured via )
/// before making another token request, enabling certificate rotation scenarios.
- /// MSAL's internal throttling and retry mechanisms will still apply, including respecting Retry-After headers.
- /// To prevent infinite loops, ensure your retry policy has appropriate termination conditions.
+ /// MSAL's internal throttling and retry mechanisms will still apply, including respecting Retry-After headers from the identity provider.
+ /// To prevent infinite loops, ensure your callback has appropriate termination conditions (e.g., max retry count, timeout).
+ /// The callback can perform async operations such as logging to remote services, checking external health endpoints, or querying configuration stores.
///
- public static ConfidentialClientApplicationBuilder WithRetry(
+ ///
+ ///
+ /// int retryCount = 0;
+ /// var app = ConfidentialClientApplicationBuilder
+ /// .Create(clientId)
+ /// .WithCertificate(async parameters => await GetCertificateFromKeyVaultAsync(parameters.TenantId))
+ /// .OnMsalServiceFailure(async (parameters, serviceException) =>
+ /// {
+ /// retryCount++;
+ /// await LogExceptionAsync(serviceException);
+ ///
+ /// // Retry up to 3 times for transient service errors (5xx)
+ /// return serviceException.StatusCode >= 500 && retryCount < 3;
+ /// })
+ /// .Build();
+ ///
+ ///
+ public static ConfidentialClientApplicationBuilder OnMsalServiceFailure(
this ConfidentialClientApplicationBuilder builder,
- Func retryPolicy)
+ Func> onMsalServiceFailureCallback)
{
- if (retryPolicy == null)
- throw new ArgumentNullException(nameof(retryPolicy));
+ if (onMsalServiceFailureCallback == null)
+ throw new ArgumentNullException(nameof(onMsalServiceFailureCallback));
- builder.Config.RetryPolicy = retryPolicy;
+ builder.Config.OnMsalServiceFailureCallback = onMsalServiceFailureCallback;
return builder;
}
///
- /// Configures an observer callback that receives the final result of token acquisition.
- /// The observer is invoked once at the completion of ExecuteAsync, with either a success or failure result.
- /// This enables scenarios such as telemetry, logging, and custom error handling.
+ /// Configures an async callback that is invoked when a token acquisition request completes.
+ /// This callback is invoked once per AcquireTokenForClient call, after all retry attempts have been exhausted.
+ /// While named OnSuccess for the common case, this callback fires for both successful and failed acquisitions.
+ /// This enables scenarios such as telemetry, logging, and custom result handling.
///
/// The confidential client application builder.
- ///
- /// A callback that receives the application configuration and the execution result.
+ ///
+ /// An async callback that receives the application configuration parameters and the execution result.
/// The result contains either the successful or the that occurred.
- /// This callback is invoked after all retries have been exhausted (if retry policy is configured).
+ /// This callback is invoked after all retries have been exhausted (if an handler is configured).
///
/// The builder to chain additional configuration calls.
- /// Thrown when is null.
+ /// Thrown when is null.
///
- /// The observer is only invoked for network token acquisition attempts, not for cached token retrievals.
- /// If multiple calls to WithObserver are made, only the last configured observer will be used.
- /// Exceptions thrown by the observer callback will be caught and logged internally to prevent disruption of the authentication flow.
- /// The observer is called on the same thread as the token acquisition request.
+ /// This callback is invoked for both successful and failed token acquisitions. Check to determine the outcome.
+ /// This callback is only invoked for network token acquisition attempts, not when tokens are retrieved from cache.
+ /// If multiple calls to OnSuccess are made, only the last configured callback will be used.
+ /// Exceptions thrown by this callback will be caught and logged internally to prevent disruption of the authentication flow.
+ /// The callback is invoked on the same thread/context as the token acquisition request.
+ /// The callback can perform async operations such as sending telemetry to Application Insights, persisting logs to databases, or triggering webhooks.
///
- public static ConfidentialClientApplicationBuilder WithObserver(
+ ///
+ ///
+ /// var app = ConfidentialClientApplicationBuilder
+ /// .Create(clientId)
+ /// .WithCertificate(certificate)
+ /// .OnSuccess(async (parameters, result) =>
+ /// {
+ /// if (result.Successful)
+ /// {
+ /// await telemetry.TrackEventAsync("TokenAcquired", new { ClientId = parameters.ClientId });
+ /// }
+ /// else
+ /// {
+ /// await telemetry.TrackExceptionAsync(result.Exception);
+ /// }
+ /// })
+ /// .Build();
+ ///
+ ///
+ public static ConfidentialClientApplicationBuilder OnSuccess(
this ConfidentialClientApplicationBuilder builder,
- Action observer)
+ Func onSuccessCallback)
{
- if (observer == null)
- throw new ArgumentNullException(nameof(observer));
+ if (onSuccessCallback == null)
+ throw new ArgumentNullException(nameof(onSuccessCallback));
- builder.Config.ExecutionObserver = observer;
+ builder.Config.OnSuccessCallback = onSuccessCallback;
return builder;
}
}
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs b/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs
index 779febf2f6..f6660206b2 100644
--- a/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs
@@ -5,7 +5,7 @@ namespace Microsoft.Identity.Client.Extensibility
{
///
/// Represents the result of a token acquisition attempt.
- /// Used by the execution observer configured via .
+ /// Used by the execution observer configured via .
///
public class ExecutionResult
{
@@ -15,29 +15,29 @@ public class ExecutionResult
internal ExecutionResult() { }
///
- /// Indicates whether the token acquisition was successful.
+ /// Indicates whether the token acquisition was successful.
///
-///
+ ///
/// true if the token was successfully acquired; otherwise, false.
- ///
- public bool Successful { get; internal set; }
+ ///
+ public bool Successful { get; internal set; }
///
- /// The authentication result if the token acquisition was successful.
-///
+ /// The authentication result if the token acquisition was successful.
+ ///
///
- /// An containing the access token and related metadata if is true;
+ /// An containing the access token and related metadata if is true;
/// otherwise, null.
///
public AuthenticationResult Result { get; internal set; }
- ///
-/// The exception that occurred if the token acquisition failed.
+ ///
+ /// The exception that occurred if the token acquisition failed.
///
///
- /// An describing the failure if is false;
+ /// An describing the failure if is false;
/// otherwise, null.
///
-public MsalException Exception { get; internal set; }
-}
+ public MsalException Exception { get; internal set; }
+ }
}
diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
index 8cde7a5d70..ef2b608f0d 100644
--- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
@@ -40,7 +40,7 @@ public CertificateAndClaimsClientCredential(
}
}
- public Task AddConfidentialClientParametersAsync(
+ public async Task AddConfidentialClientParametersAsync(
OAuth2Client oAuth2Client,
AuthenticationRequestParameters requestParameters,
ICryptographyManager cryptographyManager,
@@ -57,7 +57,7 @@ public Task AddConfidentialClientParametersAsync(
requestParameters.RequestContext.Logger.Verbose(() => "Proceeding with JWT token creation and adding client assertion.");
// Resolve the certificate - either from static config or dynamic provider
- X509Certificate2 effectiveCertificate = ResolveCertificate(requestParameters);
+ X509Certificate2 effectiveCertificate = await ResolveCertificateAsync(requestParameters, cancellationToken).ConfigureAwait(false);
bool useSha2 = requestParameters.AuthorityManager.Authority.AuthorityInfo.IsSha2CredentialSupported;
@@ -78,8 +78,6 @@ public Task AddConfidentialClientParametersAsync(
// Log that MTLS PoP is required and JWT token creation is skipped
requestParameters.RequestContext.Logger.Verbose(() => "MTLS PoP Client credential request. Skipping client assertion.");
}
-
- return Task.CompletedTask;
}
///
@@ -88,9 +86,12 @@ public Task AddConfidentialClientParametersAsync(
/// Otherwise, the static certificate configured at build time is used.
///
/// The authentication request parameters containing app config
+ /// Cancellation token for the async operation
/// The X509Certificate2 to use for signing
/// Thrown if the certificate provider returns null or an invalid certificate
- private X509Certificate2 ResolveCertificate(AuthenticationRequestParameters requestParameters)
+ private async Task ResolveCertificateAsync(
+ AuthenticationRequestParameters requestParameters,
+ CancellationToken cancellationToken)
{
// Check if dynamic certificate provider is configured
if (requestParameters.AppConfig.ClientCredentialCertificateProvider != null)
@@ -98,10 +99,15 @@ private X509Certificate2 ResolveCertificate(AuthenticationRequestParameters requ
requestParameters.RequestContext.Logger.Verbose(
() => "[CertificateAndClaimsClientCredential] Resolving certificate from dynamic provider.");
- // Invoke the provider to get the certificate
- X509Certificate2 providedCertificate = requestParameters.AppConfig.ClientCredentialCertificateProvider(
+ // Create parameters for the callback
+ var parameters = new Extensibility.ClientCredentialExtensionParameters(
requestParameters.AppConfig);
+ // Invoke the provider to get the certificate
+ X509Certificate2 providedCertificate = await requestParameters.AppConfig
+ .ClientCredentialCertificateProvider(parameters)
+ .ConfigureAwait(false);
+
// Validate the certificate returned by the provider
if (providedCertificate == null)
{
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
index e7c08f0fc8..3f518e13ad 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
@@ -127,14 +127,150 @@ private async Task GetAccessTokenAsync(
{
await ResolveAuthorityAsync().ConfigureAwait(false);
- // Get a token from AAD
- if (ServiceBundle.Config.AppTokenProvider == null)
+ AuthenticationResult authResult = null;
+
+ // Retry loop using the retry callback if configured
+ while (true)
{
- MsalTokenResponse msalTokenResponse = await SendTokenRequestAsync(GetBodyParameters(), cancellationToken).ConfigureAwait(false);
- return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false);
+ try
+ {
+ // Get a token from AAD
+ if (ServiceBundle.Config.AppTokenProvider == null)
+ {
+ logger.Verbose(() => "[ClientCredentialRequest] Sending token request to AAD.");
+ MsalTokenResponse msalTokenResponse = await SendTokenRequestAsync(
+ GetBodyParameters(),
+ cancellationToken).ConfigureAwait(false);
+
+ authResult = await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ // Get a token from the app provider delegate
+ authResult = await GetAccessTokenFromAppProviderAsync(cancellationToken, logger)
+ .ConfigureAwait(false);
+ }
+
+ // Success - invoke OnSuccess callback if configured
+ await InvokeOnSuccessCallbackAsync(authResult, exception: null, logger).ConfigureAwait(false);
+
+ return authResult;
+ }
+ catch (MsalServiceException serviceEx)
+ {
+ // Check if OnMsalServiceFailureCallback is configured
+ if (AuthenticationRequestParameters.AppConfig.OnMsalServiceFailureCallback != null)
+ {
+ logger.Info("[ClientCredentialRequest] MsalServiceException caught. Invoking OnMsalServiceFailureCallback.");
+
+ bool shouldRetry = await InvokeOnMsalServiceFailureCallbackAsync(serviceEx, logger)
+ .ConfigureAwait(false);
+
+ if (shouldRetry)
+ {
+ logger.Info("[ClientCredentialRequest] OnMsalServiceFailureCallback returned true. Retrying token request.");
+ continue; // Retry the loop
+ }
+
+ logger.Info("[ClientCredentialRequest] OnMsalServiceFailureCallback returned false. Propagating exception.");
+ }
+
+ // Invoke OnSuccess callback with failure result
+ await InvokeOnSuccessCallbackAsync(authResult: null, exception: serviceEx, logger).ConfigureAwait(false);
+
+ // Re-throw if no callback or callback returned false
+ throw;
+ }
+ catch (MsalException ex)
+ {
+ // For non-service exceptions (MsalClientException, etc.), invoke OnSuccess and re-throw
+ await InvokeOnSuccessCallbackAsync(authResult: null, exception: ex, logger).ConfigureAwait(false);
+ throw;
+ }
}
+ }
- // Get a token from the app provider delegate
+ ///
+ /// Invokes the OnMsalServiceFailureCallback if configured.
+ /// Returns true if the request should be retried, false otherwise.
+ ///
+ private async Task InvokeOnMsalServiceFailureCallbackAsync(
+ MsalServiceException serviceException,
+ ILoggerAdapter logger)
+ {
+ try
+ {
+ var parameters = new ClientCredentialExtensionParameters(
+ (ApplicationConfiguration)AuthenticationRequestParameters.AppConfig);
+
+ bool shouldRetry = await AuthenticationRequestParameters.AppConfig
+ .OnMsalServiceFailureCallback(parameters, serviceException)
+ .ConfigureAwait(false);
+
+ logger.Verbose(() => $"[ClientCredentialRequest] OnMsalServiceFailureCallback returned: {shouldRetry}");
+ return shouldRetry;
+ }
+ catch (Exception ex)
+ {
+ // If the callback throws, log and don't retry
+ logger.Error($"[ClientCredentialRequest] OnMsalServiceFailureCallback threw an exception: {ex.Message}");
+ logger.ErrorPii(ex);
+ return false;
+ }
+ }
+
+ ///
+ /// Invokes the OnSuccessCallback if configured.
+ /// Exceptions from the callback are caught and logged to prevent disrupting the authentication flow.
+ ///
+ private async Task InvokeOnSuccessCallbackAsync(
+ AuthenticationResult authResult,
+ MsalException exception,
+ ILoggerAdapter logger)
+ {
+ if (AuthenticationRequestParameters.AppConfig.OnSuccessCallback == null)
+ {
+ return;
+ }
+
+ try
+ {
+ logger.Verbose(() => "[ClientCredentialRequest] Invoking OnSuccess callback.");
+
+ var parameters = new ClientCredentialExtensionParameters(
+ (ApplicationConfiguration)AuthenticationRequestParameters.AppConfig);
+
+ var executionResult = new ExecutionResult
+ {
+ Successful = authResult != null,
+ Result = authResult,
+ Exception = exception
+ };
+
+ await AuthenticationRequestParameters.AppConfig
+ .OnSuccessCallback(parameters, executionResult)
+ .ConfigureAwait(false);
+
+ logger.Verbose(() => "[ClientCredentialRequest] OnSuccess callback completed successfully.");
+ }
+ catch (Exception ex)
+ {
+ // Catch and log any exceptions from the observer callback
+ // Do not propagate - observer should not disrupt authentication flow
+ logger.Error($"[ClientCredentialRequest] OnSuccess callback threw an exception: {ex.Message}");
+ logger.ErrorPii(ex);
+ }
+ }
+
+ ///
+ /// Gets an access token from the app token provider.
+ /// Uses semaphore to prevent concurrent calls to the external provider.
+ ///
+ private async Task GetAccessTokenFromAppProviderAsync(
+ CancellationToken cancellationToken,
+ ILoggerAdapter logger)
+ {
AuthenticationResult authResult;
MsalAccessTokenCacheItem cachedAccessTokenItem;
diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs
index 52ef40dbad..f3935b5152 100644
--- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs
+++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs
@@ -312,7 +312,7 @@ private void HandleException(Exception ex,
_requestContext.Logger.Error($"[Managed Identity] Format Exception: {errorMessage}");
CreateAndThrowException(MsalError.InvalidManagedIdentityEndpoint, errorMessage, formatException, source);
}
- else if (ex is not MsalServiceException or TaskCanceledException)
+ else if (ex is not MsalServiceException)
{
_requestContext.Logger.Error($"[Managed Identity] Exception: {ex.Message}");
CreateAndThrowException(MsalError.ManagedIdentityRequestFailed, ex.Message, ex, source);
diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
index 08925db453..81068e24b8 100644
--- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
+++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
@@ -80,6 +80,7 @@
+
@@ -163,6 +164,7 @@
+
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
index 9b7afbe438..bafd8fe278 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
@@ -3,16 +3,20 @@ const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certific
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.Authority.get -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.ClientId.get -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.TenantId.get -> string
+Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithObserver(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Action observer) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithRetry(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func retryPolicy) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-Microsoft.Identity.Client.Extensibility.ExecutionResult
-Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
-Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
-Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
index 9b7afbe438..bafd8fe278 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
@@ -3,16 +3,20 @@ const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certific
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.Authority.get -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.ClientId.get -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.TenantId.get -> string
+Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithObserver(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Action observer) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithRetry(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func retryPolicy) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-Microsoft.Identity.Client.Extensibility.ExecutionResult
-Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
-Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
-Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
index 0f039f9f0e..bafd8fe278 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
@@ -3,6 +3,10 @@ const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certific
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.Authority.get -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.ClientId.get -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.TenantId.get -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
@@ -13,6 +17,6 @@ Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsy
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithObserver(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Action observer) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithRetry(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func retryPolicy) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
index 0f039f9f0e..bafd8fe278 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
@@ -3,6 +3,10 @@ const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certific
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.Authority.get -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.ClientId.get -> string
+Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.TenantId.get -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
@@ -13,6 +17,6 @@ Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsy
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithObserver(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Action observer) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithRetry(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func retryPolicy) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
similarity index 56%
rename from tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityTests.cs
rename to tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
index b7ce29daad..7dfae5082c 100644
--- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
@@ -16,7 +16,7 @@ namespace Microsoft.Identity.Test.Unit.AppConfigTests
{
[TestClass]
[TestCategory(TestCategories.BuilderTests)]
- public class ConfidentialClientApplicationExtensibilityTests
+ public class ConfidentialClientApplicationExtensibilityApiTests
{
private X509Certificate2 _certificate;
@@ -39,7 +39,7 @@ public void WithCertificate_CallbackIsStored()
{
// Arrange
bool callbackInvoked = false;
- Func certificateProvider = (config) =>
+ Func> certificateProvider = async (parameters) =>
{
callbackInvoked = true;
return GetTestCertificate();
@@ -47,9 +47,9 @@ public void WithCertificate_CallbackIsStored()
// Act
var app = ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithCertificate(certificateProvider)
- .BuildConcrete();
+ .Create(TestConstants.ClientId)
+ .WithCertificate(certificateProvider)
+ .BuildConcrete();
// Assert
Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ClientCredentialCertificateProvider);
@@ -61,51 +61,12 @@ public void WithCertificate_ThrowsOnNullCallback()
{
// Act & Assert
var ex = Assert.ThrowsException(() =>
- ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithCertificate((Func)null)
- .Build());
-
- Assert.AreEqual("certificateProvider", ex.ParamName);
- }
-
- [TestMethod]
- [DeploymentItem(@"Resources\testCert.crtfile")]
- public void WithCertificate_ThrowsWhenBothStaticAndDynamicCertificateConfigured()
- {
- // Arrange
- var staticCert = GetTestCertificate();
- Func certificateProvider = (config) => GetTestCertificate();
-
- // Act & Assert
- var ex = Assert.ThrowsException(() =>
ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithCertificate(staticCert)
- .WithCertificate(certificateProvider)
- .Build());
-
- Assert.AreEqual(MsalError.InvalidClientCredentialConfiguration, ex.ErrorCode);
- Assert.IsTrue(ex.Message.Contains("Choose one approach"));
- }
-
- [TestMethod]
- [DeploymentItem(@"Resources\testCert.crtfile")]
- public void WithCertificate_ThrowsWhenDynamicAndThenStaticCertificateConfigured()
- {
- // Arrange
- var staticCert = GetTestCertificate();
- Func certificateProvider = (config) => GetTestCertificate();
-
- // Act & Assert
- var ex = Assert.ThrowsException(() =>
- ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithCertificate(certificateProvider)
- .WithCertificate(staticCert)
- .Build());
+ .Create(TestConstants.ClientId)
+ .WithCertificate((Func>)null)
+ .Build());
- Assert.AreEqual(MsalError.InvalidClientCredentialConfiguration, ex.ErrorCode);
+ Assert.AreEqual("certificateProvider", ex.ParamName);
}
[TestMethod]
@@ -115,24 +76,24 @@ public void WithCertificate_AllowsMultipleCallbackRegistrations_LastOneWins()
int firstCallbackInvoked = 0;
int secondCallbackInvoked = 0;
- Func firstProvider = (config) =>
+ Func> firstProvider = async (parameters) =>
{
firstCallbackInvoked++;
return GetTestCertificate();
};
- Func secondProvider = (config) =>
- {
- secondCallbackInvoked++;
- return GetTestCertificate();
- };
+ Func> secondProvider = async (parameters) =>
+ {
+ secondCallbackInvoked++;
+ return GetTestCertificate();
+ };
// Act
var app = ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithCertificate(firstProvider)
- .WithCertificate(secondProvider)
- .BuildConcrete();
+ .Create(TestConstants.ClientId)
+ .WithCertificate(firstProvider)
+ .WithCertificate(secondProvider)
+ .BuildConcrete();
// Assert - last one should be stored
var config = app.AppConfig as ApplicationConfiguration;
@@ -142,114 +103,114 @@ public void WithCertificate_AllowsMultipleCallbackRegistrations_LastOneWins()
#endregion
- #region WithRetry Tests
+ #region OnMsalServiceFailure Tests
[TestMethod]
- public void WithRetry_CallbackIsStored()
+ public void OnMsalServiceFailure_CallbackIsStored()
{
// Arrange
- Func retryPolicy = (config, ex) => false;
+ Func> onMsalServiceFailureCallback = async (parameters, ex) => false;
// Act
var app = ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithClientSecret(TestConstants.ClientSecret)
- .WithRetry(retryPolicy)
- .BuildConcrete();
+ .Create(TestConstants.ClientId)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .OnMsalServiceFailure(onMsalServiceFailureCallback)
+ .BuildConcrete();
// Assert
- Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.RetryPolicy);
+ Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.OnMsalServiceFailureCallback);
}
[TestMethod]
- public void WithRetry_ThrowsOnNullCallback()
+ public void OnMsalServiceFailure_ThrowsOnNullCallback()
{
// Act & Assert
var ex = Assert.ThrowsException(() =>
- ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithClientSecret(TestConstants.ClientSecret)
- .WithRetry(null)
- .Build());
+ ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .OnMsalServiceFailure(null)
+ .Build());
- Assert.AreEqual("retryPolicy", ex.ParamName);
+ Assert.AreEqual("onMsalServiceFailureCallback", ex.ParamName);
}
[TestMethod]
- public void WithRetry_AllowsMultipleRegistrations_LastOneWins()
+ public void OnMsalServiceFailure_AllowsMultipleRegistrations_LastOneWins()
{
// Arrange
- Func firstPolicy = (config, ex) => true;
- Func secondPolicy = (config, ex) => false;
+ Func> firstPolicy = async (parameters, ex) => true;
+ Func> secondPolicy = async (parameters, ex) => false;
// Act
var app = ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithClientSecret(TestConstants.ClientSecret)
- .WithRetry(firstPolicy)
- .WithRetry(secondPolicy)
- .BuildConcrete();
+ .Create(TestConstants.ClientId)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .OnMsalServiceFailure(firstPolicy)
+ .OnMsalServiceFailure(secondPolicy)
+ .BuildConcrete();
// Assert
var config = app.AppConfig as ApplicationConfiguration;
- Assert.IsNotNull(config.RetryPolicy);
- Assert.AreSame(secondPolicy, config.RetryPolicy);
+ Assert.IsNotNull(config.OnMsalServiceFailureCallback);
+ Assert.AreSame(secondPolicy, config.OnMsalServiceFailureCallback);
}
#endregion
- #region WithObserver Tests
+ #region OnSuccess Tests
[TestMethod]
- public void WithObserver_CallbackIsStored()
+ public void OnSuccess_CallbackIsStored()
{
// Arrange
- Action observer = (config, result) => { };
+ Func onSuccessCallback = async (parameters, result) => { };
// Act
var app = ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithClientSecret(TestConstants.ClientSecret)
- .WithObserver(observer)
- .BuildConcrete();
+ .Create(TestConstants.ClientId)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .OnSuccess(onSuccessCallback)
+ .BuildConcrete();
// Assert
- Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ExecutionObserver);
+ Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.OnSuccessCallback);
}
[TestMethod]
- public void WithObserver_ThrowsOnNullCallback()
+ public void OnSuccess_ThrowsOnNullCallback()
{
// Act & Assert
var ex = Assert.ThrowsException(() =>
ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithClientSecret(TestConstants.ClientSecret)
- .WithObserver(null)
+ .OnSuccess(null)
.Build());
- Assert.AreEqual("observer", ex.ParamName);
+ Assert.AreEqual("onSuccessCallback", ex.ParamName);
}
[TestMethod]
- public void WithObserver_AllowsMultipleRegistrations_LastOneWins()
+ public void OnSuccess_AllowsMultipleRegistrations_LastOneWins()
{
// Arrange
- Action firstObserver = (config, result) => { };
- Action secondObserver = (config, result) => { };
+ Func firstObserver = async (parameters, result) => { };
+ Func secondObserver = async (parameters, result) => { };
// Act
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithClientSecret(TestConstants.ClientSecret)
- .WithObserver(firstObserver)
- .WithObserver(secondObserver)
+ .OnSuccess(firstObserver)
+ .OnSuccess(secondObserver)
.BuildConcrete();
// Assert
var config = app.AppConfig as ApplicationConfiguration;
- Assert.IsNotNull(config.ExecutionObserver);
- Assert.AreSame(secondObserver, config.ExecutionObserver);
+ Assert.IsNotNull(config.OnSuccessCallback);
+ Assert.AreSame(secondObserver, config.OnSuccessCallback);
}
#endregion
@@ -324,86 +285,82 @@ public void ExecutionResult_PropertiesCanBeSet()
public void AllThreeExtensibilityPoints_CanBeConfiguredTogether()
{
// Arrange
- Func certificateProvider = (config) => GetTestCertificate();
- Func retryPolicy = (config, ex) => false;
- Action observer = (config, result) => { };
+ Func> certificateProvider = async (parameters) => GetTestCertificate();
+ Func> onMsalServiceFailure = async (parameters, ex) => false;
+ Func onSuccess = async (parameters, result) => { };
// Act
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithCertificate(certificateProvider)
- .WithRetry(retryPolicy)
- .WithObserver(observer)
+ .OnMsalServiceFailure(onMsalServiceFailure)
+ .OnSuccess(onSuccess)
.BuildConcrete();
// Assert
var config = app.AppConfig as ApplicationConfiguration;
Assert.IsNotNull(config.ClientCredentialCertificateProvider);
- Assert.IsNotNull(config.RetryPolicy);
- Assert.IsNotNull(config.ExecutionObserver);
+ Assert.IsNotNull(config.OnMsalServiceFailureCallback);
+ Assert.IsNotNull(config.OnSuccessCallback);
}
[TestMethod]
public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
{
// Arrange
- Func certificateProvider = (config) => GetTestCertificate();
- Func retryPolicy = (config, ex) => false;
- Action observer = (config, result) => { };
+ Func> certificateProvider = async (parameters) => GetTestCertificate();
+ Func> onMsalServiceFailure = async (parameters, ex) => false;
+ Func onSuccess = async (parameters, result) => { };
- // Act - Order: Observer, Retry, Certificate
+ // Act - Order: OnSuccess, OnMsalServiceFailure, Certificate
var app1 = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
- .WithObserver(observer)
- .WithRetry(retryPolicy)
+ .OnSuccess(onSuccess)
+ .OnMsalServiceFailure(onMsalServiceFailure)
.WithCertificate(certificateProvider)
.BuildConcrete();
- // Act - Order: Retry, Certificate, Observer
+ // Act - Order: OnMsalServiceFailure, Certificate, OnSuccess
var app2 = ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithRetry(retryPolicy)
- .WithCertificate(certificateProvider)
- .WithObserver(observer)
- .BuildConcrete();
+ .Create(TestConstants.ClientId)
+ .OnMsalServiceFailure(onMsalServiceFailure)
+ .WithCertificate(certificateProvider)
+ .OnSuccess(onSuccess)
+ .BuildConcrete();
// Assert
var config1 = app1.AppConfig as ApplicationConfiguration;
Assert.IsNotNull(config1.ClientCredentialCertificateProvider);
- Assert.IsNotNull(config1.RetryPolicy);
- Assert.IsNotNull(config1.ExecutionObserver);
+ Assert.IsNotNull(config1.OnMsalServiceFailureCallback);
+ Assert.IsNotNull(config1.OnSuccessCallback);
var config2 = app2.AppConfig as ApplicationConfiguration;
Assert.IsNotNull(config2.ClientCredentialCertificateProvider);
- Assert.IsNotNull(config2.RetryPolicy);
- Assert.IsNotNull(config2.ExecutionObserver);
+ Assert.IsNotNull(config2.OnMsalServiceFailureCallback);
+ Assert.IsNotNull(config2.OnSuccessCallback);
}
[TestMethod]
public void WithCertificate_WorksWithOtherConfidentialClientOptions()
{
// Arrange
- Func certificateProvider = (config) =>
- {
- Assert.AreEqual(TestConstants.ClientId, config.ClientId);
- Assert.AreEqual(TestConstants.TenantId, config.TenantId);
- return GetTestCertificate();
- };
+ Func> certificateProvider = async (parameters) =>
+ {
+ Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
+ Assert.AreEqual(TestConstants.AadTenantId, parameters.TenantId);
+ return GetTestCertificate();
+ };
// Act
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
- .WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs)
- .WithRedirectUri("https://localhost")
- .WithClientName("TestApp")
- .WithClientVersion("1.0.0")
+ .WithAuthority(TestConstants.AadAuthorityWithTestTenantId)
.WithCertificate(certificateProvider)
.BuildConcrete();
// Assert
Assert.IsNotNull(app);
- Assert.AreEqual(TestConstants.ClientId, app.AppConfig.ClientId);
- Assert.AreEqual(TestConstants.TenantId, app.AppConfig.TenantId);
+
Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ClientCredentialCertificateProvider);
}
@@ -418,8 +375,8 @@ private X509Certificate2 GetTestCertificate()
if (_certificate == null)
{
_certificate = new X509Certificate2(
- ResourceHelper.GetTestResourceRelativePath("testCert.crtfile"),
- TestConstants.TestCertPassword);
+ ResourceHelper.GetTestResourceRelativePath("testCert.crtfile"),
+ TestConstants.TestCertPassword);
}
return _certificate;
}
diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
new file mode 100644
index 0000000000..188fefe86b
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
@@ -0,0 +1,476 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#if !ANDROID && !iOS
+using System;
+using System.Threading.Tasks;
+using Microsoft.Identity.Client;
+using Microsoft.Identity.Client.Extensibility;
+using Microsoft.Identity.Test.Common.Core.Helpers;
+using Microsoft.Identity.Test.Common.Core.Mocks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.Identity.Test.Unit.PublicApiTests
+{
+ [TestClass]
+ [DeploymentItem(@"Resources\testCert.crtfile")]
+ public class ConfidentialClientApplicationExtensibilityTests : TestBase
+ {
+ [TestInitialize]
+ public override void TestInitialize()
+ {
+ base.TestInitialize();
+ }
+
+ #region WithCertificate (Dynamic Provider) Integration Tests
+
+ [TestMethod]
+ [Description("Dynamic certificate provider is invoked and cert is used for client assertion")]
+ public async Task DynamicCertificateProvider_IsInvoked_AndUsedForAssertionAsync()
+ {
+ // Arrange
+ using (var harness = CreateTestHarness())
+ {
+ harness.HttpManager.AddInstanceDiscoveryMockHandler();
+
+ bool providerInvoked = false;
+ ClientCredentialExtensionParameters capturedParameters = null;
+
+ var certificate = CertHelper.GetOrCreateTestCert();
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(TestConstants.AuthorityCommonTenant)
+ .WithHttpManager(harness.HttpManager)
+ .WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
+ {
+ providerInvoked = true;
+ capturedParameters = parameters;
+
+ // Validate parameters
+ Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
+ Assert.IsNotNull(parameters.Authority);
+
+ return certificate;
+ })
+ .Build();
+
+ harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage();
+
+ // Act
+ var result = await app.AcquireTokenForClient(TestConstants.s_scope)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+
+ // Assert
+ Assert.IsTrue(providerInvoked, "Certificate provider should have been invoked");
+ Assert.IsNotNull(capturedParameters);
+ Assert.IsNotNull(result.AccessToken);
+ Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
+ }
+ }
+
+ [TestMethod]
+ [Description("Dynamic certificate provider returning null throws appropriate exception")]
+ public async Task DynamicCertificateProvider_ReturnsNull_ThrowsExceptionAsync()
+ {
+ // Arrange
+ using (var harness = CreateTestHarness())
+ {
+ harness.HttpManager.AddInstanceDiscoveryMockHandler();
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(TestConstants.AuthorityCommonTenant)
+ .WithHttpManager(harness.HttpManager)
+ .WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
+ {
+ return null; // Provider returns null
+ })
+ .Build();
+
+ // Act & Assert
+ var exception = await Assert.ThrowsExceptionAsync(async () =>
+ {
+ await app.AcquireTokenForClient(TestConstants.s_scope)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+
+ Assert.AreEqual(MsalError.InvalidClientAssertion, exception.ErrorCode);
+ Assert.IsTrue(exception.Message.Contains("returned null"));
+ }
+ }
+
+ #endregion
+
+ #region OnMsalServiceFailure Integration Tests
+
+ [TestMethod]
+ [Description("OnMsalServiceFailure is invoked on service exception and retries successfully")]
+ public async Task OnMsalServiceFailure_RetriesOnServiceError_SucceedsAsync()
+ {
+ // Arrange
+ using (var harness = CreateTestHarness())
+ {
+ harness.HttpManager.AddInstanceDiscoveryMockHandler();
+
+ int failureCallbackCount = 0;
+ MsalServiceException capturedException = null;
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(TestConstants.AuthorityCommonTenant)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithHttpManager(harness.HttpManager)
+ .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ {
+ failureCallbackCount++;
+ capturedException = ex as MsalServiceException;
+
+ Assert.IsNotNull(capturedException, "Exception should be MsalServiceException");
+ Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
+
+ // Retry on 503
+ return capturedException.StatusCode == 400 && failureCallbackCount < 3;
+ })
+ .Build();
+
+ // Mock 2 failures, then success
+ harness.HttpManager.AddFailureTokenEndpointResponse("request_failed");
+ harness.HttpManager.AddFailureTokenEndpointResponse("request_failed");
+ harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage();
+
+ // Act
+ var result = await app.AcquireTokenForClient(TestConstants.s_scope)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(2, failureCallbackCount, "Callback should be invoked twice");
+ Assert.IsNotNull(result.AccessToken);
+ Assert.AreEqual(400, capturedException.StatusCode);
+ }
+ }
+
+ [TestMethod]
+ [Description("OnMsalServiceFailure returns false and exception is propagated")]
+ public async Task OnMsalServiceFailure_ReturnsFalse_PropagatesExceptionAsync()
+ {
+ // Arrange
+ using (var harness = CreateTestHarness())
+ {
+ harness.HttpManager.AddInstanceDiscoveryMockHandler();
+
+ bool callbackInvoked = false;
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(TestConstants.AuthorityCommonTenant)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithHttpManager(harness.HttpManager)
+ .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ {
+ callbackInvoked = true;
+ return false; // Don't retry
+ })
+ .Build();
+
+ harness.HttpManager.AddFailureTokenEndpointResponse("request_failed");
+
+ // Act & Assert
+ var exception = await Assert.ThrowsExceptionAsync(async () =>
+ {
+ await app.AcquireTokenForClient(TestConstants.s_scope)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+
+ Assert.IsTrue(callbackInvoked);
+ }
+ }
+
+ [TestMethod]
+ [Description("OnMsalServiceFailure is NOT invoked for client exceptions")]
+ public async Task OnMsalServiceFailure_NotInvokedForClientExceptionsAsync()
+ {
+ // Arrange
+ using (var harness = CreateTestHarness())
+ {
+ harness.HttpManager.AddInstanceDiscoveryMockHandler();
+
+ bool callbackInvoked = false;
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(TestConstants.AuthorityCommonTenant)
+ .WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
+ {
+ return null; // Will cause MsalClientException
+ })
+ .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ {
+ callbackInvoked = true;
+ return false;
+ })
+ .Build();
+
+ // Act & Assert
+ var exception = await Assert.ThrowsExceptionAsync(async () =>
+ {
+ await app.AcquireTokenForClient(TestConstants.s_scope)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+
+ Assert.IsFalse(callbackInvoked, "Callback should NOT be invoked for client exceptions");
+ Assert.AreEqual(MsalError.InvalidClientAssertion, exception.ErrorCode);
+ }
+ }
+
+ #endregion
+
+ #region OnSuccess Integration Tests
+
+ [TestMethod]
+ [Description("OnSuccess is invoked with successful result")]
+ public async Task OnSuccess_InvokedWithSuccessfulResultAsync()
+ {
+ // Arrange
+ using (var harness = CreateTestHarness())
+ {
+ harness.HttpManager.AddInstanceDiscoveryMockHandler();
+
+ bool observerInvoked = false;
+ ExecutionResult capturedResult = null;
+ ClientCredentialExtensionParameters capturedParameters = null;
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(TestConstants.AuthorityCommonTenant)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithHttpManager(harness.HttpManager)
+ .OnSuccess(async (ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ {
+ observerInvoked = true;
+ capturedResult = result;
+ capturedParameters = parameters;
+
+ Assert.IsTrue(result.Successful);
+ Assert.IsNotNull(result.Result);
+ Assert.IsNull(result.Exception);
+ Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
+ })
+ .Build();
+
+ harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage();
+
+ // Act
+ var result = await app.AcquireTokenForClient(TestConstants.s_scope)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+
+ // Assert
+ Assert.IsTrue(observerInvoked, "Observer should be invoked");
+ Assert.IsNotNull(capturedResult);
+ Assert.IsTrue(capturedResult.Successful);
+ Assert.IsNotNull(capturedResult.Result);
+ Assert.AreEqual(result.AccessToken, capturedResult.Result.AccessToken);
+ }
+ }
+
+ [TestMethod]
+ [Description("OnSuccess is invoked with failure result after retries exhausted")]
+ public async Task OnSuccess_InvokedWithFailureResult_AfterRetriesExhaustedAsync()
+ {
+ // Arrange
+ using (var harness = CreateTestHarness())
+ {
+ harness.HttpManager.AddInstanceDiscoveryMockHandler();
+
+ int retryCount = 0;
+ bool observerInvoked = false;
+ ExecutionResult capturedResult = null;
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(TestConstants.AuthorityCommonTenant)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithHttpManager(harness.HttpManager)
+ .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ {
+ retryCount++;
+ return retryCount < 2; // Retry once, then give up
+ })
+ .OnSuccess(async (ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ {
+ observerInvoked = true;
+ capturedResult = result;
+
+ Assert.IsFalse(result.Successful);
+ Assert.IsNull(result.Result);
+ Assert.IsNotNull(result.Exception);
+ Assert.IsInstanceOfType(result.Exception, typeof(MsalServiceException));
+ })
+ .Build();
+
+ // Mock 2 failures
+ harness.HttpManager.AddFailureTokenEndpointResponse("request_failed");
+ harness.HttpManager.AddFailureTokenEndpointResponse("request_failed");
+
+ // Act & Assert
+ var exception = await Assert.ThrowsExceptionAsync(async () =>
+ {
+ await app.AcquireTokenForClient(TestConstants.s_scope)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+ }).ConfigureAwait(false);
+
+ Assert.IsTrue(observerInvoked, "Observer should be invoked even on failure");
+ Assert.IsNotNull(capturedResult);
+ Assert.IsFalse(capturedResult.Successful);
+ Assert.AreEqual(exception, capturedResult.Exception);
+ }
+ }
+
+ [TestMethod]
+ [Description("OnSuccess exception is caught and logged, doesn't disrupt flow")]
+ public async Task OnSuccess_ExceptionIsCaught_DoesNotDisruptFlowAsync()
+ {
+ // Arrange
+ using (var harness = CreateTestHarness())
+ {
+ harness.HttpManager.AddInstanceDiscoveryMockHandler();
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(TestConstants.AuthorityCommonTenant)
+ .WithClientSecret(TestConstants.ClientSecret)
+ .WithHttpManager(harness.HttpManager)
+ .OnSuccess(async (ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ {
+ throw new InvalidOperationException("Observer threw exception");
+ })
+ .Build();
+
+ harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage();
+
+ // Act - should NOT throw, observer exception should be caught
+ var result = await app.AcquireTokenForClient(TestConstants.s_scope)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.IsNotNull(result.AccessToken);
+ }
+ }
+
+ #endregion
+
+ #region Combined Scenarios
+
+ [TestMethod]
+ [Description("All three extensibility points work together: cert provider, retry, observer")]
+ public async Task AllThreeExtensibilityPoints_WorkTogetherAsync()
+ {
+ // Arrange
+ using (var harness = CreateTestHarness())
+ {
+ harness.HttpManager.AddInstanceDiscoveryMockHandler();
+
+ int certProviderCount = 0;
+ int retryCallbackCount = 0;
+ bool observerInvoked = false;
+
+ var certificate = CertHelper.GetOrCreateTestCert();
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(TestConstants.AuthorityCommonTenant)
+ .WithHttpManager(harness.HttpManager)
+ .WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
+ {
+ certProviderCount++;
+ Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
+ return certificate;
+ })
+ .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ {
+ retryCallbackCount++;
+ Assert.IsInstanceOfType(ex, typeof(MsalServiceException));
+ return retryCallbackCount < 2; // Retry once
+ })
+ .OnSuccess(async (ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ {
+ observerInvoked = true;
+ Assert.IsTrue(result.Successful);
+ Assert.IsNotNull(result.Result);
+ })
+ .Build();
+
+ // Mock: fail once, then succeed
+ harness.HttpManager.AddFailureTokenEndpointResponse("request_failed");
+ harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage();
+
+ // Act
+ var result = await app.AcquireTokenForClient(TestConstants.s_scope)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(2, certProviderCount, "Cert provider invoked for initial + retry");
+ Assert.AreEqual(1, retryCallbackCount, "Retry callback invoked once");
+ Assert.IsTrue(observerInvoked, "Observer invoked once at completion");
+ Assert.IsNotNull(result.AccessToken);
+ }
+ }
+
+ [TestMethod]
+ [Description("Certificate rotation scenario: different cert returned on retry")]
+ public async Task CertificateRotation_DifferentCertOnRetryAsync()
+ {
+ // Arrange
+ using (var harness = CreateTestHarness())
+ {
+ harness.HttpManager.AddInstanceDiscoveryMockHandler();
+
+ int certProviderCount = 0;
+ var cert1 = CertHelper.GetOrCreateTestCert();
+ var cert2 = CertHelper.GetOrCreateTestCert(regenerateCert: true);
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(TestConstants.ClientId)
+ .WithAuthority(TestConstants.AuthorityCommonTenant)
+ .WithHttpManager(harness.HttpManager)
+ .WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
+ {
+ certProviderCount++;
+ // Return different cert on retry
+ return certProviderCount == 1 ? cert1 : cert2;
+ })
+ .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ {
+ return true; // Always retry once
+ })
+ .Build();
+
+ // First call fails (cert1), second succeeds (cert2)
+ harness.HttpManager.AddFailureTokenEndpointResponse("request_failed");
+ harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage();
+
+ // Act
+ var result = await app.AcquireTokenForClient(TestConstants.s_scope)
+ .ExecuteAsync()
+ .ConfigureAwait(false);
+
+ // Assert
+ Assert.AreEqual(2, certProviderCount, "Provider should be called twice");
+ Assert.IsNotNull(result.AccessToken);
+ }
+ }
+
+ #endregion
+ }
+}
+#endif
From 17765a01d3f12cf2fd21ee5a5c688cb37981316c Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Fri, 14 Nov 2025 09:53:35 -0800
Subject: [PATCH 03/12] Fix build
---
.../PublicApi/net462/PublicAPI.Unshipped.txt | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
index 2014bd37c6..c9bafe148a 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
@@ -22,4 +22,3 @@ static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
\ No newline at end of file
From 9270a68543702ab4d55015c19fca42fa10b406d3 Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Fri, 14 Nov 2025 14:33:05 -0800
Subject: [PATCH 04/12] Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../ConfidentialClientApplicationBuilderExtensions.cs | 4 +---
.../ConfidentialClientApplicationExtensibilityApiTests.cs | 5 +++++
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
index bcc8d7624a..2a63f66aba 100644
--- a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
@@ -44,9 +44,7 @@ public static ConfidentialClientApplicationBuilder WithAppTokenProvider(
///
/// The builder to chain additional configuration calls.
/// Thrown when is null.
- ///
- /// Thrown if a static certificate is already configured via .
- ///
+
///
/// This method cannot be used together with .
/// The callback is not invoked when tokens are retrieved from cache, only for network calls.
diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
index 7dfae5082c..a1bbdd4328 100644
--- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
@@ -97,6 +97,7 @@ public void WithCertificate_AllowsMultipleCallbackRegistrations_LastOneWins()
// Assert - last one should be stored
var config = app.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config);
Assert.IsNotNull(config.ClientCredentialCertificateProvider);
Assert.AreNotSame(firstProvider, config.ClientCredentialCertificateProvider);
}
@@ -153,6 +154,7 @@ public void OnMsalServiceFailure_AllowsMultipleRegistrations_LastOneWins()
// Assert
var config = app.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config, "AppConfig should be of type ApplicationConfiguration.");
Assert.IsNotNull(config.OnMsalServiceFailureCallback);
Assert.AreSame(secondPolicy, config.OnMsalServiceFailureCallback);
}
@@ -209,6 +211,7 @@ public void OnSuccess_AllowsMultipleRegistrations_LastOneWins()
// Assert
var config = app.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config, "AppConfig is not of type ApplicationConfiguration.");
Assert.IsNotNull(config.OnSuccessCallback);
Assert.AreSame(secondObserver, config.OnSuccessCallback);
}
@@ -330,11 +333,13 @@ public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
// Assert
var config1 = app1.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config1);
Assert.IsNotNull(config1.ClientCredentialCertificateProvider);
Assert.IsNotNull(config1.OnMsalServiceFailureCallback);
Assert.IsNotNull(config1.OnSuccessCallback);
var config2 = app2.AppConfig as ApplicationConfiguration;
+ Assert.IsNotNull(config2, "app2.AppConfig should be of type ApplicationConfiguration");
Assert.IsNotNull(config2.ClientCredentialCertificateProvider);
Assert.IsNotNull(config2.OnMsalServiceFailureCallback);
Assert.IsNotNull(config2.OnSuccessCallback);
From c7363e043338048302394e2d772bbbdf305f1f89 Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Mon, 17 Nov 2025 13:15:01 -0800
Subject: [PATCH 05/12] Make the APIs experimental
---
...ntialClientApplicationBuilderExtensions.cs | 24 +++--
.../Requests/ClientCredentialRequest.cs | 5 --
...lClientApplicationExtensibilityApiTests.cs | 88 ++++++-------------
...tialClientApplicationExtensibilityTests.cs | 12 ++-
4 files changed, 56 insertions(+), 73 deletions(-)
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
index 2a63f66aba..1b9320b1b0 100644
--- a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
@@ -4,6 +4,7 @@
using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
+using Microsoft.Identity.Client.Internal.ClientCredential;
namespace Microsoft.Identity.Client.Extensibility
{
@@ -44,18 +45,18 @@ public static ConfidentialClientApplicationBuilder WithAppTokenProvider(
///
/// The builder to chain additional configuration calls.
/// Thrown when is null.
-
///
- /// This method cannot be used together with .
/// The callback is not invoked when tokens are retrieved from cache, only for network calls.
/// The certificate returned by the callback will be used to sign the client assertion (JWT) for that token request.
/// The callback can perform async operations such as fetching certificates from Azure Key Vault or other secret management systems.
+ /// This callback is used together with and
/// See https://aka.ms/msal-net-client-credentials for more details on client credentials.
///
public static ConfidentialClientApplicationBuilder WithCertificate(
this ConfidentialClientApplicationBuilder builder,
Func> certificateProvider)
{
+ builder.ValidateUseOfExperimentalFeature();
if (certificateProvider == null)
{
throw new ArgumentNullException(nameof(certificateProvider));
@@ -65,7 +66,7 @@ public static ConfidentialClientApplicationBuilder WithCertificate(
// Create a CertificateAndClaimsClientCredential with null certificate
// The certificate will be resolved dynamically via the provider in ResolveCertificateAsync
- builder.Config.ClientCredential = new Microsoft.Identity.Client.Internal.ClientCredential.CertificateAndClaimsClientCredential(
+ builder.Config.ClientCredential = new CertificateAndClaimsClientCredential(
certificate: null,
claimsToSign: null,
appendDefaultClaims: true);
@@ -74,7 +75,7 @@ public static ConfidentialClientApplicationBuilder WithCertificate(
}
///
- /// Configures an async callback that is invoked when MSAL receives an error response from the identity provider (Security Token Service).
+ /// Configures an async callback that is invoked when MSAL receives an error response from the identity provider.
/// The callback determines whether MSAL should retry the token request or propagate the exception.
/// This callback is invoked after each service failure and can be called multiple times until it returns false or the request succeeds.
///
@@ -88,7 +89,7 @@ public static ConfidentialClientApplicationBuilder WithCertificate(
/// The builder to chain additional configuration calls.
/// Thrown when is null.
///
- /// This callback is ONLY triggered for - errors returned by the identity provider (e.g., HTTP 500, 503, throttling).
+ /// This callback is ONLY triggered for - errors returned by STS.
/// This callback is NOT triggered for client-side errors () or network failures handled internally by MSAL.
/// This callback is only invoked for network token acquisition attempts, not when tokens are retrieved from cache.
/// When the callback returns true, MSAL will invoke the certificate provider (if configured via )
@@ -96,6 +97,7 @@ public static ConfidentialClientApplicationBuilder WithCertificate(
/// MSAL's internal throttling and retry mechanisms will still apply, including respecting Retry-After headers from the identity provider.
/// To prevent infinite loops, ensure your callback has appropriate termination conditions (e.g., max retry count, timeout).
/// The callback can perform async operations such as logging to remote services, checking external health endpoints, or querying configuration stores.
+ /// This callback is used together with callback.
///
///
///
@@ -108,8 +110,8 @@ public static ConfidentialClientApplicationBuilder WithCertificate(
/// retryCount++;
/// await LogExceptionAsync(serviceException);
///
- /// // Retry up to 3 times for transient service errors (5xx)
- /// return serviceException.StatusCode >= 500 && retryCount < 3;
+ /// // Retry up to 3 times for errors received from STS
+ /// return serviceException.ErrorCode == "SpecificErrorCodeToRetry"; retryCount < 3;
/// })
/// .Build();
///
@@ -118,8 +120,12 @@ public static ConfidentialClientApplicationBuilder OnMsalServiceFailure(
this ConfidentialClientApplicationBuilder builder,
Func> onMsalServiceFailureCallback)
{
+ builder.ValidateUseOfExperimentalFeature();
+
if (onMsalServiceFailureCallback == null)
+ {
throw new ArgumentNullException(nameof(onMsalServiceFailureCallback));
+ }
builder.Config.OnMsalServiceFailureCallback = onMsalServiceFailureCallback;
return builder;
@@ -170,8 +176,12 @@ public static ConfidentialClientApplicationBuilder OnSuccess(
this ConfidentialClientApplicationBuilder builder,
Func onSuccessCallback)
{
+ builder.ValidateUseOfExperimentalFeature();
+
if (onSuccessCallback == null)
+ {
throw new ArgumentNullException(nameof(onSuccessCallback));
+ }
builder.Config.OnSuccessCallback = onSuccessCallback;
return builder;
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
index 3f518e13ad..469f37759a 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
@@ -3,10 +3,7 @@
using System;
using System.Collections.Generic;
-using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Parameters;
@@ -14,8 +11,6 @@
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Extensibility;
using Microsoft.Identity.Client.Instance;
-using Microsoft.Identity.Client.Internal.ClientCredential;
-using Microsoft.Identity.Client.Internal.Requests;
using Microsoft.Identity.Client.OAuth2;
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
using Microsoft.Identity.Client.Utils;
diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
index a1bbdd4328..f0e4082605 100644
--- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
@@ -39,15 +39,16 @@ public void WithCertificate_CallbackIsStored()
{
// Arrange
bool callbackInvoked = false;
- Func> certificateProvider = async (parameters) =>
+ async Task certificateProvider(ClientCredentialExtensionParameters parameters)
{
callbackInvoked = true;
return GetTestCertificate();
- };
+ }
// Act
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithCertificate(certificateProvider)
.BuildConcrete();
@@ -63,6 +64,7 @@ public void WithCertificate_ThrowsOnNullCallback()
var ex = Assert.ThrowsException(() =>
ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithCertificate((Func>)null)
.Build());
@@ -76,21 +78,22 @@ public void WithCertificate_AllowsMultipleCallbackRegistrations_LastOneWins()
int firstCallbackInvoked = 0;
int secondCallbackInvoked = 0;
- Func> firstProvider = async (parameters) =>
+ async Task firstProvider(ClientCredentialExtensionParameters parameters)
{
firstCallbackInvoked++;
return GetTestCertificate();
- };
+ }
- Func> secondProvider = async (parameters) =>
+ async Task secondProvider(ClientCredentialExtensionParameters parameters)
{
secondCallbackInvoked++;
return GetTestCertificate();
- };
+ }
// Act
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithCertificate(firstProvider)
.WithCertificate(secondProvider)
.BuildConcrete();
@@ -110,11 +113,12 @@ public void WithCertificate_AllowsMultipleCallbackRegistrations_LastOneWins()
public void OnMsalServiceFailure_CallbackIsStored()
{
// Arrange
- Func> onMsalServiceFailureCallback = async (parameters, ex) => false;
+ async Task onMsalServiceFailureCallback(ClientCredentialExtensionParameters parameters, MsalException ex) => false;
// Act
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithClientSecret(TestConstants.ClientSecret)
.OnMsalServiceFailure(onMsalServiceFailureCallback)
.BuildConcrete();
@@ -130,6 +134,7 @@ public void OnMsalServiceFailure_ThrowsOnNullCallback()
var ex = Assert.ThrowsException(() =>
ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithClientSecret(TestConstants.ClientSecret)
.OnMsalServiceFailure(null)
.Build());
@@ -137,28 +142,6 @@ public void OnMsalServiceFailure_ThrowsOnNullCallback()
Assert.AreEqual("onMsalServiceFailureCallback", ex.ParamName);
}
- [TestMethod]
- public void OnMsalServiceFailure_AllowsMultipleRegistrations_LastOneWins()
- {
- // Arrange
- Func> firstPolicy = async (parameters, ex) => true;
- Func> secondPolicy = async (parameters, ex) => false;
-
- // Act
- var app = ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithClientSecret(TestConstants.ClientSecret)
- .OnMsalServiceFailure(firstPolicy)
- .OnMsalServiceFailure(secondPolicy)
- .BuildConcrete();
-
- // Assert
- var config = app.AppConfig as ApplicationConfiguration;
- Assert.IsNotNull(config, "AppConfig should be of type ApplicationConfiguration.");
- Assert.IsNotNull(config.OnMsalServiceFailureCallback);
- Assert.AreSame(secondPolicy, config.OnMsalServiceFailureCallback);
- }
-
#endregion
#region OnSuccess Tests
@@ -167,11 +150,13 @@ public void OnMsalServiceFailure_AllowsMultipleRegistrations_LastOneWins()
public void OnSuccess_CallbackIsStored()
{
// Arrange
- Func onSuccessCallback = async (parameters, result) => { };
+ async Task onSuccessCallback(ClientCredentialExtensionParameters parameters, ExecutionResult result)
+ { }
// Act
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithClientSecret(TestConstants.ClientSecret)
.OnSuccess(onSuccessCallback)
.BuildConcrete();
@@ -187,6 +172,7 @@ public void OnSuccess_ThrowsOnNullCallback()
var ex = Assert.ThrowsException(() =>
ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithClientSecret(TestConstants.ClientSecret)
.OnSuccess(null)
.Build());
@@ -194,28 +180,6 @@ public void OnSuccess_ThrowsOnNullCallback()
Assert.AreEqual("onSuccessCallback", ex.ParamName);
}
- [TestMethod]
- public void OnSuccess_AllowsMultipleRegistrations_LastOneWins()
- {
- // Arrange
- Func firstObserver = async (parameters, result) => { };
- Func secondObserver = async (parameters, result) => { };
-
- // Act
- var app = ConfidentialClientApplicationBuilder
- .Create(TestConstants.ClientId)
- .WithClientSecret(TestConstants.ClientSecret)
- .OnSuccess(firstObserver)
- .OnSuccess(secondObserver)
- .BuildConcrete();
-
- // Assert
- var config = app.AppConfig as ApplicationConfiguration;
- Assert.IsNotNull(config, "AppConfig is not of type ApplicationConfiguration.");
- Assert.IsNotNull(config.OnSuccessCallback);
- Assert.AreSame(secondObserver, config.OnSuccessCallback);
- }
-
#endregion
#region ExecutionResult Tests
@@ -288,13 +252,14 @@ public void ExecutionResult_PropertiesCanBeSet()
public void AllThreeExtensibilityPoints_CanBeConfiguredTogether()
{
// Arrange
- Func> certificateProvider = async (parameters) => GetTestCertificate();
- Func> onMsalServiceFailure = async (parameters, ex) => false;
- Func onSuccess = async (parameters, result) => { };
+ async Task certificateProvider(ClientCredentialExtensionParameters parameters) => GetTestCertificate();
+ async Task onMsalServiceFailure(ClientCredentialExtensionParameters parameters, MsalException ex) => false;
+ async Task onSuccess(ClientCredentialExtensionParameters parameters, ExecutionResult result) { }
// Act
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithCertificate(certificateProvider)
.OnMsalServiceFailure(onMsalServiceFailure)
.OnSuccess(onSuccess)
@@ -311,13 +276,14 @@ public void AllThreeExtensibilityPoints_CanBeConfiguredTogether()
public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
{
// Arrange
- Func> certificateProvider = async (parameters) => GetTestCertificate();
- Func> onMsalServiceFailure = async (parameters, ex) => false;
- Func onSuccess = async (parameters, result) => { };
+ async Task certificateProvider(ClientCredentialExtensionParameters parameters) => GetTestCertificate();
+ async Task onMsalServiceFailure(ClientCredentialExtensionParameters parameters, MsalException ex) => false;
+ async Task onSuccess(ClientCredentialExtensionParameters parameters, ExecutionResult result) { }
// Act - Order: OnSuccess, OnMsalServiceFailure, Certificate
var app1 = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.OnSuccess(onSuccess)
.OnMsalServiceFailure(onMsalServiceFailure)
.WithCertificate(certificateProvider)
@@ -326,6 +292,7 @@ public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
// Act - Order: OnMsalServiceFailure, Certificate, OnSuccess
var app2 = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.OnMsalServiceFailure(onMsalServiceFailure)
.WithCertificate(certificateProvider)
.OnSuccess(onSuccess)
@@ -349,16 +316,17 @@ public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
public void WithCertificate_WorksWithOtherConfidentialClientOptions()
{
// Arrange
- Func> certificateProvider = async (parameters) =>
+ async Task certificateProvider(ClientCredentialExtensionParameters parameters)
{
Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
Assert.AreEqual(TestConstants.AadTenantId, parameters.TenantId);
return GetTestCertificate();
- };
+ }
// Act
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AadAuthorityWithTestTenantId)
.WithCertificate(certificateProvider)
.BuildConcrete();
diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
index 188fefe86b..7b19c4f28f 100644
--- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
@@ -40,6 +40,7 @@ public async Task DynamicCertificateProvider_IsInvoked_AndUsedForAssertionAsync(
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
.WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
@@ -81,6 +82,7 @@ public async Task DynamicCertificateProvider_ReturnsNull_ThrowsExceptionAsync()
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
.WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
@@ -120,6 +122,7 @@ public async Task OnMsalServiceFailure_RetriesOnServiceError_SucceedsAsync()
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
@@ -166,6 +169,7 @@ public async Task OnMsalServiceFailure_ReturnsFalse_PropagatesExceptionAsync()
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
@@ -179,7 +183,7 @@ public async Task OnMsalServiceFailure_ReturnsFalse_PropagatesExceptionAsync()
harness.HttpManager.AddFailureTokenEndpointResponse("request_failed");
// Act & Assert
- var exception = await Assert.ThrowsExceptionAsync(async () =>
+ await Assert.ThrowsExceptionAsync(async () =>
{
await app.AcquireTokenForClient(TestConstants.s_scope)
.ExecuteAsync()
@@ -203,6 +207,7 @@ public async Task OnMsalServiceFailure_NotInvokedForClientExceptionsAsync()
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
{
@@ -247,6 +252,7 @@ public async Task OnSuccess_InvokedWithSuccessfulResultAsync()
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
@@ -294,6 +300,7 @@ public async Task OnSuccess_InvokedWithFailureResult_AfterRetriesExhaustedAsync(
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
@@ -344,6 +351,7 @@ public async Task OnSuccess_ExceptionIsCaught_DoesNotDisruptFlowAsync()
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
@@ -387,6 +395,7 @@ public async Task AllThreeExtensibilityPoints_WorkTogetherAsync()
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
.WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
@@ -441,6 +450,7 @@ public async Task CertificateRotation_DifferentCertOnRetryAsync()
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
+ .WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
.WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
From 1f5b23ab07d17b2a0ecc6c5001e4165d5712d570 Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Tue, 18 Nov 2025 13:03:10 -0800
Subject: [PATCH 06/12] Fix build failures in pipeline
---
...lClientApplicationExtensibilityApiTests.cs | 34 +++++++++----------
1 file changed, 16 insertions(+), 18 deletions(-)
diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
index f0e4082605..9bf1fa1379 100644
--- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
@@ -39,10 +39,10 @@ public void WithCertificate_CallbackIsStored()
{
// Arrange
bool callbackInvoked = false;
- async Task certificateProvider(ClientCredentialExtensionParameters parameters)
+ Task certificateProvider(ClientCredentialExtensionParameters parameters)
{
callbackInvoked = true;
- return GetTestCertificate();
+ return Task.FromResult(GetTestCertificate());
}
// Act
@@ -78,16 +78,16 @@ public void WithCertificate_AllowsMultipleCallbackRegistrations_LastOneWins()
int firstCallbackInvoked = 0;
int secondCallbackInvoked = 0;
- async Task firstProvider(ClientCredentialExtensionParameters parameters)
+ Task firstProvider(ClientCredentialExtensionParameters parameters)
{
firstCallbackInvoked++;
- return GetTestCertificate();
+ return Task.FromResult(GetTestCertificate());
}
- async Task secondProvider(ClientCredentialExtensionParameters parameters)
+ Task secondProvider(ClientCredentialExtensionParameters parameters)
{
secondCallbackInvoked++;
- return GetTestCertificate();
+ return Task.FromResult(GetTestCertificate());
}
// Act
@@ -113,7 +113,7 @@ async Task secondProvider(ClientCredentialExtensionParameters
public void OnMsalServiceFailure_CallbackIsStored()
{
// Arrange
- async Task onMsalServiceFailureCallback(ClientCredentialExtensionParameters parameters, MsalException ex) => false;
+ Task onMsalServiceFailureCallback(ClientCredentialExtensionParameters parameters, MsalException ex) => Task.FromResult(false);
// Act
var app = ConfidentialClientApplicationBuilder
@@ -150,8 +150,7 @@ public void OnMsalServiceFailure_ThrowsOnNullCallback()
public void OnSuccess_CallbackIsStored()
{
// Arrange
- async Task onSuccessCallback(ClientCredentialExtensionParameters parameters, ExecutionResult result)
- { }
+ Task onSuccessCallback(ClientCredentialExtensionParameters parameters, ExecutionResult result) => Task.CompletedTask;
// Act
var app = ConfidentialClientApplicationBuilder
@@ -252,9 +251,9 @@ public void ExecutionResult_PropertiesCanBeSet()
public void AllThreeExtensibilityPoints_CanBeConfiguredTogether()
{
// Arrange
- async Task certificateProvider(ClientCredentialExtensionParameters parameters) => GetTestCertificate();
- async Task onMsalServiceFailure(ClientCredentialExtensionParameters parameters, MsalException ex) => false;
- async Task onSuccess(ClientCredentialExtensionParameters parameters, ExecutionResult result) { }
+ Task certificateProvider(ClientCredentialExtensionParameters parameters) => Task.FromResult(GetTestCertificate());
+ Task onMsalServiceFailure(ClientCredentialExtensionParameters parameters, MsalException ex) => Task.FromResult(false);
+ Task onSuccess(ClientCredentialExtensionParameters parameters, ExecutionResult result) => Task.CompletedTask;
// Act
var app = ConfidentialClientApplicationBuilder
@@ -276,9 +275,9 @@ async Task onSuccess(ClientCredentialExtensionParameters parameters, ExecutionRe
public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
{
// Arrange
- async Task certificateProvider(ClientCredentialExtensionParameters parameters) => GetTestCertificate();
- async Task onMsalServiceFailure(ClientCredentialExtensionParameters parameters, MsalException ex) => false;
- async Task onSuccess(ClientCredentialExtensionParameters parameters, ExecutionResult result) { }
+ Task certificateProvider(ClientCredentialExtensionParameters parameters) => Task.FromResult(GetTestCertificate());
+ Task onMsalServiceFailure(ClientCredentialExtensionParameters parameters, MsalException ex) => Task.FromResult(false);
+ Task onSuccess(ClientCredentialExtensionParameters parameters, ExecutionResult result) => Task.CompletedTask;
// Act - Order: OnSuccess, OnMsalServiceFailure, Certificate
var app1 = ConfidentialClientApplicationBuilder
@@ -316,11 +315,11 @@ async Task onSuccess(ClientCredentialExtensionParameters parameters, ExecutionRe
public void WithCertificate_WorksWithOtherConfidentialClientOptions()
{
// Arrange
- async Task certificateProvider(ClientCredentialExtensionParameters parameters)
+ Task certificateProvider(ClientCredentialExtensionParameters parameters)
{
Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
Assert.AreEqual(TestConstants.AadTenantId, parameters.TenantId);
- return GetTestCertificate();
+ return Task.FromResult(GetTestCertificate());
}
// Act
@@ -333,7 +332,6 @@ async Task certificateProvider(ClientCredentialExtensionParame
// Assert
Assert.IsNotNull(app);
-
Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ClientCredentialCertificateProvider);
}
From 037e52e50d029155ae3b4389c842115666388480 Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Tue, 18 Nov 2025 16:49:33 -0800
Subject: [PATCH 07/12] Fix tests
---
...tialClientApplicationExtensibilityTests.cs | 57 ++++++++++---------
1 file changed, 31 insertions(+), 26 deletions(-)
diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
index 7b19c4f28f..274f1b4191 100644
--- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
@@ -43,7 +43,7 @@ public async Task DynamicCertificateProvider_IsInvoked_AndUsedForAssertionAsync(
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
- .WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
+ .WithCertificate((ClientCredentialExtensionParameters parameters) =>
{
providerInvoked = true;
capturedParameters = parameters;
@@ -52,7 +52,7 @@ public async Task DynamicCertificateProvider_IsInvoked_AndUsedForAssertionAsync(
Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
Assert.IsNotNull(parameters.Authority);
- return certificate;
+ return Task.FromResult(certificate);
})
.Build();
@@ -85,9 +85,9 @@ public async Task DynamicCertificateProvider_ReturnsNull_ThrowsExceptionAsync()
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
- .WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
+ .WithCertificate((ClientCredentialExtensionParameters parameters) =>
{
- return null; // Provider returns null
+ return Task.FromResult(null); // Provider returns null
})
.Build();
@@ -126,7 +126,7 @@ public async Task OnMsalServiceFailure_RetriesOnServiceError_SucceedsAsync()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
{
failureCallbackCount++;
capturedException = ex as MsalServiceException;
@@ -135,7 +135,7 @@ public async Task OnMsalServiceFailure_RetriesOnServiceError_SucceedsAsync()
Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
// Retry on 503
- return capturedException.StatusCode == 400 && failureCallbackCount < 3;
+ return Task.FromResult(capturedException.StatusCode == 400 && failureCallbackCount < 3);
})
.Build();
@@ -173,10 +173,10 @@ public async Task OnMsalServiceFailure_ReturnsFalse_PropagatesExceptionAsync()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
{
callbackInvoked = true;
- return false; // Don't retry
+ return Task.FromResult(false); // Don't retry
})
.Build();
@@ -209,14 +209,14 @@ public async Task OnMsalServiceFailure_NotInvokedForClientExceptionsAsync()
.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
- .WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
+ .WithCertificate((ClientCredentialExtensionParameters parameters) =>
{
- return null; // Will cause MsalClientException
+ return Task.FromResult(null); // Will cause MsalClientException
})
- .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
{
callbackInvoked = true;
- return false;
+ return Task.FromResult(false);
})
.Build();
@@ -256,7 +256,7 @@ public async Task OnSuccess_InvokedWithSuccessfulResultAsync()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnSuccess(async (ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ .OnSuccess((ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
{
observerInvoked = true;
capturedResult = result;
@@ -266,6 +266,8 @@ public async Task OnSuccess_InvokedWithSuccessfulResultAsync()
Assert.IsNotNull(result.Result);
Assert.IsNull(result.Exception);
Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
+
+ return Task.CompletedTask;
})
.Build();
@@ -304,12 +306,12 @@ public async Task OnSuccess_InvokedWithFailureResult_AfterRetriesExhaustedAsync(
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
{
retryCount++;
- return retryCount < 2; // Retry once, then give up
+ return Task.FromResult(retryCount < 2); // Retry once, then give up
})
- .OnSuccess(async (ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ .OnSuccess((ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
{
observerInvoked = true;
capturedResult = result;
@@ -318,6 +320,8 @@ public async Task OnSuccess_InvokedWithFailureResult_AfterRetriesExhaustedAsync(
Assert.IsNull(result.Result);
Assert.IsNotNull(result.Exception);
Assert.IsInstanceOfType(result.Exception, typeof(MsalServiceException));
+
+ return Task.CompletedTask;
})
.Build();
@@ -355,7 +359,7 @@ public async Task OnSuccess_ExceptionIsCaught_DoesNotDisruptFlowAsync()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnSuccess(async (ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ .OnSuccess((ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
{
throw new InvalidOperationException("Observer threw exception");
})
@@ -398,23 +402,24 @@ public async Task AllThreeExtensibilityPoints_WorkTogetherAsync()
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
- .WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
+ .WithCertificate((ClientCredentialExtensionParameters parameters) =>
{
certProviderCount++;
Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
- return certificate;
+ return Task.FromResult(certificate);
})
- .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
{
retryCallbackCount++;
Assert.IsInstanceOfType(ex, typeof(MsalServiceException));
- return retryCallbackCount < 2; // Retry once
+ return Task.FromResult(retryCallbackCount < 2); // Retry once
})
- .OnSuccess(async (ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ .OnSuccess((ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
{
observerInvoked = true;
Assert.IsTrue(result.Successful);
Assert.IsNotNull(result.Result);
+ return Task.CompletedTask;
})
.Build();
@@ -453,15 +458,15 @@ public async Task CertificateRotation_DifferentCertOnRetryAsync()
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
- .WithCertificate(async (ClientCredentialExtensionParameters parameters) =>
+ .WithCertificate((ClientCredentialExtensionParameters parameters) =>
{
certProviderCount++;
// Return different cert on retry
- return certProviderCount == 1 ? cert1 : cert2;
+ return Task.FromResult(certProviderCount == 1 ? cert1 : cert2);
})
- .OnMsalServiceFailure(async (ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
{
- return true; // Always retry once
+ return Task.FromResult(true); // Always retry once
})
.Build();
From 9fb9dc771bd1c1ba4fa6decff2dd6c28f1bf9c4a Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Mon, 24 Nov 2025 11:38:53 -0800
Subject: [PATCH 08/12] Update to use AssertionRequestOptions
---
.../AppConfig/ApplicationConfiguration.cs | 6 +--
.../AppConfig/AssertionRequestOptions.cs | 37 +++++++++++--
.../ClientCredentialExtensionParameters.cs | 42 ---------------
...ntialClientApplicationBuilderExtensions.cs | 42 +++++++--------
.../CertificateAndClaimsClientCredential.cs | 10 ++--
.../Requests/ClientCredentialRequest.cs | 28 +++++-----
.../Microsoft.Identity.Client.csproj | 7 ---
.../PublicApi/net462/PublicAPI.Unshipped.txt | 14 ++---
.../PublicApi/net472/PublicAPI.Unshipped.txt | 14 ++---
.../net8.0-android/PublicAPI.Unshipped.txt | 14 ++++-
.../net8.0-ios/PublicAPI.Unshipped.txt | 12 +++++
.../PublicApi/net8.0/PublicAPI.Unshipped.txt | 14 ++---
.../netstandard2.0/PublicAPI.Unshipped.txt | 14 ++---
...lClientApplicationExtensibilityApiTests.cs | 32 +++++-------
...tialClientApplicationExtensibilityTests.cs | 52 +++++++++----------
15 files changed, 168 insertions(+), 170 deletions(-)
delete mode 100644 src/client/Microsoft.Identity.Client/Extensibility/ClientCredentialExtensionParameters.cs
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
index 8776b1a043..7371f02a7d 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
@@ -136,18 +136,18 @@ public string ClientVersion
///
/// Dynamic certificate provider callback for client credential flows.
///
- public Func> ClientCredentialCertificateProvider { get; set; }
+ public Func> ClientCredentialCertificateProvider { get; set; }
///
/// MSAL service failure callback that determines whether to retry after a token acquisition failure from the identity provider.
/// Only invoked for MsalServiceException (errors from the Security Token Service).
///
- public Func> OnMsalServiceFailureCallback { get; set; }
+ public Func> OnMsalServiceFailureCallback { get; set; }
///
/// Success callback that receives the result of token acquisition attempts (typically successful, but can include failures after retries are exhausted).
///
- public Func OnSuccessCallback { get; set; }
+ public Func OnSuccessCallback { get; set; }
#endregion
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs b/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs
index 245db3fada..41ee6bcf63 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs
@@ -6,15 +6,34 @@
namespace Microsoft.Identity.Client
{
- ///
- /// Information about the client assertion that need to be generated See https://aka.ms/msal-net-client-assertion
- ///
- /// Use the provided information to generate the client assertion payload
+ ///
+ /// Information about the client assertion that need to be generated See https://aka.ms/msal-net-client-assertion
+ ///
+ /// Use the provided information to generate the client assertion payload
#if !SUPPORTS_CONFIDENTIAL_CLIENT
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile
#endif
public class AssertionRequestOptions {
///
+ /// Default constructor for AssertionRequestOptions
+ ///
+ public AssertionRequestOptions()
+ {
+ }
+
+ ///
+ /// Internal constructor that creates AssertionRequestOptions from ApplicationConfiguration
+ ///
+ /// The application configuration
+ internal AssertionRequestOptions(ApplicationConfiguration appConfig)
+ {
+ ClientID = appConfig.ClientId;
+ TenantId = appConfig.Authority?.TenantId;
+ Authority = appConfig.Authority?.AuthorityInfo?.CanonicalAuthority?.ToString();
+ }
+
+ ///
+ /// Cancellation token to cancel the operation
///
public CancellationToken CancellationToken { get; set; }
@@ -23,6 +42,16 @@ public class AssertionRequestOptions {
///
public string ClientID { get; set; }
+ ///
+ /// Tenant ID for the authentication request
+ ///
+ public string TenantId { get; set; }
+
+ ///
+ /// The authority URL (e.g., https://login.microsoftonline.com/{tenantId})
+ ///
+ public string Authority { get; set; }
+
///
/// The intended token endpoint
///
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ClientCredentialExtensionParameters.cs b/src/client/Microsoft.Identity.Client/Extensibility/ClientCredentialExtensionParameters.cs
deleted file mode 100644
index 10ddd89cb0..0000000000
--- a/src/client/Microsoft.Identity.Client/Extensibility/ClientCredentialExtensionParameters.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-namespace Microsoft.Identity.Client.Extensibility
-{
- ///
- /// Provides application configuration context to client credential extensibility callbacks.
- /// Contains read-only information about the confidential client application.
- ///
-#if !SUPPORTS_CONFIDENTIAL_CLIENT
- [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile
-#endif
- public class ClientCredentialExtensionParameters
- {
- ///
- /// Internal constructor - only MSAL can create instances of this class.
- ///
- /// The application configuration.
- internal ClientCredentialExtensionParameters(ApplicationConfiguration config)
- {
- ClientId = config.ClientId;
- TenantId = config.TenantId;
- Authority = config.Authority?.AuthorityInfo?.CanonicalAuthority?.ToString();
- }
-
- ///
- /// The application (client) ID as registered in the Azure portal or application registration portal.
- ///
- public string ClientId { get; }
-
- ///
- /// The tenant ID if the application is configured for a specific tenant.
- /// Will be null for multi-tenant applications.
- ///
- public string TenantId { get; }
-
- ///
- /// The authority URL used for authentication (e.g., https://login.microsoftonline.com/common).
- ///
- public string Authority { get; }
- }
-}
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
index 1b9320b1b0..8b7e1f90ec 100644
--- a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
@@ -45,28 +45,31 @@ public static ConfidentialClientApplicationBuilder WithAppTokenProvider(
///
/// The builder to chain additional configuration calls.
/// Thrown when is null.
+ ///
+ /// Thrown at build time if both
+ /// and this method are configured.
+ ///
///
+ /// This method cannot be used together with .
/// The callback is not invoked when tokens are retrieved from cache, only for network calls.
/// The certificate returned by the callback will be used to sign the client assertion (JWT) for that token request.
/// The callback can perform async operations such as fetching certificates from Azure Key Vault or other secret management systems.
- /// This callback is used together with and
/// See https://aka.ms/msal-net-client-credentials for more details on client credentials.
///
public static ConfidentialClientApplicationBuilder WithCertificate(
this ConfidentialClientApplicationBuilder builder,
- Func> certificateProvider)
+ Func> certificateProvider)
{
- builder.ValidateUseOfExperimentalFeature();
if (certificateProvider == null)
{
throw new ArgumentNullException(nameof(certificateProvider));
}
-
+
builder.Config.ClientCredentialCertificateProvider = certificateProvider;
// Create a CertificateAndClaimsClientCredential with null certificate
// The certificate will be resolved dynamically via the provider in ResolveCertificateAsync
- builder.Config.ClientCredential = new CertificateAndClaimsClientCredential(
+ builder.Config.ClientCredential = new Microsoft.Identity.Client.Internal.ClientCredential.CertificateAndClaimsClientCredential(
certificate: null,
claimsToSign: null,
appendDefaultClaims: true);
@@ -75,21 +78,21 @@ public static ConfidentialClientApplicationBuilder WithCertificate(
}
///
- /// Configures an async callback that is invoked when MSAL receives an error response from the identity provider.
+ /// Configures an async callback that is invoked when MSAL receives an error response from the identity provider (Security Token Service).
/// The callback determines whether MSAL should retry the token request or propagate the exception.
/// This callback is invoked after each service failure and can be called multiple times until it returns false or the request succeeds.
///
/// The confidential client application builder.
///
/// An async callback that determines whether to retry after a service failure.
- /// Receives the application configuration parameters and the that occurred.
+ /// Receives the assertion request options and the that occurred.
/// Returns true to retry the request, or false to stop retrying and propagate the exception.
/// The callback will be invoked repeatedly after each service failure until it returns false or the request succeeds.
///
/// The builder to chain additional configuration calls.
/// Thrown when is null.
///
- /// This callback is ONLY triggered for - errors returned by STS.
+ /// This callback is ONLY triggered for - errors returned by the identity provider (e.g., HTTP 500, 503, throttling).
/// This callback is NOT triggered for client-side errors () or network failures handled internally by MSAL.
/// This callback is only invoked for network token acquisition attempts, not when tokens are retrieved from cache.
/// When the callback returns true, MSAL will invoke the certificate provider (if configured via )
@@ -97,35 +100,30 @@ public static ConfidentialClientApplicationBuilder WithCertificate(
/// MSAL's internal throttling and retry mechanisms will still apply, including respecting Retry-After headers from the identity provider.
/// To prevent infinite loops, ensure your callback has appropriate termination conditions (e.g., max retry count, timeout).
/// The callback can perform async operations such as logging to remote services, checking external health endpoints, or querying configuration stores.
- /// This callback is used together with callback.
///
///
///
/// int retryCount = 0;
/// var app = ConfidentialClientApplicationBuilder
/// .Create(clientId)
- /// .WithCertificate(async parameters => await GetCertificateFromKeyVaultAsync(parameters.TenantId))
- /// .OnMsalServiceFailure(async (parameters, serviceException) =>
+ /// .WithCertificate(async options => await GetCertificateFromKeyVaultAsync(options.TokenEndpoint))
+ /// .OnMsalServiceFailure(async (options, serviceException) =>
/// {
/// retryCount++;
/// await LogExceptionAsync(serviceException);
///
- /// // Retry up to 3 times for errors received from STS
- /// return serviceException.ErrorCode == "SpecificErrorCodeToRetry"; retryCount < 3;
+ /// // Retry up to 3 times for transient service errors (5xx)
+ /// return serviceException.StatusCode >= 500 && retryCount < 3;
/// })
/// .Build();
///
///
public static ConfidentialClientApplicationBuilder OnMsalServiceFailure(
this ConfidentialClientApplicationBuilder builder,
- Func> onMsalServiceFailureCallback)
+ Func> onMsalServiceFailureCallback)
{
- builder.ValidateUseOfExperimentalFeature();
-
if (onMsalServiceFailureCallback == null)
- {
throw new ArgumentNullException(nameof(onMsalServiceFailureCallback));
- }
builder.Config.OnMsalServiceFailureCallback = onMsalServiceFailureCallback;
return builder;
@@ -139,7 +137,7 @@ public static ConfidentialClientApplicationBuilder OnMsalServiceFailure(
///
/// The confidential client application builder.
///
- /// An async callback that receives the application configuration parameters and the execution result.
+ /// An async callback that receives the assertion request options and the execution result.
/// The result contains either the successful or the that occurred.
/// This callback is invoked after all retries have been exhausted (if an handler is configured).
///
@@ -158,11 +156,11 @@ public static ConfidentialClientApplicationBuilder OnMsalServiceFailure(
/// var app = ConfidentialClientApplicationBuilder
/// .Create(clientId)
/// .WithCertificate(certificate)
- /// .OnSuccess(async (parameters, result) =>
+ /// .OnSuccess(async (options, result) =>
/// {
/// if (result.Successful)
/// {
- /// await telemetry.TrackEventAsync("TokenAcquired", new { ClientId = parameters.ClientId });
+ /// await telemetry.TrackEventAsync("TokenAcquired", new { ClientId = options.ClientID });
/// }
/// else
/// {
@@ -174,7 +172,7 @@ public static ConfidentialClientApplicationBuilder OnMsalServiceFailure(
///
public static ConfidentialClientApplicationBuilder OnSuccess(
this ConfidentialClientApplicationBuilder builder,
- Func onSuccessCallback)
+ Func onSuccessCallback)
{
builder.ValidateUseOfExperimentalFeature();
diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
index ef2b608f0d..c2446b2a33 100644
--- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
@@ -99,13 +99,15 @@ private async Task ResolveCertificateAsync(
requestParameters.RequestContext.Logger.Verbose(
() => "[CertificateAndClaimsClientCredential] Resolving certificate from dynamic provider.");
- // Create parameters for the callback
- var parameters = new Extensibility.ClientCredentialExtensionParameters(
- requestParameters.AppConfig);
+ // Create AssertionRequestOptions for the callback
+ var options = new AssertionRequestOptions((ApplicationConfiguration)requestParameters.AppConfig)
+ {
+ CancellationToken = cancellationToken
+ };
// Invoke the provider to get the certificate
X509Certificate2 providedCertificate = await requestParameters.AppConfig
- .ClientCredentialCertificateProvider(parameters)
+ .ClientCredentialCertificateProvider(options)
.ConfigureAwait(false);
// Validate the certificate returned by the provider
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
index 469f37759a..c583743f3a 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
@@ -196,11 +196,10 @@ private async Task InvokeOnMsalServiceFailureCallbackAsync(
{
try
{
- var parameters = new ClientCredentialExtensionParameters(
- (ApplicationConfiguration)AuthenticationRequestParameters.AppConfig);
+ var options = new AssertionRequestOptions(AuthenticationRequestParameters.AppConfig);
bool shouldRetry = await AuthenticationRequestParameters.AppConfig
- .OnMsalServiceFailureCallback(parameters, serviceException)
+ .OnMsalServiceFailureCallback(options, serviceException)
.ConfigureAwait(false);
logger.Verbose(() => $"[ClientCredentialRequest] OnMsalServiceFailureCallback returned: {shouldRetry}");
@@ -233,8 +232,7 @@ private async Task InvokeOnSuccessCallbackAsync(
{
logger.Verbose(() => "[ClientCredentialRequest] Invoking OnSuccess callback.");
- var parameters = new ClientCredentialExtensionParameters(
- (ApplicationConfiguration)AuthenticationRequestParameters.AppConfig);
+ var options = new AssertionRequestOptions(AuthenticationRequestParameters.AppConfig);
var executionResult = new ExecutionResult
{
@@ -244,7 +242,7 @@ private async Task InvokeOnSuccessCallbackAsync(
};
await AuthenticationRequestParameters.AppConfig
- .OnSuccessCallback(parameters, executionResult)
+ .OnSuccessCallback(options, executionResult)
.ConfigureAwait(false);
logger.Verbose(() => "[ClientCredentialRequest] OnSuccess callback completed successfully.");
@@ -432,15 +430,15 @@ private void MarkAccessTokenAsCacheHit()
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);
+ cachedAccessTokenItem,
+ null,
+ AuthenticationRequestParameters.AuthenticationScheme,
+ AuthenticationRequestParameters.RequestContext.CorrelationId,
+ TokenSource.Cache,
+ AuthenticationRequestParameters.RequestContext.ApiEvent,
+ account: null,
+ spaAuthCode: null,
+ additionalResponseParameters: null);
return authResult;
}
diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
index 227e7a2e57..7bbdb0ba3e 100644
--- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
+++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj
@@ -80,8 +80,6 @@
-
-
@@ -164,9 +162,4 @@
-
-
-
-
-
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
index c9bafe148a..bc9817ce62 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
@@ -1,14 +1,14 @@
Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.Authority.get -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.ClientId.get -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.TenantId.get -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
@@ -19,6 +19,6 @@ Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsy
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
index c9bafe148a..bc9817ce62 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
@@ -1,14 +1,14 @@
Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.Authority.get -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.ClientId.get -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.TenantId.get -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
@@ -19,6 +19,6 @@ Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsy
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
index a77a3542bb..bc9817ce62 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
@@ -1,12 +1,24 @@
Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
+const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
+Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
-static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
\ No newline at end of file
+static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
index bb1898ebb1..bc9817ce62 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
@@ -1,12 +1,24 @@
Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
+const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
+Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
index c9bafe148a..bc9817ce62 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
@@ -1,14 +1,14 @@
Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.Authority.get -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.ClientId.get -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.TenantId.get -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
@@ -19,6 +19,6 @@ Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsy
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
index c9bafe148a..bc9817ce62 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
@@ -1,14 +1,14 @@
Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.Authority.get -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.ClientId.get -> string
-Microsoft.Identity.Client.Extensibility.ClientCredentialExtensionParameters.TenantId.get -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
@@ -19,6 +19,6 @@ Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsy
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
index 9bf1fa1379..a8ff9440d9 100644
--- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
@@ -3,13 +3,10 @@
using System;
using System.Security.Cryptography.X509Certificates;
-using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensibility;
-using Microsoft.Identity.Client.Internal.ClientCredential;
using Microsoft.Identity.Test.Common;
-using Microsoft.Identity.Test.Common.Core.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.Identity.Test.Unit.AppConfigTests
@@ -39,7 +36,7 @@ public void WithCertificate_CallbackIsStored()
{
// Arrange
bool callbackInvoked = false;
- Task certificateProvider(ClientCredentialExtensionParameters parameters)
+ Task certificateProvider(AssertionRequestOptions options)
{
callbackInvoked = true;
return Task.FromResult(GetTestCertificate());
@@ -65,7 +62,7 @@ public void WithCertificate_ThrowsOnNullCallback()
ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
- .WithCertificate((Func>)null)
+ .WithCertificate((Func>)null)
.Build());
Assert.AreEqual("certificateProvider", ex.ParamName);
@@ -78,13 +75,13 @@ public void WithCertificate_AllowsMultipleCallbackRegistrations_LastOneWins()
int firstCallbackInvoked = 0;
int secondCallbackInvoked = 0;
- Task firstProvider(ClientCredentialExtensionParameters parameters)
+ Task firstProvider(AssertionRequestOptions options)
{
firstCallbackInvoked++;
return Task.FromResult(GetTestCertificate());
}
- Task secondProvider(ClientCredentialExtensionParameters parameters)
+ Task secondProvider(AssertionRequestOptions options)
{
secondCallbackInvoked++;
return Task.FromResult(GetTestCertificate());
@@ -113,7 +110,7 @@ Task secondProvider(ClientCredentialExtensionParameters parame
public void OnMsalServiceFailure_CallbackIsStored()
{
// Arrange
- Task onMsalServiceFailureCallback(ClientCredentialExtensionParameters parameters, MsalException ex) => Task.FromResult(false);
+ Task onMsalServiceFailureCallback(AssertionRequestOptions options, MsalException ex) => Task.FromResult(false);
// Act
var app = ConfidentialClientApplicationBuilder
@@ -150,7 +147,7 @@ public void OnMsalServiceFailure_ThrowsOnNullCallback()
public void OnSuccess_CallbackIsStored()
{
// Arrange
- Task onSuccessCallback(ClientCredentialExtensionParameters parameters, ExecutionResult result) => Task.CompletedTask;
+ Task onSuccessCallback(AssertionRequestOptions options, ExecutionResult result) => Task.CompletedTask;
// Act
var app = ConfidentialClientApplicationBuilder
@@ -251,9 +248,9 @@ public void ExecutionResult_PropertiesCanBeSet()
public void AllThreeExtensibilityPoints_CanBeConfiguredTogether()
{
// Arrange
- Task certificateProvider(ClientCredentialExtensionParameters parameters) => Task.FromResult(GetTestCertificate());
- Task onMsalServiceFailure(ClientCredentialExtensionParameters parameters, MsalException ex) => Task.FromResult(false);
- Task onSuccess(ClientCredentialExtensionParameters parameters, ExecutionResult result) => Task.CompletedTask;
+ Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate());
+ Task onMsalServiceFailure(AssertionRequestOptions options, MsalException ex) => Task.FromResult(false);
+ Task onSuccess(AssertionRequestOptions options, ExecutionResult result) => Task.CompletedTask;
// Act
var app = ConfidentialClientApplicationBuilder
@@ -275,9 +272,9 @@ public void AllThreeExtensibilityPoints_CanBeConfiguredTogether()
public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
{
// Arrange
- Task certificateProvider(ClientCredentialExtensionParameters parameters) => Task.FromResult(GetTestCertificate());
- Task onMsalServiceFailure(ClientCredentialExtensionParameters parameters, MsalException ex) => Task.FromResult(false);
- Task onSuccess(ClientCredentialExtensionParameters parameters, ExecutionResult result) => Task.CompletedTask;
+ Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate());
+ Task onMsalServiceFailure(AssertionRequestOptions options, MsalException ex) => Task.FromResult(false);
+ Task onSuccess(AssertionRequestOptions options, ExecutionResult result) => Task.CompletedTask;
// Act - Order: OnSuccess, OnMsalServiceFailure, Certificate
var app1 = ConfidentialClientApplicationBuilder
@@ -315,10 +312,9 @@ public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
public void WithCertificate_WorksWithOtherConfidentialClientOptions()
{
// Arrange
- Task certificateProvider(ClientCredentialExtensionParameters parameters)
+ Task certificateProvider(AssertionRequestOptions options)
{
- Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
- Assert.AreEqual(TestConstants.AadTenantId, parameters.TenantId);
+ Assert.AreEqual(TestConstants.ClientId, options.ClientID);
return Task.FromResult(GetTestCertificate());
}
diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
index 274f1b4191..d9727fd777 100644
--- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
@@ -34,7 +34,7 @@ public async Task DynamicCertificateProvider_IsInvoked_AndUsedForAssertionAsync(
harness.HttpManager.AddInstanceDiscoveryMockHandler();
bool providerInvoked = false;
- ClientCredentialExtensionParameters capturedParameters = null;
+ AssertionRequestOptions capturedOptions = null;
var certificate = CertHelper.GetOrCreateTestCert();
@@ -43,14 +43,14 @@ public async Task DynamicCertificateProvider_IsInvoked_AndUsedForAssertionAsync(
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
- .WithCertificate((ClientCredentialExtensionParameters parameters) =>
+ .WithCertificate((AssertionRequestOptions options) =>
{
providerInvoked = true;
- capturedParameters = parameters;
+ capturedOptions = options;
- // Validate parameters
- Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
- Assert.IsNotNull(parameters.Authority);
+ // Validate options
+ Assert.AreEqual(TestConstants.ClientId, options.ClientID);
+ Assert.IsNotNull(options.TokenEndpoint);
return Task.FromResult(certificate);
})
@@ -65,7 +65,7 @@ public async Task DynamicCertificateProvider_IsInvoked_AndUsedForAssertionAsync(
// Assert
Assert.IsTrue(providerInvoked, "Certificate provider should have been invoked");
- Assert.IsNotNull(capturedParameters);
+ Assert.IsNotNull(capturedOptions);
Assert.IsNotNull(result.AccessToken);
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
}
@@ -85,7 +85,7 @@ public async Task DynamicCertificateProvider_ReturnsNull_ThrowsExceptionAsync()
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
- .WithCertificate((ClientCredentialExtensionParameters parameters) =>
+ .WithCertificate((AssertionRequestOptions options) =>
{
return Task.FromResult(null); // Provider returns null
})
@@ -126,13 +126,13 @@ public async Task OnMsalServiceFailure_RetriesOnServiceError_SucceedsAsync()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((AssertionRequestOptions options, MsalException ex) =>
{
failureCallbackCount++;
capturedException = ex as MsalServiceException;
Assert.IsNotNull(capturedException, "Exception should be MsalServiceException");
- Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
+ Assert.AreEqual(TestConstants.ClientId, options.ClientID);
// Retry on 503
return Task.FromResult(capturedException.StatusCode == 400 && failureCallbackCount < 3);
@@ -173,7 +173,7 @@ public async Task OnMsalServiceFailure_ReturnsFalse_PropagatesExceptionAsync()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((AssertionRequestOptions options, MsalException ex) =>
{
callbackInvoked = true;
return Task.FromResult(false); // Don't retry
@@ -209,11 +209,11 @@ public async Task OnMsalServiceFailure_NotInvokedForClientExceptionsAsync()
.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
- .WithCertificate((ClientCredentialExtensionParameters parameters) =>
+ .WithCertificate((AssertionRequestOptions options) =>
{
return Task.FromResult(null); // Will cause MsalClientException
})
- .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((AssertionRequestOptions options, MsalException ex) =>
{
callbackInvoked = true;
return Task.FromResult(false);
@@ -248,7 +248,7 @@ public async Task OnSuccess_InvokedWithSuccessfulResultAsync()
bool observerInvoked = false;
ExecutionResult capturedResult = null;
- ClientCredentialExtensionParameters capturedParameters = null;
+ AssertionRequestOptions capturedOptions = null;
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
@@ -256,16 +256,16 @@ public async Task OnSuccess_InvokedWithSuccessfulResultAsync()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnSuccess((ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ .OnSuccess((AssertionRequestOptions options, ExecutionResult result) =>
{
observerInvoked = true;
capturedResult = result;
- capturedParameters = parameters;
+ capturedOptions = options;
Assert.IsTrue(result.Successful);
Assert.IsNotNull(result.Result);
Assert.IsNull(result.Exception);
- Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
+ Assert.AreEqual(TestConstants.ClientId, options.ClientID);
return Task.CompletedTask;
})
@@ -306,12 +306,12 @@ public async Task OnSuccess_InvokedWithFailureResult_AfterRetriesExhaustedAsync(
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((AssertionRequestOptions options, MsalException ex) =>
{
retryCount++;
return Task.FromResult(retryCount < 2); // Retry once, then give up
})
- .OnSuccess((ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ .OnSuccess((AssertionRequestOptions options, ExecutionResult result) =>
{
observerInvoked = true;
capturedResult = result;
@@ -359,7 +359,7 @@ public async Task OnSuccess_ExceptionIsCaught_DoesNotDisruptFlowAsync()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnSuccess((ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ .OnSuccess((AssertionRequestOptions options, ExecutionResult result) =>
{
throw new InvalidOperationException("Observer threw exception");
})
@@ -402,19 +402,19 @@ public async Task AllThreeExtensibilityPoints_WorkTogetherAsync()
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
- .WithCertificate((ClientCredentialExtensionParameters parameters) =>
+ .WithCertificate((AssertionRequestOptions options) =>
{
certProviderCount++;
- Assert.AreEqual(TestConstants.ClientId, parameters.ClientId);
+ Assert.AreEqual(TestConstants.ClientId, options.ClientID);
return Task.FromResult(certificate);
})
- .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((AssertionRequestOptions options, MsalException ex) =>
{
retryCallbackCount++;
Assert.IsInstanceOfType(ex, typeof(MsalServiceException));
return Task.FromResult(retryCallbackCount < 2); // Retry once
})
- .OnSuccess((ClientCredentialExtensionParameters parameters, ExecutionResult result) =>
+ .OnSuccess((AssertionRequestOptions options, ExecutionResult result) =>
{
observerInvoked = true;
Assert.IsTrue(result.Successful);
@@ -458,13 +458,13 @@ public async Task CertificateRotation_DifferentCertOnRetryAsync()
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
- .WithCertificate((ClientCredentialExtensionParameters parameters) =>
+ .WithCertificate((AssertionRequestOptions options) =>
{
certProviderCount++;
// Return different cert on retry
return Task.FromResult(certProviderCount == 1 ? cert1 : cert2);
})
- .OnMsalServiceFailure((ClientCredentialExtensionParameters parameters, MsalException ex) =>
+ .OnMsalServiceFailure((AssertionRequestOptions options, MsalException ex) =>
{
return Task.FromResult(true); // Always retry once
})
From 20ce7bc9dc9786ea4dd9e74985ce91cea54d0cde Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Mon, 24 Nov 2025 13:28:52 -0800
Subject: [PATCH 09/12] Resolve conflicts
---
.../PublicApi/net462/PublicAPI.Unshipped.txt | 12 ------------
.../PublicApi/net472/PublicAPI.Unshipped.txt | 18 +++---------------
.../net8.0-android/PublicAPI.Unshipped.txt | 2 +-
.../net8.0-ios/PublicAPI.Unshipped.txt | 2 +-
.../PublicApi/net8.0/PublicAPI.Unshipped.txt | 18 +++---------------
.../netstandard2.0/PublicAPI.Unshipped.txt | 18 +++---------------
6 files changed, 11 insertions(+), 59 deletions(-)
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
index 2d191c4617..9fc309c640 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
@@ -1,24 +1,12 @@
-Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
-Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
-const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
-const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
-const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
-const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
-Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient
-Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task
-Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
-Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
-static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
index bc9817ce62..9fc309c640 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
@@ -1,24 +1,12 @@
-Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
-Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
-const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
-const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
-const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
-const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
-Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient
-Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task
-Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
-Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
-static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
index 5f282702bb..3e06c566f7 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
@@ -1 +1 @@
-
\ No newline at end of file
+const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
index 5f282702bb..3e06c566f7 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
@@ -1 +1 @@
-
\ No newline at end of file
+const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
index bc9817ce62..9fc309c640 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
@@ -1,24 +1,12 @@
-Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
-Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
-const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
-const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
-const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
-const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
-Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient
-Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task
-Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
-Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
-static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
index bc9817ce62..9fc309c640 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
@@ -1,24 +1,12 @@
-Microsoft.Identity.Client.AbstractApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
-Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> T
-const Microsoft.Identity.Client.MsalError.CannotSwitchBetweenImdsVersionsForPreview = "cannot_switch_between_imds_versions_for_preview" -> string
-const Microsoft.Identity.Client.MsalError.InvalidCertificate = "invalid_certificate" -> string
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
-const Microsoft.Identity.Client.MsalError.MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity" -> string
-const Microsoft.Identity.Client.MsalError.MtlsPopTokenNotSupportedinImdsV1 = "mtls_pop_token_not_supported_in_imds_v1" -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-Microsoft.Identity.Client.IMsalMtlsHttpClientFactory
-Microsoft.Identity.Client.IMsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2 x509Certificate2) -> System.Net.Http.HttpClient
-Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync() -> System.Threading.Tasks.Task
-Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource.ImdsV2 = 8 -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
-Microsoft.Identity.Client.ManagedIdentityApplicationBuilder.WithExtraQueryParameters(System.Collections.Generic.IDictionary extraQueryParameters) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
-static Microsoft.Identity.Client.ApplicationBase.ResetStateForTest() -> void
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
From 589a614e724a0d6b850b6eaea88809cde9f42f8d Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Mon, 24 Nov 2025 13:59:44 -0800
Subject: [PATCH 10/12] Fix build issue
---
.../PublicApi/net462/PublicAPI.Unshipped.txt | 6 +++---
.../PublicApi/net472/PublicAPI.Unshipped.txt | 6 +++---
.../PublicApi/net8.0/PublicAPI.Unshipped.txt | 6 +++---
.../PublicApi/netstandard2.0/PublicAPI.Unshipped.txt | 6 +++---
4 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
index 9fc309c640..0028dea8d0 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
@@ -7,6 +7,6 @@ Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
index 9fc309c640..0028dea8d0 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
@@ -7,6 +7,6 @@ Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
index 9fc309c640..0028dea8d0 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
@@ -7,6 +7,6 @@ Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
index 9fc309c640..0028dea8d0 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
@@ -7,6 +7,6 @@ Microsoft.Identity.Client.Extensibility.ExecutionResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
From 6d303a99a6dc4128122c62c53d60f1e67b264c75 Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Tue, 25 Nov 2025 09:38:46 -0800
Subject: [PATCH 11/12] Public API analyzers
---
.../net8.0-android/PublicAPI.Unshipped.txt | 13 ++++++++++++-
.../PublicApi/net8.0-ios/PublicAPI.Unshipped.txt | 13 ++++++++++++-
2 files changed, 24 insertions(+), 2 deletions(-)
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
index 3e06c566f7..487820e412 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt
@@ -1 +1,12 @@
-const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
+const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
+Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
index 3e06c566f7..487820e412 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt
@@ -1 +1,12 @@
-const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.Authority.set -> void
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
+Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
+const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
+Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
From d0129be2717fd7963dbe1b56df2de2b0a3b3d050 Mon Sep 17 00:00:00 2001
From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
Date: Mon, 8 Dec 2025 18:34:29 -0800
Subject: [PATCH 12/12] Address comments
---
...ntialClientAcquireTokenParameterBuilder.cs | 3 +-
.../AppConfig/ApplicationConfiguration.cs | 15 +--
.../AppConfig/AssertionRequestOptions.cs | 7 +-
.../ConfidentialClientApplicationBuilder.cs | 6 +-
...ntialClientApplicationBuilderExtensions.cs | 42 +++---
.../Extensibility/ExecutionResult.cs | 21 ++-
.../CertificateAndClaimsClientCredential.cs | 123 +++++++++---------
.../CertificateClientCredential.cs | 13 +-
.../DynamicCertificateClientCredential.cs | 25 ++++
.../AuthenticationRequestParameters.cs | 6 +
.../Requests/ClientCredentialRequest.cs | 53 +++++---
.../PublicApi/net462/PublicAPI.Unshipped.txt | 5 +-
.../PublicApi/net472/PublicAPI.Unshipped.txt | 5 +-
.../PublicApi/net8.0/PublicAPI.Unshipped.txt | 5 +-
.../netstandard2.0/PublicAPI.Unshipped.txt | 5 +-
...lClientApplicationExtensibilityApiTests.cs | 48 +++----
...tialClientApplicationExtensibilityTests.cs | 41 ++++--
17 files changed, 261 insertions(+), 162 deletions(-)
create mode 100644 src/client/Microsoft.Identity.Client/Internal/ClientCredential/DynamicCertificateClientCredential.cs
diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs
index fcb67f34c6..a96c9fbe68 100644
--- a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs
+++ b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs
@@ -48,8 +48,7 @@ protected override void Validate()
// Confidential client must have a credential
if (ServiceBundle?.Config.ClientCredential == null &&
CommonParameters.OnBeforeTokenRequestHandler == null &&
- ServiceBundle?.Config.AppTokenProvider == null &&
- ServiceBundle?.Config.ClientCredentialCertificateProvider == null
+ ServiceBundle?.Config.AppTokenProvider == null
)
{
throw new MsalClientException(
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
index 7371f02a7d..27fc024de8 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs
@@ -133,21 +133,16 @@ public string ClientVersion
#region Extensibility Callbacks
- ///
- /// Dynamic certificate provider callback for client credential flows.
- ///
- public Func> ClientCredentialCertificateProvider { get; set; }
-
///
/// MSAL service failure callback that determines whether to retry after a token acquisition failure from the identity provider.
/// Only invoked for MsalServiceException (errors from the Security Token Service).
///
- public Func> OnMsalServiceFailureCallback { get; set; }
+ public Func> OnMsalServiceFailure { get; set; }
///
/// Success callback that receives the result of token acquisition attempts (typically successful, but can include failures after retries are exhausted).
///
- public Func OnSuccessCallback { get; set; }
+ public Func OnCompletion { get; set; }
#endregion
@@ -174,14 +169,16 @@ public string ClientSecret
///
/// This is here just to support the public IAppConfig. Should not be used internally, instead use the abstraction.
+ /// Note: This returns null when using dynamic certificate providers since the certificate is resolved at runtime.
///
public X509Certificate2 ClientCredentialCertificate
{
get
{
- if (ClientCredential is CertificateAndClaimsClientCredential cred)
+ // Return the certificate if using static certificate (CertificateClientCredential)
+ if (ClientCredential is CertificateClientCredential certCred)
{
- return cred.Certificate;
+ return certCred.Certificate;
}
return null;
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs b/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs
index 41ee6bcf63..4cacac7cb2 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/AssertionRequestOptions.cs
@@ -25,11 +25,14 @@ public AssertionRequestOptions()
/// Internal constructor that creates AssertionRequestOptions from ApplicationConfiguration
///
/// The application configuration
- internal AssertionRequestOptions(ApplicationConfiguration appConfig)
+ /// The token endpoint used to acquire the token
+ /// The tenant ID from the runtime authority
+ internal AssertionRequestOptions(ApplicationConfiguration appConfig, string tokenEndpoint, string tenantId)
{
ClientID = appConfig.ClientId;
- TenantId = appConfig.Authority?.TenantId;
+ TokenEndpoint = tokenEndpoint;
Authority = appConfig.Authority?.AuthorityInfo?.CanonicalAuthority?.ToString();
+ TenantId = tenantId;
}
///
diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs
index 2f1463e56d..0f33ce83bc 100644
--- a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs
+++ b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs
@@ -169,7 +169,11 @@ public ConfidentialClientApplicationBuilder WithClientClaims(X509Certificate2 ce
throw new ArgumentNullException(nameof(claimsToSign));
}
- Config.ClientCredential = new CertificateAndClaimsClientCredential(certificate, claimsToSign, mergeWithDefaultClaims);
+ // Wrap the static certificate in a provider delegate
+ Config.ClientCredential = new CertificateAndClaimsClientCredential(
+ certificateProvider: _ => Task.FromResult(certificate),
+ claimsToSign: claimsToSign,
+ appendDefaultClaims: mergeWithDefaultClaims);
Config.SendX5C = sendX5C;
return this;
}
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
index 8b7e1f90ec..f41b176bd5 100644
--- a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs
@@ -64,15 +64,11 @@ public static ConfidentialClientApplicationBuilder WithCertificate(
{
throw new ArgumentNullException(nameof(certificateProvider));
}
-
- builder.Config.ClientCredentialCertificateProvider = certificateProvider;
- // Create a CertificateAndClaimsClientCredential with null certificate
+ // Create a DynamicCertificateClientCredential with the certificate provider
// The certificate will be resolved dynamically via the provider in ResolveCertificateAsync
- builder.Config.ClientCredential = new Microsoft.Identity.Client.Internal.ClientCredential.CertificateAndClaimsClientCredential(
- certificate: null,
- claimsToSign: null,
- appendDefaultClaims: true);
+ builder.Config.ClientCredential = new DynamicCertificateClientCredential(
+ certificateProvider: certificateProvider);
return builder;
}
@@ -83,14 +79,14 @@ public static ConfidentialClientApplicationBuilder WithCertificate(
/// This callback is invoked after each service failure and can be called multiple times until it returns false or the request succeeds.
///
/// The confidential client application builder.
- ///
+ ///
/// An async callback that determines whether to retry after a service failure.
/// Receives the assertion request options and the that occurred.
/// Returns true to retry the request, or false to stop retrying and propagate the exception.
/// The callback will be invoked repeatedly after each service failure until it returns false or the request succeeds.
///
/// The builder to chain additional configuration calls.
- /// Thrown when is null.
+ /// Thrown when is null.
///
/// This callback is ONLY triggered for - errors returned by the identity provider (e.g., HTTP 500, 503, throttling).
/// This callback is NOT triggered for client-side errors () or network failures handled internally by MSAL.
@@ -120,33 +116,33 @@ public static ConfidentialClientApplicationBuilder WithCertificate(
///
public static ConfidentialClientApplicationBuilder OnMsalServiceFailure(
this ConfidentialClientApplicationBuilder builder,
- Func> onMsalServiceFailureCallback)
+ Func> onMsalServiceFailure)
{
- if (onMsalServiceFailureCallback == null)
- throw new ArgumentNullException(nameof(onMsalServiceFailureCallback));
+ if (onMsalServiceFailure == null)
+ throw new ArgumentNullException(nameof(onMsalServiceFailure));
- builder.Config.OnMsalServiceFailureCallback = onMsalServiceFailureCallback;
+ builder.Config.OnMsalServiceFailure = onMsalServiceFailure;
return builder;
}
///
/// Configures an async callback that is invoked when a token acquisition request completes.
/// This callback is invoked once per AcquireTokenForClient call, after all retry attempts have been exhausted.
- /// While named OnSuccess for the common case, this callback fires for both successful and failed acquisitions.
+ /// While named OnCompletion for the common case, this callback fires for both successful and failed acquisitions.
/// This enables scenarios such as telemetry, logging, and custom result handling.
///
/// The confidential client application builder.
- ///
+ ///
/// An async callback that receives the assertion request options and the execution result.
/// The result contains either the successful or the that occurred.
/// This callback is invoked after all retries have been exhausted (if an handler is configured).
///
/// The builder to chain additional configuration calls.
- /// Thrown when is null.
+ /// Thrown when is null.
///
/// This callback is invoked for both successful and failed token acquisitions. Check to determine the outcome.
/// This callback is only invoked for network token acquisition attempts, not when tokens are retrieved from cache.
- /// If multiple calls to OnSuccess are made, only the last configured callback will be used.
+ /// If multiple calls to OnCompletion are made, only the last configured callback will be used.
/// Exceptions thrown by this callback will be caught and logged internally to prevent disruption of the authentication flow.
/// The callback is invoked on the same thread/context as the token acquisition request.
/// The callback can perform async operations such as sending telemetry to Application Insights, persisting logs to databases, or triggering webhooks.
@@ -156,7 +152,7 @@ public static ConfidentialClientApplicationBuilder OnMsalServiceFailure(
/// var app = ConfidentialClientApplicationBuilder
/// .Create(clientId)
/// .WithCertificate(certificate)
- /// .OnSuccess(async (options, result) =>
+ /// .OnCompletion(async (options, result) =>
/// {
/// if (result.Successful)
/// {
@@ -170,18 +166,18 @@ public static ConfidentialClientApplicationBuilder OnMsalServiceFailure(
/// .Build();
///
///
- public static ConfidentialClientApplicationBuilder OnSuccess(
+ public static ConfidentialClientApplicationBuilder OnCompletion(
this ConfidentialClientApplicationBuilder builder,
- Func onSuccessCallback)
+ Func onCompletion)
{
builder.ValidateUseOfExperimentalFeature();
- if (onSuccessCallback == null)
+ if (onCompletion == null)
{
- throw new ArgumentNullException(nameof(onSuccessCallback));
+ throw new ArgumentNullException(nameof(onCompletion));
}
- builder.Config.OnSuccessCallback = onSuccessCallback;
+ builder.Config.OnCompletion = onCompletion;
return builder;
}
}
diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs b/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs
index f6660206b2..ea81384d38 100644
--- a/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs
+++ b/src/client/Microsoft.Identity.Client/Extensibility/ExecutionResult.cs
@@ -1,11 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using System.Security.Cryptography.X509Certificates;
+
namespace Microsoft.Identity.Client.Extensibility
{
+#if !SUPPORTS_CONFIDENTIAL_CLIENT
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile
+#endif
///
/// Represents the result of a token acquisition attempt.
- /// Used by the execution observer configured via .
+ /// Used by the execution observer configured via .
///
public class ExecutionResult
{
@@ -39,5 +44,19 @@ internal ExecutionResult() { }
/// otherwise, null.
///
public MsalException Exception { get; internal set; }
+
+ ///
+ /// The certificate used for authentication, if certificate-based authentication was used.
+ ///
+ ///
+ /// An used to authenticate the client application;
+ /// otherwise, null if certificate authentication was not used or if the certificate is not available.
+ ///
+ ///
+ /// This property provides access to the certificate for logging and auditing purposes.
+ /// The certificate may be disposed after the token acquisition completes, so accessing its properties
+ /// may throw exceptions if the certificate has been disposed.
+ ///
+ public X509Certificate2 Certificate { get; internal set; }
}
}
diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
index c2446b2a33..a8c69823fc 100644
--- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
@@ -10,7 +11,6 @@
using Microsoft.Identity.Client.OAuth2;
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
using Microsoft.Identity.Client.TelemetryCore;
-using Microsoft.Identity.Client.Utils;
namespace Microsoft.Identity.Client.Internal.ClientCredential
{
@@ -18,26 +18,25 @@ internal class CertificateAndClaimsClientCredential : IClientCredential
{
private readonly IDictionary _claimsToSign;
private readonly bool _appendDefaultClaims;
- private readonly string _base64EncodedThumbprint; // x5t
-
- public X509Certificate2 Certificate { get; }
+ private readonly Func> _certificateProvider;
public AssertionType AssertionType => AssertionType.CertificateWithoutSni;
+ ///
+ /// Constructor that accepts a certificate provider delegate.
+ /// This allows both static certificates (via a simple delegate) and dynamic certificate resolution.
+ ///
+ /// Async delegate that provides the certificate
+ /// Additional claims to include in the client assertion
+ /// Whether to append default claims
public CertificateAndClaimsClientCredential(
- X509Certificate2 certificate,
+ Func> certificateProvider,
IDictionary claimsToSign,
bool appendDefaultClaims)
{
- Certificate = certificate;
+ _certificateProvider = certificateProvider;
_claimsToSign = claimsToSign;
_appendDefaultClaims = appendDefaultClaims;
-
- // Certificate can be null when using dynamic certificate provider
- if (certificate != null)
- {
- _base64EncodedThumbprint = Base64UrlHelpers.Encode(certificate.GetCertHash());
- }
}
public async Task AddConfidentialClientParametersAsync(
@@ -56,8 +55,11 @@ public async Task AddConfidentialClientParametersAsync(
{
requestParameters.RequestContext.Logger.Verbose(() => "Proceeding with JWT token creation and adding client assertion.");
- // Resolve the certificate - either from static config or dynamic provider
- X509Certificate2 effectiveCertificate = await ResolveCertificateAsync(requestParameters, cancellationToken).ConfigureAwait(false);
+ // Resolve the certificate via the provider
+ X509Certificate2 certificate = await ResolveCertificateAsync(requestParameters, tokenEndpoint, cancellationToken).ConfigureAwait(false);
+
+ // Store the resolved certificate in request parameters for later use (e.g., ExecutionResult)
+ requestParameters.ResolvedCertificate = certificate;
bool useSha2 = requestParameters.AuthorityManager.Authority.AuthorityInfo.IsSha2CredentialSupported;
@@ -68,7 +70,7 @@ public async Task AddConfidentialClientParametersAsync(
_claimsToSign,
_appendDefaultClaims);
- string assertion = jwtToken.Sign(effectiveCertificate, requestParameters.SendX5C, useSha2);
+ string assertion = jwtToken.Sign(certificate, requestParameters.SendX5C, useSha2);
oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer);
oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, assertion);
@@ -77,83 +79,82 @@ public async Task AddConfidentialClientParametersAsync(
{
// Log that MTLS PoP is required and JWT token creation is skipped
requestParameters.RequestContext.Logger.Verbose(() => "MTLS PoP Client credential request. Skipping client assertion.");
+
+ // Store the mTLS certificate in request parameters for later use (e.g., ExecutionResult)
+ requestParameters.ResolvedCertificate = requestParameters.MtlsCertificate;
}
}
///
/// Resolves the certificate to use for signing the client assertion.
- /// If a dynamic certificate provider is configured, it will be invoked to get the certificate.
- /// Otherwise, the static certificate configured at build time is used.
+ /// Invokes the certificate provider delegate to get the certificate.
///
/// The authentication request parameters containing app config
+ /// The token endpoint URL
/// Cancellation token for the async operation
/// The X509Certificate2 to use for signing
/// Thrown if the certificate provider returns null or an invalid certificate
private async Task ResolveCertificateAsync(
AuthenticationRequestParameters requestParameters,
+ string tokenEndpoint,
CancellationToken cancellationToken)
{
- // Check if dynamic certificate provider is configured
- if (requestParameters.AppConfig.ClientCredentialCertificateProvider != null)
- {
- requestParameters.RequestContext.Logger.Verbose(
- () => "[CertificateAndClaimsClientCredential] Resolving certificate from dynamic provider.");
+ requestParameters.RequestContext.Logger.Verbose(
+ () => "[CertificateAndClaimsClientCredential] Resolving certificate from provider.");
- // Create AssertionRequestOptions for the callback
- var options = new AssertionRequestOptions((ApplicationConfiguration)requestParameters.AppConfig)
- {
- CancellationToken = cancellationToken
- };
+ // Create AssertionRequestOptions for the callback
+ var options = new AssertionRequestOptions(
+ requestParameters.AppConfig,
+ tokenEndpoint,
+ requestParameters.AuthorityManager.Authority.TenantId)
+ {
+ Claims = requestParameters.Claims,
+ ClientCapabilities = requestParameters.AppConfig.ClientCapabilities,
+ CancellationToken = cancellationToken
+ };
- // Invoke the provider to get the certificate
- X509Certificate2 providedCertificate = await requestParameters.AppConfig
- .ClientCredentialCertificateProvider(options)
- .ConfigureAwait(false);
+ // Invoke the provider to get the certificate
+ X509Certificate2 certificate = await _certificateProvider(options).ConfigureAwait(false);
- // Validate the certificate returned by the provider
- if (providedCertificate == null)
- {
- requestParameters.RequestContext.Logger.Error(
- "[CertificateAndClaimsClientCredential] Certificate provider returned null.");
-
- throw new MsalClientException(
- MsalError.InvalidClientAssertion,
- "The certificate provider callback returned null. Ensure the callback returns a valid X509Certificate2 instance.");
- }
+ // Validate the certificate returned by the provider
+ if (certificate == null)
+ {
+ requestParameters.RequestContext.Logger.Error(
+ "[CertificateAndClaimsClientCredential] Certificate provider returned null.");
+
+ throw new MsalClientException(
+ MsalError.InvalidClientAssertion,
+ "The certificate provider callback returned null. Ensure the callback returns a valid X509Certificate2 instance.");
+ }
- if (!providedCertificate.HasPrivateKey)
+ try
+ {
+ if (!certificate.HasPrivateKey)
{
requestParameters.RequestContext.Logger.Error(
"[CertificateAndClaimsClientCredential] Certificate from provider does not have a private key.");
-
+
throw new MsalClientException(
MsalError.CertWithoutPrivateKey,
- "The certificate returned by the provider does not have a private key. " +
- "Ensure the certificate has a private key for signing operations.");
+ MsalErrorMessage.CertMustHavePrivateKey(certificate.FriendlyName));
}
-
- requestParameters.RequestContext.Logger.Info(
- () => $"[CertificateAndClaimsClientCredential] Successfully resolved certificate from provider. " +
- $"Thumbprint: {providedCertificate.Thumbprint}");
-
- return providedCertificate;
}
-
- // Use the static certificate configured at build time
- if (Certificate == null)
+ catch (System.Security.Cryptography.CryptographicException ex)
{
requestParameters.RequestContext.Logger.Error(
- "[CertificateAndClaimsClientCredential] No certificate configured (static or dynamic).");
-
+ "[CertificateAndClaimsClientCredential] A cryptographic error occurred while accessing the certificate.");
+
throw new MsalClientException(
- MsalError.InvalidClientAssertion,
- "No certificate is configured. Use WithCertificate() to provide a certificate.");
+ MsalError.CryptographicError,
+ MsalErrorMessage.CryptographicError,
+ ex);
}
- requestParameters.RequestContext.Logger.Verbose(
- () => $"[CertificateAndClaimsClientCredential] Using static certificate. Thumbprint: {Certificate.Thumbprint}");
+ requestParameters.RequestContext.Logger.Info(
+ () => $"[CertificateAndClaimsClientCredential] Successfully resolved certificate from provider. " +
+ $"Thumbprint: {certificate.Thumbprint}");
- return Certificate;
+ return certificate;
}
}
}
diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateClientCredential.cs
index d516a5480c..a58fa33418 100644
--- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateClientCredential.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateClientCredential.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
@@ -13,9 +14,17 @@ namespace Microsoft.Identity.Client.Internal.ClientCredential
{
internal class CertificateClientCredential : CertificateAndClaimsClientCredential
{
- public CertificateClientCredential(X509Certificate2 certificate) : base(certificate, null, true)
- {
+ ///
+ /// Gets the static certificate when using WithCertificate(X509Certificate2).
+ /// This is needed for mTLS scenarios where we need synchronous access to the certificate.
+ /// Returns null when using dynamic certificate providers.
+ ///
+ public X509Certificate2 Certificate { get; }
+ public CertificateClientCredential(X509Certificate2 certificate)
+ : base(certificateProvider: _ => Task.FromResult(certificate), claimsToSign: null, appendDefaultClaims: true)
+ {
+ Certificate = certificate;
}
}
}
diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/DynamicCertificateClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/DynamicCertificateClientCredential.cs
new file mode 100644
index 0000000000..32433081cd
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/DynamicCertificateClientCredential.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+
+namespace Microsoft.Identity.Client.Internal.ClientCredential
+{
+ ///
+ /// Client credential that resolves certificates dynamically at runtime via a provider delegate.
+ /// Used when certificates need to be rotated or selected based on runtime conditions.
+ ///
+ internal class DynamicCertificateClientCredential : CertificateAndClaimsClientCredential
+ {
+ public DynamicCertificateClientCredential(
+ Func> certificateProvider)
+ : base(
+ certificateProvider: certificateProvider,
+ claimsToSign: null,
+ appendDefaultClaims: true)
+ {
+ }
+ }
+}
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs
index 4e8e66f2ef..c13aaf7e0b 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs
@@ -115,6 +115,12 @@ public AuthenticationRequestParameters(
public bool IsMtlsPopRequested => _commonParameters.IsMtlsPopRequested;
+ ///
+ /// The certificate resolved and used for client authentication (if certificate-based authentication was used).
+ /// This is set during the token request when the certificate is resolved.
+ ///
+ public X509Certificate2 ResolvedCertificate { get; set; }
+
///
/// Indicates if the user configured claims via .WithClaims. Not affected by Client Capabilities
///
diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
index c583743f3a..88bd435764 100644
--- a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
+++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs
@@ -123,6 +123,7 @@ private async Task GetAccessTokenAsync(
await ResolveAuthorityAsync().ConfigureAwait(false);
AuthenticationResult authResult = null;
+ int retryCount = 0;
// Retry loop using the retry callback if configured
while (true)
@@ -147,31 +148,32 @@ private async Task GetAccessTokenAsync(
.ConfigureAwait(false);
}
- // Success - invoke OnSuccess callback if configured
+ // Success - invoke OnCompletion callback if configured
await InvokeOnSuccessCallbackAsync(authResult, exception: null, logger).ConfigureAwait(false);
return authResult;
}
catch (MsalServiceException serviceEx)
{
- // Check if OnMsalServiceFailureCallback is configured
- if (AuthenticationRequestParameters.AppConfig.OnMsalServiceFailureCallback != null)
+ // Check if OnMsalServiceFailure is configured
+ if (AuthenticationRequestParameters.AppConfig.OnMsalServiceFailure != null)
{
- logger.Info("[ClientCredentialRequest] MsalServiceException caught. Invoking OnMsalServiceFailureCallback.");
+ logger.Info("[ClientCredentialRequest] MsalServiceException caught. Invoking OnMsalServiceFailure.");
bool shouldRetry = await InvokeOnMsalServiceFailureCallbackAsync(serviceEx, logger)
.ConfigureAwait(false);
if (shouldRetry)
{
- logger.Info("[ClientCredentialRequest] OnMsalServiceFailureCallback returned true. Retrying token request.");
+ retryCount++;
+ logger.Info($"[ClientCredentialRequest] OnMsalServiceFailure returned true. Retrying token request (Retry #{retryCount}).");
continue; // Retry the loop
}
- logger.Info("[ClientCredentialRequest] OnMsalServiceFailureCallback returned false. Propagating exception.");
+ logger.Info("[ClientCredentialRequest] OnMsalServiceFailure returned false. Propagating exception.");
}
- // Invoke OnSuccess callback with failure result
+ // Invoke OnCompletion callback with failure result
await InvokeOnSuccessCallbackAsync(authResult: null, exception: serviceEx, logger).ConfigureAwait(false);
// Re-throw if no callback or callback returned false
@@ -179,7 +181,7 @@ private async Task GetAccessTokenAsync(
}
catch (MsalException ex)
{
- // For non-service exceptions (MsalClientException, etc.), invoke OnSuccess and re-throw
+ // For non-service exceptions (MsalClientException, etc.), invoke OnCompletion and re-throw
await InvokeOnSuccessCallbackAsync(authResult: null, exception: ex, logger).ConfigureAwait(false);
throw;
}
@@ -187,7 +189,7 @@ private async Task GetAccessTokenAsync(
}
///
- /// Invokes the OnMsalServiceFailureCallback if configured.
+ /// Invokes the OnMsalServiceFailure if configured.
/// Returns true if the request should be retried, false otherwise.
///
private async Task InvokeOnMsalServiceFailureCallbackAsync(
@@ -196,26 +198,30 @@ private async Task InvokeOnMsalServiceFailureCallbackAsync(
{
try
{
- var options = new AssertionRequestOptions(AuthenticationRequestParameters.AppConfig);
+ var tokenEndpoint = await AuthenticationRequestParameters.Authority.GetTokenEndpointAsync(AuthenticationRequestParameters.RequestContext).ConfigureAwait(false);
+ var options = new AssertionRequestOptions(
+ AuthenticationRequestParameters.AppConfig,
+ tokenEndpoint,
+ AuthenticationRequestParameters.AuthorityManager.Authority.TenantId);
bool shouldRetry = await AuthenticationRequestParameters.AppConfig
- .OnMsalServiceFailureCallback(options, serviceException)
+ .OnMsalServiceFailure(options, serviceException)
.ConfigureAwait(false);
- logger.Verbose(() => $"[ClientCredentialRequest] OnMsalServiceFailureCallback returned: {shouldRetry}");
+ logger.Verbose(() => $"[ClientCredentialRequest] OnMsalServiceFailure returned: {shouldRetry}");
return shouldRetry;
}
catch (Exception ex)
{
// If the callback throws, log and don't retry
- logger.Error($"[ClientCredentialRequest] OnMsalServiceFailureCallback threw an exception: {ex.Message}");
+ logger.Error($"[ClientCredentialRequest] OnMsalServiceFailure threw an exception: {ex.Message}");
logger.ErrorPii(ex);
return false;
}
}
///
- /// Invokes the OnSuccessCallback if configured.
+ /// Invokes the OnCompletion if configured.
/// Exceptions from the callback are caught and logged to prevent disrupting the authentication flow.
///
private async Task InvokeOnSuccessCallbackAsync(
@@ -223,35 +229,40 @@ private async Task InvokeOnSuccessCallbackAsync(
MsalException exception,
ILoggerAdapter logger)
{
- if (AuthenticationRequestParameters.AppConfig.OnSuccessCallback == null)
+ if (AuthenticationRequestParameters.AppConfig.OnCompletion == null)
{
return;
}
try
{
- logger.Verbose(() => "[ClientCredentialRequest] Invoking OnSuccess callback.");
+ logger.Verbose(() => "[ClientCredentialRequest] Invoking OnCompletion callback.");
- var options = new AssertionRequestOptions(AuthenticationRequestParameters.AppConfig);
+ var tokenEndpoint = await AuthenticationRequestParameters.Authority.GetTokenEndpointAsync(AuthenticationRequestParameters.RequestContext).ConfigureAwait(false);
+ var options = new AssertionRequestOptions(
+ AuthenticationRequestParameters.AppConfig,
+ tokenEndpoint,
+ AuthenticationRequestParameters.AuthorityManager.Authority.TenantId);
var executionResult = new ExecutionResult
{
Successful = authResult != null,
Result = authResult,
- Exception = exception
+ Exception = exception,
+ Certificate = AuthenticationRequestParameters.ResolvedCertificate
};
await AuthenticationRequestParameters.AppConfig
- .OnSuccessCallback(options, executionResult)
+ .OnCompletion(options, executionResult)
.ConfigureAwait(false);
- logger.Verbose(() => "[ClientCredentialRequest] OnSuccess callback completed successfully.");
+ logger.Verbose(() => "[ClientCredentialRequest] OnCompletion callback completed successfully.");
}
catch (Exception ex)
{
// Catch and log any exceptions from the observer callback
// Do not propagate - observer should not disrupt authentication flow
- logger.Error($"[ClientCredentialRequest] OnSuccess callback threw an exception: {ex.Message}");
+ logger.Error($"[ClientCredentialRequest] OnCompletion callback threw an exception: {ex.Message}");
logger.ErrorPii(ex);
}
}
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
index 0028dea8d0..5382d2bfa5 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt
@@ -4,9 +4,10 @@ Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnCompletion(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onCompletion) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailure) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
index 0028dea8d0..5382d2bfa5 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt
@@ -4,9 +4,10 @@ Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnCompletion(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onCompletion) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailure) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
index 0028dea8d0..5382d2bfa5 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt
@@ -4,9 +4,10 @@ Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnCompletion(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onCompletion) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailure) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
index 0028dea8d0..5382d2bfa5 100644
--- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt
@@ -4,9 +4,10 @@ Microsoft.Identity.Client.AssertionRequestOptions.TenantId.get -> string
Microsoft.Identity.Client.AssertionRequestOptions.TenantId.set -> void
const Microsoft.Identity.Client.MsalError.InvalidClientCredentialConfiguration = "invalid_client_credential_configuration" -> string
Microsoft.Identity.Client.Extensibility.ExecutionResult
+Microsoft.Identity.Client.Extensibility.ExecutionResult.Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2
Microsoft.Identity.Client.Extensibility.ExecutionResult.Exception.get -> Microsoft.Identity.Client.MsalException
Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft.Identity.Client.AuthenticationResult
Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailureCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
-static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnSuccess(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onSuccessCallback) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnCompletion(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func onCompletion) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
+static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnMsalServiceFailure(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> onMsalServiceFailure) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
index a8ff9440d9..853a654d5c 100644
--- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
@@ -6,6 +6,7 @@
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensibility;
+using Microsoft.Identity.Client.Internal.ClientCredential;
using Microsoft.Identity.Test.Common;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -50,7 +51,8 @@ Task certificateProvider(AssertionRequestOptions options)
.BuildConcrete();
// Assert
- Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ClientCredentialCertificateProvider);
+ Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ClientCredential);
+ Assert.IsInstanceOfType((app.AppConfig as ApplicationConfiguration)?.ClientCredential, typeof(DynamicCertificateClientCredential));
Assert.IsFalse(callbackInvoked, "Certificate provider callback is not yet invoked.");
}
@@ -98,8 +100,8 @@ Task secondProvider(AssertionRequestOptions options)
// Assert - last one should be stored
var config = app.AppConfig as ApplicationConfiguration;
Assert.IsNotNull(config);
- Assert.IsNotNull(config.ClientCredentialCertificateProvider);
- Assert.AreNotSame(firstProvider, config.ClientCredentialCertificateProvider);
+ Assert.IsNotNull(config.ClientCredential);
+ Assert.IsInstanceOfType(config.ClientCredential, typeof(DynamicCertificateClientCredential));
}
#endregion
@@ -121,7 +123,7 @@ public void OnMsalServiceFailure_CallbackIsStored()
.BuildConcrete();
// Assert
- Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.OnMsalServiceFailureCallback);
+ Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.OnMsalServiceFailure);
}
[TestMethod]
@@ -154,11 +156,11 @@ public void OnSuccess_CallbackIsStored()
.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
.WithClientSecret(TestConstants.ClientSecret)
- .OnSuccess(onSuccessCallback)
+ .OnCompletion(onSuccessCallback)
.BuildConcrete();
// Assert
- Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.OnSuccessCallback);
+ Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.OnCompletion);
}
[TestMethod]
@@ -170,10 +172,10 @@ public void OnSuccess_ThrowsOnNullCallback()
.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
.WithClientSecret(TestConstants.ClientSecret)
- .OnSuccess(null)
+ .OnCompletion(null)
.Build());
- Assert.AreEqual("onSuccessCallback", ex.ParamName);
+ Assert.AreEqual("onCompletion", ex.ParamName);
}
#endregion
@@ -258,14 +260,14 @@ public void AllThreeExtensibilityPoints_CanBeConfiguredTogether()
.WithExperimentalFeatures()
.WithCertificate(certificateProvider)
.OnMsalServiceFailure(onMsalServiceFailure)
- .OnSuccess(onSuccess)
+ .OnCompletion(onSuccess)
.BuildConcrete();
// Assert
var config = app.AppConfig as ApplicationConfiguration;
- Assert.IsNotNull(config.ClientCredentialCertificateProvider);
- Assert.IsNotNull(config.OnMsalServiceFailureCallback);
- Assert.IsNotNull(config.OnSuccessCallback);
+ Assert.IsNotNull(config.ClientCredential);
+ Assert.IsNotNull(config.OnMsalServiceFailure);
+ Assert.IsNotNull(config.OnCompletion);
}
[TestMethod]
@@ -276,36 +278,36 @@ public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
Task onMsalServiceFailure(AssertionRequestOptions options, MsalException ex) => Task.FromResult(false);
Task onSuccess(AssertionRequestOptions options, ExecutionResult result) => Task.CompletedTask;
- // Act - Order: OnSuccess, OnMsalServiceFailure, Certificate
+ // Act - Order: OnCompletion, OnMsalServiceFailure, Certificate
var app1 = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
- .OnSuccess(onSuccess)
+ .OnCompletion(onSuccess)
.OnMsalServiceFailure(onMsalServiceFailure)
.WithCertificate(certificateProvider)
.BuildConcrete();
- // Act - Order: OnMsalServiceFailure, Certificate, OnSuccess
+ // Act - Order: OnMsalServiceFailure, Certificate, OnCompletion
var app2 = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
.OnMsalServiceFailure(onMsalServiceFailure)
.WithCertificate(certificateProvider)
- .OnSuccess(onSuccess)
+ .OnCompletion(onSuccess)
.BuildConcrete();
// Assert
var config1 = app1.AppConfig as ApplicationConfiguration;
Assert.IsNotNull(config1);
- Assert.IsNotNull(config1.ClientCredentialCertificateProvider);
- Assert.IsNotNull(config1.OnMsalServiceFailureCallback);
- Assert.IsNotNull(config1.OnSuccessCallback);
+ Assert.IsNotNull(config1.ClientCredential);
+ Assert.IsNotNull(config1.OnMsalServiceFailure);
+ Assert.IsNotNull(config1.OnCompletion);
var config2 = app2.AppConfig as ApplicationConfiguration;
Assert.IsNotNull(config2, "app2.AppConfig should be of type ApplicationConfiguration");
- Assert.IsNotNull(config2.ClientCredentialCertificateProvider);
- Assert.IsNotNull(config2.OnMsalServiceFailureCallback);
- Assert.IsNotNull(config2.OnSuccessCallback);
+ Assert.IsNotNull(config2.ClientCredential);
+ Assert.IsNotNull(config2.OnMsalServiceFailure);
+ Assert.IsNotNull(config2.OnCompletion);
}
[TestMethod]
@@ -328,7 +330,7 @@ Task certificateProvider(AssertionRequestOptions options)
// Assert
Assert.IsNotNull(app);
- Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ClientCredentialCertificateProvider);
+ Assert.IsNotNull((app.AppConfig as ApplicationConfiguration)?.ClientCredential);
}
#endregion
diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
index d9727fd777..7b5f2eac99 100644
--- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs
@@ -3,6 +3,7 @@
#if !ANDROID && !iOS
using System;
+using System.Linq;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensibility;
@@ -133,6 +134,7 @@ public async Task OnMsalServiceFailure_RetriesOnServiceError_SucceedsAsync()
Assert.IsNotNull(capturedException, "Exception should be MsalServiceException");
Assert.AreEqual(TestConstants.ClientId, options.ClientID);
+ Assert.IsNotNull(options.TokenEndpoint, "TokenEndpoint should be available in failure callback");
// Retry on 503
return Task.FromResult(capturedException.StatusCode == 400 && failureCallbackCount < 3);
@@ -238,7 +240,7 @@ await app.AcquireTokenForClient(TestConstants.s_scope)
#region OnSuccess Integration Tests
[TestMethod]
- [Description("OnSuccess is invoked with successful result")]
+ [Description("OnCompletion is invoked with successful result")]
public async Task OnSuccess_InvokedWithSuccessfulResultAsync()
{
// Arrange
@@ -256,7 +258,7 @@ public async Task OnSuccess_InvokedWithSuccessfulResultAsync()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnSuccess((AssertionRequestOptions options, ExecutionResult result) =>
+ .OnCompletion((AssertionRequestOptions options, ExecutionResult result) =>
{
observerInvoked = true;
capturedResult = result;
@@ -266,6 +268,7 @@ public async Task OnSuccess_InvokedWithSuccessfulResultAsync()
Assert.IsNotNull(result.Result);
Assert.IsNull(result.Exception);
Assert.AreEqual(TestConstants.ClientId, options.ClientID);
+ Assert.IsNotNull(options.TokenEndpoint, "TokenEndpoint should be available in success callback");
return Task.CompletedTask;
})
@@ -288,11 +291,14 @@ public async Task OnSuccess_InvokedWithSuccessfulResultAsync()
}
[TestMethod]
- [Description("OnSuccess is invoked with failure result after retries exhausted")]
+ [Description("OnCompletion is invoked with failure result after retries exhausted")]
public async Task OnSuccess_InvokedWithFailureResult_AfterRetriesExhaustedAsync()
{
// Arrange
- using (var harness = CreateTestHarness())
+ var logMessages = new System.Collections.Generic.List();
+ LogCallback logCallback = (level, message, pii) => logMessages.Add(message);
+
+ using (var harness = CreateTestHarness(logCallback: logCallback))
{
harness.HttpManager.AddInstanceDiscoveryMockHandler();
@@ -306,12 +312,13 @@ public async Task OnSuccess_InvokedWithFailureResult_AfterRetriesExhaustedAsync(
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
+ .WithLogging(logCallback, LogLevel.Info, enablePiiLogging: true, enableDefaultPlatformLogging: false)
.OnMsalServiceFailure((AssertionRequestOptions options, MsalException ex) =>
{
retryCount++;
return Task.FromResult(retryCount < 2); // Retry once, then give up
})
- .OnSuccess((AssertionRequestOptions options, ExecutionResult result) =>
+ .OnCompletion((AssertionRequestOptions options, ExecutionResult result) =>
{
observerInvoked = true;
capturedResult = result;
@@ -320,6 +327,7 @@ public async Task OnSuccess_InvokedWithFailureResult_AfterRetriesExhaustedAsync(
Assert.IsNull(result.Result);
Assert.IsNotNull(result.Exception);
Assert.IsInstanceOfType(result.Exception, typeof(MsalServiceException));
+ Assert.IsNotNull(options.TokenEndpoint, "TokenEndpoint should be available even on failure");
return Task.CompletedTask;
})
@@ -341,11 +349,15 @@ await app.AcquireTokenForClient(TestConstants.s_scope)
Assert.IsNotNull(capturedResult);
Assert.IsFalse(capturedResult.Successful);
Assert.AreEqual(exception, capturedResult.Exception);
+
+ // Verify retry logging
+ Assert.IsTrue(logMessages.Any(m => m.Contains("[ClientCredentialRequest] OnMsalServiceFailure returned true. Retrying token request (Retry #1).")),
+ "Should log retry #1");
}
}
[TestMethod]
- [Description("OnSuccess exception is caught and logged, doesn't disrupt flow")]
+ [Description("OnCompletion exception is caught and logged, doesn't disrupt flow")]
public async Task OnSuccess_ExceptionIsCaught_DoesNotDisruptFlowAsync()
{
// Arrange
@@ -359,7 +371,7 @@ public async Task OnSuccess_ExceptionIsCaught_DoesNotDisruptFlowAsync()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(harness.HttpManager)
- .OnSuccess((AssertionRequestOptions options, ExecutionResult result) =>
+ .OnCompletion((AssertionRequestOptions options, ExecutionResult result) =>
{
throw new InvalidOperationException("Observer threw exception");
})
@@ -387,7 +399,10 @@ public async Task OnSuccess_ExceptionIsCaught_DoesNotDisruptFlowAsync()
public async Task AllThreeExtensibilityPoints_WorkTogetherAsync()
{
// Arrange
- using (var harness = CreateTestHarness())
+ var logMessages = new System.Collections.Generic.List();
+ LogCallback logCallback = (level, message, pii) => logMessages.Add(message);
+
+ using (var harness = CreateTestHarness(logCallback: logCallback))
{
harness.HttpManager.AddInstanceDiscoveryMockHandler();
@@ -402,23 +417,27 @@ public async Task AllThreeExtensibilityPoints_WorkTogetherAsync()
.WithExperimentalFeatures()
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpManager(harness.HttpManager)
+ .WithLogging(logCallback, LogLevel.Info, enablePiiLogging: true, enableDefaultPlatformLogging: false)
.WithCertificate((AssertionRequestOptions options) =>
{
certProviderCount++;
Assert.AreEqual(TestConstants.ClientId, options.ClientID);
+ Assert.IsNotNull(options.TokenEndpoint, "TokenEndpoint should be available in cert provider");
return Task.FromResult(certificate);
})
.OnMsalServiceFailure((AssertionRequestOptions options, MsalException ex) =>
{
retryCallbackCount++;
Assert.IsInstanceOfType(ex, typeof(MsalServiceException));
+ Assert.IsNotNull(options.TokenEndpoint, "TokenEndpoint should be available in retry callback");
return Task.FromResult(retryCallbackCount < 2); // Retry once
})
- .OnSuccess((AssertionRequestOptions options, ExecutionResult result) =>
+ .OnCompletion((AssertionRequestOptions options, ExecutionResult result) =>
{
observerInvoked = true;
Assert.IsTrue(result.Successful);
Assert.IsNotNull(result.Result);
+ Assert.IsNotNull(options.TokenEndpoint, "TokenEndpoint should be available in success callback");
return Task.CompletedTask;
})
.Build();
@@ -437,6 +456,10 @@ public async Task AllThreeExtensibilityPoints_WorkTogetherAsync()
Assert.AreEqual(1, retryCallbackCount, "Retry callback invoked once");
Assert.IsTrue(observerInvoked, "Observer invoked once at completion");
Assert.IsNotNull(result.AccessToken);
+
+ // Verify retry logging
+ Assert.IsTrue(logMessages.Any(m => m.Contains("[ClientCredentialRequest] OnMsalServiceFailure returned true. Retrying token request (Retry #1).")),
+ "Should log retry #1");
}
}