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"); } }