From 95ca36a1e728255b76eef0956620604a733bcba1 Mon Sep 17 00:00:00 2001 From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:32:45 -0800 Subject: [PATCH 1/8] Add CertificateOptions and update the WithCertificate APIs to use CertificateOptions --- .../AppConfig/CertificateOptions.cs | 25 +++ .../ConfidentialClientApplicationBuilder.cs | 35 +++ ...ntialClientApplicationBuilderExtensions.cs | 12 +- .../PublicApi/net462/PublicAPI.Shipped.txt | 1 - .../PublicApi/net462/PublicAPI.Unshipped.txt | 8 +- .../PublicApi/net472/PublicAPI.Shipped.txt | 1 - .../PublicApi/net472/PublicAPI.Unshipped.txt | 8 +- .../net8.0-android/PublicAPI.Shipped.txt | 2 +- .../net8.0-android/PublicAPI.Unshipped.txt | 8 +- .../net8.0-ios/PublicAPI.Shipped.txt | 1 - .../net8.0-ios/PublicAPI.Unshipped.txt | 8 +- .../PublicApi/net8.0/PublicAPI.Shipped.txt | 1 - .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 8 +- .../netstandard2.0/PublicAPI.Shipped.txt | 1 - .../netstandard2.0/PublicAPI.Unshipped.txt | 8 +- .../Core/Mocks/MockHttpManagerExtensions.cs | 27 ++- ...nfidentialClientApplicationBuilderTests.cs | 94 +++++++- ...lClientApplicationExtensibilityApiTests.cs | 77 ++++++- ...tialClientApplicationExtensibilityTests.cs | 201 +++++++++++++++++- 19 files changed, 491 insertions(+), 35 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs diff --git a/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs b/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs new file mode 100644 index 0000000000..be47d7860c --- /dev/null +++ b/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Identity.Client.AppConfig +{ + /// + /// Represents configuration options for certificate handling or management. + /// + public class CertificateOptions + { + /// + /// Gets or sets a value indicating whether the X.509 certificate chain (x5c) should be included in the token + /// request. + /// + /// Set this property to to include X5C in the token request; + /// otherwise, set it to . + public bool SendX5C { get; set; } = false; + } +} diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs index cf9e22b9c2..28f97f2912 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs @@ -130,6 +130,41 @@ public ConfidentialClientApplicationBuilder WithCertificate(X509Certificate2 cer return this; } + /// + /// Sets the certificate associated with the application. + /// Applicable to first-party applications only, this method also allows to specify + /// if the x5c claim should be sent to Azure AD. + /// Sending the x5c enables application developers to achieve easy certificate roll-over in Azure AD: + /// this method will send the certificate chain to Azure AD along with the token request, + /// so that Azure AD can use it to validate the subject name based on a trusted issuer policy. + /// This saves the application admin from the need to explicitly manage the certificate rollover + /// (either via portal or PowerShell/CLI operation). For details see https://aka.ms/msal-net-sni + /// + /// The X509 certificate used as credentials to prove the identity of the application to Azure AD. + /// Configuration options for certificate handling. + /// You should use certificates with a private key size of at least 2048 bytes. Future versions of this library might reject certificates with smaller keys. + public ConfidentialClientApplicationBuilder WithCertificate(X509Certificate2 certificate, CertificateOptions certificateOptions) + { + if (certificate == null) + { + throw new ArgumentNullException(nameof(certificate)); + } + + if (certificateOptions?.SendX5C?? false && !certificate.HasPrivateKey) + { + throw new MsalClientException(MsalError.CertWithoutPrivateKey, MsalErrorMessage.CertMustHavePrivateKey(nameof(certificate))); + } + + if (!certificate.HasPrivateKey) + { + throw new MsalClientException(MsalError.CertWithoutPrivateKey, MsalErrorMessage.CertMustHavePrivateKey(nameof(certificate))); + } + + Config.ClientCredential = new CertificateClientCredential(certificate); + Config.SendX5C = certificateOptions?.SendX5C ?? false; + return this; + } + /// /// Sets the certificate associated with the application along with the specific claims to sign. /// By default, this will merge the with the default required set of claims needed for authentication. diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs index 2596563196..159bca7b34 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.AppConfig; using Microsoft.Identity.Client.Internal.ClientCredential; namespace Microsoft.Identity.Client.Extensibility @@ -41,8 +42,8 @@ public static ConfidentialClientApplicationBuilder WithAppTokenProvider( /// /// 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. - /// + /// Must return a valid with a private key. + /// Configuration options for the certificate handling. /// The builder to chain additional configuration calls. /// Thrown when is null. /// @@ -58,7 +59,8 @@ public static ConfidentialClientApplicationBuilder WithAppTokenProvider( /// public static ConfidentialClientApplicationBuilder WithCertificate( this ConfidentialClientApplicationBuilder builder, - Func> certificateProvider) + Func> certificateProvider, + CertificateOptions certificateOptions) { if (certificateProvider == null) { @@ -69,7 +71,9 @@ public static ConfidentialClientApplicationBuilder WithCertificate( // The certificate will be resolved dynamically via the provider in ResolveCertificateAsync builder.Config.ClientCredential = new DynamicCertificateClientCredential( certificateProvider: certificateProvider); - + + builder.Config.SendX5C = certificateOptions?.SendX5C ?? false; + return builder; } diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt index f0b1669e7f..d9760945d6 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt @@ -1109,7 +1109,6 @@ Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft. Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool 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 Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.get -> string Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void 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 7f5e2198da..e472e3f717 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1,2 +1,8 @@ +Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions -static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder \ No newline at end of file +static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt index f0b1669e7f..d9760945d6 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt @@ -1109,7 +1109,6 @@ Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft. Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool 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 Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.get -> string Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void 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 7f5e2198da..e472e3f717 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1,2 +1,8 @@ +Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions -static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder \ No newline at end of file +static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt index 2a0890082a..052e7123f4 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt @@ -25,7 +25,7 @@ const Microsoft.Identity.Client.MsalError.BrokerResponseHashMismatch = "broker_r const Microsoft.Identity.Client.MsalError.BrokerResponseReturnedError = "broker_response_returned_error" -> string const Microsoft.Identity.Client.MsalError.CannotAccessUserInformationOrUserNotDomainJoined = "user_information_access_failed" -> string const Microsoft.Identity.Client.MsalError.CannotInvokeBroker = "cannot_invoke_broker" -> string -const Microsoft.Identity.Client.MsalError.CertificateNotRsa = "certificate_not_rsa" -> string +const Microsoft.Identity.Client.MsalError.CertificateNotRsa = "certificaFwithcertificatete_not_rsa" -> string const Microsoft.Identity.Client.MsalError.CertWithoutPrivateKey = "cert_without_private_key" -> string const Microsoft.Identity.Client.MsalError.ClientCredentialAuthenticationTypeMustBeDefined = "Client_Credentials_Required_In_Confidential_Client_Application" -> string const Microsoft.Identity.Client.MsalError.ClientCredentialAuthenticationTypesAreMutuallyExclusive = "Client_Credential_Authentication_Types_Are_Mutually_Exclusive" -> string 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 7f5e2198da..e472e3f717 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,2 +1,8 @@ +Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions -static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder \ No newline at end of file +static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Shipped.txt index c95127d057..979f38fe47 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Shipped.txt @@ -1077,7 +1077,6 @@ Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft. Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool 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 Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.get -> string Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void 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 7f5e2198da..e472e3f717 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,2 +1,8 @@ +Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions -static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder \ No newline at end of file +static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt index 0ff72f99cf..cd3e95cecc 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt @@ -1071,7 +1071,6 @@ Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft. Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool 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 Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.get -> string Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void 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 7f5e2198da..e472e3f717 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,2 +1,8 @@ +Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions -static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder \ No newline at end of file +static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt index 25abd7973d..fa93b701b7 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt @@ -1071,7 +1071,6 @@ Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft. Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool 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 Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.get -> string Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void 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 7f5e2198da..e472e3f717 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,2 +1,8 @@ +Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions -static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder \ No newline at end of file +static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs index 8d4268f625..c1f8862c87 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs @@ -189,8 +189,8 @@ public static MockHttpMessageHandler AddMockHandlerSuccessfulClientCredentialTok string tokenType = "Bearer", IList unexpectedHttpHeaders = null, Dictionary expectedPostData = null, - bool addClientInfo = false - ) + bool addClientInfo = false, + bool sendX5C = false) { var handler = new MockHttpMessageHandler() { @@ -201,6 +201,29 @@ public static MockHttpMessageHandler AddMockHandlerSuccessfulClientCredentialTok ExpectedPostData = expectedPostData }; + if (sendX5C) + { + handler.AdditionalRequestValidation = (request) => + { + // validate that the request have x5c header + if (expectedPostData != null && expectedPostData.ContainsKey("client_assertion")) + { + string clientAssertion = expectedPostData["client_assertion"]; + string[] assertionParts = clientAssertion.Split('.'); + if (assertionParts.Length != 3) + { + throw new InvalidDataException("client_assertion is not in the correct format"); + } + string header = assertionParts[0]; + string headerJson = Base64UrlHelpers.Decode(header); + if (!headerJson.Contains("\"x5c\"")) + { + throw new InvalidDataException("client_assertion does not contain x5c header"); + } + } + }; + } + httpManager.AddMockHandler(handler); return handler; diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationBuilderTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationBuilderTests.cs index 1bab2b0ab3..8c19bd4f25 100644 --- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationBuilderTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationBuilderTests.cs @@ -2,16 +2,14 @@ // Licensed under the MIT License. using System; -using System.Drawing.Text; using System.IO; using System.Linq; using System.Security.Cryptography.X509Certificates; -using System.Security.Permissions; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Internal; -using Microsoft.Identity.Client.Internal.ClientCredential; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -380,6 +378,94 @@ public void TestConstructor_WithCertificate_SendX5C() Assert.IsTrue((app.AppConfig as ApplicationConfiguration).SendX5C); } + [TestMethod] + [DeploymentItem(@"Resources\testCert.crtfile")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Internal.Analyzers", "IA5352:DoNotMisuseCryptographicApi", Justification = "Suppressing RoslynAnalyzers: Rule: IA5352 - Do Not Misuse Cryptographic APIs in test only code")] + public void TestConstructor_WithCertificate_CertificateOptions_SendX5C_True() + { + var cert = new X509Certificate2( + ResourceHelper.GetTestResourceRelativePath("testCert.crtfile"), TestConstants.TestCertPassword); + var certificateOptions = new CertificateOptions { SendX5C = true }; + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithCertificate(cert, certificateOptions) + .Build(); + + Assert.IsTrue((app.AppConfig as ApplicationConfiguration).SendX5C, "SendX5C should be true when CertificateOptions.SendX5C is true"); + } + + [TestMethod] + [DeploymentItem(@"Resources\testCert.crtfile")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Internal.Analyzers", "IA5352:DoNotMisuseCryptographicApi", Justification = "Suppressing RoslynAnalyzers: Rule: IA5352 - Do Not Misuse Cryptographic APIs in test only code")] + public void TestConstructor_WithCertificate_CertificateOptions_SendX5C_False() + { + var cert = new X509Certificate2( + ResourceHelper.GetTestResourceRelativePath("testCert.crtfile"), TestConstants.TestCertPassword); + var certificateOptions = new CertificateOptions { SendX5C = false }; + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithCertificate(cert, certificateOptions) + .Build(); + + Assert.IsFalse((app.AppConfig as ApplicationConfiguration).SendX5C, "SendX5C should be false when CertificateOptions.SendX5C is false"); + } + + [TestMethod] + [DeploymentItem(@"Resources\testCert.crtfile")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Internal.Analyzers", "IA5352:DoNotMisuseCryptographicApi", Justification = "Suppressing RoslynAnalyzers: Rule: IA5352 - Do Not Misuse Cryptographic APIs in test only code")] + public void TestConstructor_WithCertificate_NullCertificateOptions_DefaultsToSendX5C_False() + { + var cert = new X509Certificate2( + ResourceHelper.GetTestResourceRelativePath("testCert.crtfile"), TestConstants.TestCertPassword); + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithCertificate(cert, (CertificateOptions)null) + .Build(); + + Assert.IsFalse((app.AppConfig as ApplicationConfiguration).SendX5C, "SendX5C should default to false when CertificateOptions is null"); + } + + [TestMethod] + [DeploymentItem(@"Resources\testCert.crtfile")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Internal.Analyzers", "IA5352:DoNotMisuseCryptographicApi", Justification = "Suppressing RoslynAnalyzers: Rule: IA5352 - Do Not Misuse Cryptographic APIs in test only code")] + public void TestConstructor_WithCertificate_NullCertificate_ThrowsException() + { + var certificateOptions = new CertificateOptions { SendX5C = true }; + + Assert.ThrowsException(() => + ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithCertificate((X509Certificate2)null, certificateOptions) + .Build()); + } + + [TestMethod] + [DeploymentItem(@"Resources\valid_cert.cer")] + public void TestConstructor_WithCertificate_CertificateOptions_WithoutPrivateKey() + { + var cert = new X509Certificate2( + ResourceHelper.GetTestResourceRelativePath("valid_cert.cer")); + var certificateOptions = new CertificateOptions { SendX5C = false }; + + try + { + ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithCertificate(cert, certificateOptions) + .Build(); + + Assert.Fail("Should have thrown MsalClientException"); + } + catch (MsalClientException e) + { + Assert.IsNotNull(e); + Assert.AreEqual(MsalError.CertWithoutPrivateKey, e.ErrorCode); + } + } + [TestMethod] [DeploymentItem(@"Resources\CustomInstanceMetadata.json")] public void TestConstructor_WithValidInstanceDicoveryMetadata() diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs index eb8a299ae7..570dc5fc89 100644 --- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Client.Internal.ClientCredential; using Microsoft.Identity.Test.Common; @@ -17,6 +18,7 @@ namespace Microsoft.Identity.Test.Unit.AppConfigTests public class ConfidentialClientApplicationExtensibilityApiTests { private X509Certificate2 _certificate; + private CertificateOptions _certificateOptions = new CertificateOptions(); [TestInitialize] public void TestInitialize() @@ -47,7 +49,7 @@ Task certificateProvider(AssertionRequestOptions options) var app = ConfidentialClientApplicationBuilder .Create(TestConstants.ClientId) .WithExperimentalFeatures() - .WithCertificate(certificateProvider) + .WithCertificate(certificateProvider, _certificateOptions) .BuildConcrete(); // Assert @@ -64,7 +66,7 @@ public void WithCertificate_ThrowsOnNullCallback() ConfidentialClientApplicationBuilder .Create(TestConstants.ClientId) .WithExperimentalFeatures() - .WithCertificate((Func>)null) + .WithCertificate((Func>) null, null) .Build()); Assert.AreEqual("certificateProvider", ex.ParamName); @@ -93,8 +95,8 @@ Task secondProvider(AssertionRequestOptions options) var app = ConfidentialClientApplicationBuilder .Create(TestConstants.ClientId) .WithExperimentalFeatures() - .WithCertificate(firstProvider) - .WithCertificate(secondProvider) + .WithCertificate(firstProvider, _certificateOptions) + .WithCertificate(secondProvider, _certificateOptions) .BuildConcrete(); // Assert - last one should be stored @@ -104,6 +106,65 @@ Task secondProvider(AssertionRequestOptions options) Assert.IsInstanceOfType(config.ClientCredential, typeof(DynamicCertificateClientCredential)); } + [TestMethod] + public void WithCertificate_CertificateOptions_SendX5C_True_IsStored() + { + // Arrange + var certificateOptions = new CertificateOptions { SendX5C = true }; + Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate()); + + // Act + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithCertificate(certificateProvider, certificateOptions) + .BuildConcrete(); + + // Assert + var config = app.AppConfig as ApplicationConfiguration; + Assert.IsNotNull(config); + Assert.IsTrue(config.SendX5C, "SendX5C should be true when CertificateOptions.SendX5C is true"); + } + + [TestMethod] + public void WithCertificate_CertificateOptions_SendX5C_False_IsStored() + { + // Arrange + var certificateOptions = new CertificateOptions { SendX5C = false }; + Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate()); + + // Act + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithCertificate(certificateProvider, certificateOptions) + .BuildConcrete(); + + // Assert + var config = app.AppConfig as ApplicationConfiguration; + Assert.IsNotNull(config); + Assert.IsFalse(config.SendX5C, "SendX5C should be false when CertificateOptions.SendX5C is false"); + } + + [TestMethod] + public void WithCertificate_NullCertificateOptions_DefaultsToSendX5C_False() + { + // Arrange + Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate()); + + // Act + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithCertificate(certificateProvider, null) + .BuildConcrete(); + + // Assert + var config = app.AppConfig as ApplicationConfiguration; + Assert.IsNotNull(config); + Assert.IsFalse(config.SendX5C, "SendX5C should default to false when CertificateOptions is null"); + } + #endregion #region OnMsalServiceFailure Tests @@ -258,7 +319,7 @@ public void AllThreeExtensibilityPoints_CanBeConfiguredTogether() var app = ConfidentialClientApplicationBuilder .Create(TestConstants.ClientId) .WithExperimentalFeatures() - .WithCertificate(certificateProvider) + .WithCertificate(certificateProvider, _certificateOptions) .OnMsalServiceFailure(onMsalServiceFailure) .OnCompletion(onSuccess) .BuildConcrete(); @@ -284,7 +345,7 @@ public void ExtensibilityPoints_CanBeConfiguredInAnyOrder() .WithExperimentalFeatures() .OnCompletion(onSuccess) .OnMsalServiceFailure(onMsalServiceFailure) - .WithCertificate(certificateProvider) + .WithCertificate(certificateProvider, _certificateOptions) .BuildConcrete(); // Act - Order: OnMsalServiceFailure, Certificate, OnCompletion @@ -292,7 +353,7 @@ public void ExtensibilityPoints_CanBeConfiguredInAnyOrder() .Create(TestConstants.ClientId) .WithExperimentalFeatures() .OnMsalServiceFailure(onMsalServiceFailure) - .WithCertificate(certificateProvider) + .WithCertificate(certificateProvider, _certificateOptions) .OnCompletion(onSuccess) .BuildConcrete(); @@ -325,7 +386,7 @@ Task certificateProvider(AssertionRequestOptions options) .Create(TestConstants.ClientId) .WithExperimentalFeatures() .WithAuthority(TestConstants.AadAuthorityWithTestTenantId) - .WithCertificate(certificateProvider) + .WithCertificate(certificateProvider, _certificateOptions) .BuildConcrete(); // Assert diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs index 681fdcc758..dfd6a6bd85 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationExtensibilityTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; @@ -17,12 +18,202 @@ namespace Microsoft.Identity.Test.Unit.PublicApiTests [DeploymentItem(@"Resources\testCert.crtfile")] public class ConfidentialClientApplicationExtensibilityTests : TestBase { + private CertificateOptions _certificateOptions = new CertificateOptions(); + [TestInitialize] public override void TestInitialize() { base.TestInitialize(); } + #region CertificateOptions Tests + + [TestMethod] + [Description("WithCertificate with CertificateOptions SendX5C=false stores options correctly")] + public void WithCertificate_CertificateOptions_SendX5C_False() + { + // Arrange + var certificate = CertHelper.GetOrCreateTestCert(); + var certificateOptions = new CertificateOptions { SendX5C = false }; + + // Act + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithAuthority(TestConstants.AuthorityCommonTenant) + .WithCertificate((AssertionRequestOptions options) => + { + return Task.FromResult(certificate); + }, certificateOptions) + .BuildConcrete(); + + // Assert + Assert.IsFalse((app.AppConfig as ApplicationConfiguration).SendX5C, "SendX5C should be false when CertificateOptions.SendX5C is false"); + } + + [TestMethod] + [Description("WithCertificate with CertificateOptions SendX5C=true stores options correctly")] + public void WithCertificate_CertificateOptions_SendX5C_True() + { + // Arrange + var certificate = CertHelper.GetOrCreateTestCert(); + var certificateOptions = new CertificateOptions { SendX5C = true }; + + // Act + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithAuthority(TestConstants.AuthorityCommonTenant) + .WithCertificate((AssertionRequestOptions options) => + { + return Task.FromResult(certificate); + }, certificateOptions) + .BuildConcrete(); + + // Assert + Assert.IsTrue((app.AppConfig as ApplicationConfiguration).SendX5C, "SendX5C should be true when CertificateOptions.SendX5C is true"); + } + + [TestMethod] + [Description("WithCertificate with null CertificateOptions defaults SendX5C to false")] + public void WithCertificate_NullCertificateOptions_DefaultsToFalse() + { + // Arrange + var certificate = CertHelper.GetOrCreateTestCert(); + + // Act + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithAuthority(TestConstants.AuthorityCommonTenant) + .WithCertificate((AssertionRequestOptions options) => + { + return Task.FromResult(certificate); + }, null) + .BuildConcrete(); + + // Assert + Assert.IsFalse((app.AppConfig as ApplicationConfiguration).SendX5C, "SendX5C should default to false when CertificateOptions is null"); + } + + [TestMethod] + [Description("CertificateOptions with SendX5C=true works in full token acquisition flow")] + public async Task CertificateOptions_SendX5C_True_InTokenAcquisitionAsync() + { + // Arrange + using (var harness = CreateTestHarness()) + { + harness.HttpManager.AddInstanceDiscoveryMockHandler(); + + var certificate = CertHelper.GetOrCreateTestCert(); + var certificateOptions = new CertificateOptions { SendX5C = true }; + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithAuthority(TestConstants.AuthorityCommonTenant) + .WithHttpManager(harness.HttpManager) + .WithCertificate((AssertionRequestOptions options) => + { + return Task.FromResult(certificate); + }, certificateOptions) + .Build(); + + harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(sendX5C: true); + + // Act + var result = await app.AcquireTokenForClient(TestConstants.s_scope) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.IsNotNull(result.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + } + } + + [TestMethod] + [Description("CertificateOptions with SendX5C=false works in full token acquisition flow")] + public async Task CertificateOptions_SendX5C_False_InTokenAcquisitionAsync() + { + // Arrange + using (var harness = CreateTestHarness()) + { + harness.HttpManager.AddInstanceDiscoveryMockHandler(); + + var certificate = CertHelper.GetOrCreateTestCert(); + var certificateOptions = new CertificateOptions { SendX5C = false }; + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithAuthority(TestConstants.AuthorityCommonTenant) + .WithHttpManager(harness.HttpManager) + .WithCertificate((AssertionRequestOptions options) => + { + return Task.FromResult(certificate); + }, certificateOptions) + .Build(); + + harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); + + // Act + var result = await app.AcquireTokenForClient(TestConstants.s_scope) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.IsNotNull(result.AccessToken); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + } + } + + [TestMethod] + [Description("CertificateOptions works with retry scenario")] + public async Task CertificateOptions_WorksWithRetryScenarioAsync() + { + // Arrange + using (var harness = CreateTestHarness()) + { + harness.HttpManager.AddInstanceDiscoveryMockHandler(); + + var certificate = CertHelper.GetOrCreateTestCert(); + var certificateOptions = new CertificateOptions { SendX5C = true }; + int retryCount = 0; + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithAuthority(TestConstants.AuthorityCommonTenant) + .WithHttpManager(harness.HttpManager) + .WithCertificate((AssertionRequestOptions options) => + { + return Task.FromResult(certificate); + }, certificateOptions) + .OnMsalServiceFailure((AssertionRequestOptions options, ExecutionResult result) => + { + retryCount++; + return Task.FromResult(retryCount < 2); + }) + .Build(); + + // Mock: fail once, then succeed + harness.HttpManager.AddFailureTokenEndpointResponse("request_failed"); + harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(sendX5C: true); + + // Act + var result = await app.AcquireTokenForClient(TestConstants.s_scope) + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.IsNotNull(result.AccessToken); + Assert.AreEqual(1, retryCount, "Should have retried once"); + } + } + + #endregion + #region WithCertificate (Dynamic Provider) Integration Tests [TestMethod] @@ -54,7 +245,7 @@ public async Task DynamicCertificateProvider_IsInvoked_AndUsedForAssertionAsync( Assert.IsNotNull(options.TokenEndpoint); return Task.FromResult(certificate); - }) + }, _certificateOptions) .Build(); harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); @@ -89,7 +280,7 @@ public async Task DynamicCertificateProvider_ReturnsNull_ThrowsExceptionAsync() .WithCertificate((AssertionRequestOptions options) => { return Task.FromResult(null); // Provider returns null - }) + }, _certificateOptions) .Build(); // Act & Assert @@ -224,7 +415,7 @@ public async Task OnMsalServiceFailure_NotInvokedForClientExceptionsAsync() .WithCertificate((AssertionRequestOptions options) => { return Task.FromResult(null); // Will cause MsalClientException - }) + }, _certificateOptions) .OnMsalServiceFailure((AssertionRequestOptions options, ExecutionResult result) => { callbackInvoked = true; @@ -442,7 +633,7 @@ public async Task AllThreeExtensibilityPoints_WorkTogetherAsync() Assert.AreEqual(TestConstants.ClientId, options.ClientID); Assert.IsNotNull(options.TokenEndpoint, "TokenEndpoint should be available in cert provider"); return Task.FromResult(certificate); - }) + }, _certificateOptions) .OnMsalServiceFailure((AssertionRequestOptions options, ExecutionResult result) => { retryCallbackCount++; @@ -508,7 +699,7 @@ public async Task CertificateRotation_DifferentCertOnRetryAsync() certProviderCount++; // Return different cert on retry return Task.FromResult(certProviderCount == 1 ? cert1 : cert2); - }) + }, _certificateOptions) .OnMsalServiceFailure((AssertionRequestOptions options, ExecutionResult result) => { // Validate ExecutionResult From 51a1cf35a781a6b16e5ffd14bfc1c38b5d607bc9 Mon Sep 17 00:00:00 2001 From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:57:09 -0800 Subject: [PATCH 2/8] Update validation applied twice --- .../AppConfig/ConfidentialClientApplicationBuilder.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs index 28f97f2912..8d7de24387 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs @@ -150,11 +150,6 @@ public ConfidentialClientApplicationBuilder WithCertificate(X509Certificate2 cer throw new ArgumentNullException(nameof(certificate)); } - if (certificateOptions?.SendX5C?? false && !certificate.HasPrivateKey) - { - throw new MsalClientException(MsalError.CertWithoutPrivateKey, MsalErrorMessage.CertMustHavePrivateKey(nameof(certificate))); - } - if (!certificate.HasPrivateKey) { throw new MsalClientException(MsalError.CertWithoutPrivateKey, MsalErrorMessage.CertMustHavePrivateKey(nameof(certificate))); From 9f6faf3b3216c7899d33a458ace337b79973f05b Mon Sep 17 00:00:00 2001 From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:38:38 -0800 Subject: [PATCH 3/8] Add AssociateTokensWithCertificateSerialNumber to options --- .../AppConfig/CertificateOptions.cs | 17 +++-- .../ConfidentialClientApplicationBuilder.cs | 9 ++- .../PublicApi/net462/PublicAPI.Unshipped.txt | 2 + .../PublicApi/net472/PublicAPI.Unshipped.txt | 2 + .../net8.0-android/PublicAPI.Unshipped.txt | 2 + .../net8.0-ios/PublicAPI.Unshipped.txt | 2 + .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 2 + .../netstandard2.0/PublicAPI.Unshipped.txt | 2 + ...lClientApplicationExtensibilityApiTests.cs | 69 +++++++++++++++++++ .../ClientCredentialWithCertTest.cs | 38 +++++++--- 10 files changed, 129 insertions(+), 16 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs b/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs index be47d7860c..d1d76fd81f 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs @@ -1,12 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Microsoft.Identity.Client.AppConfig { /// @@ -18,8 +12,17 @@ public class CertificateOptions /// Gets or sets a value indicating whether the X.509 certificate chain (x5c) should be included in the token /// request. /// - /// Set this property to to include X5C in the token request; + /// Set this property to to include X5C in the token request /// otherwise, set it to . public bool SendX5C { get; set; } = false; + + /// + /// Gets or sets a value indicating if the application tokens acquired from Azure AD are associated with the certificate serial number. + /// This property when set, allow you to associate the tokens acquired from Azure AD with the certificate serial number. + /// This can be used to partition the cache by certificate. Tokens acquired with one certificate will not be accessible to another certificate with a different serial number. + /// Set this property to to indicate that the tokens acquired from Azure AD are associated with the certificate serial number, + /// by default it is set to /> + /// + public bool AssociateTokensWithCertificateSerialNumber { get; set; } = false; } } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs index 8d7de24387..dd36ffc447 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -141,7 +142,7 @@ public ConfidentialClientApplicationBuilder WithCertificate(X509Certificate2 cer /// (either via portal or PowerShell/CLI operation). For details see https://aka.ms/msal-net-sni /// /// The X509 certificate used as credentials to prove the identity of the application to Azure AD. - /// Configuration options for certificate handling. + /// Configuration options for certificate handling. See for more information. /// You should use certificates with a private key size of at least 2048 bytes. Future versions of this library might reject certificates with smaller keys. public ConfidentialClientApplicationBuilder WithCertificate(X509Certificate2 certificate, CertificateOptions certificateOptions) { @@ -155,8 +156,14 @@ public ConfidentialClientApplicationBuilder WithCertificate(X509Certificate2 cer throw new MsalClientException(MsalError.CertWithoutPrivateKey, MsalErrorMessage.CertMustHavePrivateKey(nameof(certificate))); } + if (certificateOptions?.AssociateTokensWithCertificateSerialNumber ?? false) + { + Config.CertificateIdToAssociateWithToken = certificate.SerialNumber; + } + Config.ClientCredential = new CertificateClientCredential(certificate); Config.SendX5C = certificateOptions?.SendX5C ?? false; + return this; } 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 e472e3f717..634e84e125 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void 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 e472e3f717..634e84e125 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void 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 e472e3f717..634e84e125 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,4 +1,6 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void 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 e472e3f717..634e84e125 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,4 +1,6 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void 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 e472e3f717..634e84e125 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,4 +1,6 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void 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 e472e3f717..634e84e125 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,4 +1,6 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs index 570dc5fc89..7af1274840 100644 --- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs @@ -8,6 +8,7 @@ using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Client.Internal.ClientCredential; +using Microsoft.Identity.Client.RP; using Microsoft.Identity.Test.Common; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -165,6 +166,74 @@ public void WithCertificate_NullCertificateOptions_DefaultsToSendX5C_False() Assert.IsFalse(config.SendX5C, "SendX5C should default to false when CertificateOptions is null"); } + [TestMethod] + public void WithCertificate_CertificateOptions_AssociateTokensWithCertificateSerialNumber_True_IsStored() + { + // Arrange + var certificateOptions = new CertificateOptions { AssociateTokensWithCertificateSerialNumber = true }; + Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate()); + + // Act + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithCertificate(certificateProvider, certificateOptions) + .BuildConcrete(); + + // Assert + var config = app.AppConfig as ApplicationConfiguration; + Assert.IsNotNull(config); + Assert.IsTrue(certificateOptions.AssociateTokensWithCertificateSerialNumber, + "CertificateOptions.AssociateTokensWithCertificateSerialNumber should be true"); + } + + [TestMethod] + public void WithCertificate_CertificateOptions_AssociateTokensWithCertificateSerialNumber_False_IsStored() + { + // Arrange + var certificateOptions = new CertificateOptions { AssociateTokensWithCertificateSerialNumber = false }; + Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate()); + + // Act + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithCertificate(certificateProvider, certificateOptions) + .BuildConcrete(); + + // Assert + var config = app.AppConfig as ApplicationConfiguration; + Assert.IsNotNull(config); + Assert.IsFalse(certificateOptions.AssociateTokensWithCertificateSerialNumber, + "CertificateOptions.AssociateTokensWithCertificateSerialNumber should be false"); + } + + [TestMethod] + public void WithCertificate_CertificateOptions_BothPropertiesSet_AreStored() + { + // Arrange + var certificateOptions = new CertificateOptions + { + SendX5C = true, + AssociateTokensWithCertificateSerialNumber = true + }; + Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate()); + + // Act + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithExperimentalFeatures() + .WithCertificate(certificateProvider, certificateOptions) + .BuildConcrete(); + + // Assert + var config = app.AppConfig as ApplicationConfiguration; + Assert.IsNotNull(config); + Assert.IsTrue(config.SendX5C, "SendX5C should be true"); + Assert.IsTrue(certificateOptions.AssociateTokensWithCertificateSerialNumber, + "AssociateTokensWithCertificateSerialNumber should be true"); + } + #endregion #region OnMsalServiceFailure Tests diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs index 5e03718004..b2e845041b 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Shared; @@ -856,20 +857,38 @@ public async Task RopcCcaSendsX5CUsingRequestLevelAPIAsync(bool sendX5C) } } - [TestMethod] - public async Task EnsureCertificateSerialNumberIsAddedToCacheKeyTestAsync() + + [DataTestMethod] + [DataRow(true)] + [DataRow(false)] + public async Task EnsureCertificateSerialNumberIsAddedToCacheKeyTestAsync(bool useCertificateOptions) { using (var httpManager = new MockHttpManager()) { var certificate = CertHelper.GetOrCreateTestCert(); - var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true) .WithRedirectUri(TestConstants.RedirectUri) - .WithCertificate(certificate, true, true) .WithHttpManager(httpManager) - .WithExperimentalFeatures() - .BuildConcrete(); + .WithExperimentalFeatures(); + + if (useCertificateOptions) + { + CertificateOptions certificateOptions = new CertificateOptions() + { + SendX5C = true, + AssociateTokensWithCertificateSerialNumber = true + }; + + builder.WithCertificate(certificate, certificateOptions); + } + else + { + builder.WithCertificate(certificate, true, true); + } + + var app = builder.BuildConcrete(); app.AppTokenCache.SetBeforeAccess(BeforeCacheAccess); app.AppTokenCache.SetAfterAccess(AfterCacheAccess); @@ -956,8 +975,8 @@ public async Task EnsureCertificateSerialNumberIsAddedToCacheKeyTestAsync() } } - [TestMethod] - public async Task EnsureDefaultCacheKeyBehaviorWhenCertSerialNumberIsNotUsedTestAsync() + [DataTestMethod] + public async Task EnsureDefaultCacheKeyBehaviorWhenCertSerialNumberIsNotUsedTestAsync(bool useCertificateOptions) { using (var httpManager = new MockHttpManager()) { @@ -972,6 +991,9 @@ public async Task EnsureDefaultCacheKeyBehaviorWhenCertSerialNumberIsNotUsedTest .WithExperimentalFeatures() .BuildConcrete(); + + + app.AppTokenCache.SetBeforeAccess(BeforeCacheAccess); app.AppTokenCache.SetAfterAccess(AfterCacheAccess); From a2a4de0b914273d534222ff21954bf5ad3e7cf1f Mon Sep 17 00:00:00 2001 From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:06:20 -0800 Subject: [PATCH 4/8] Fix typo --- .../PublicApi/net8.0-android/PublicAPI.Shipped.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt index 052e7123f4..1f408bc861 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt @@ -25,7 +25,7 @@ const Microsoft.Identity.Client.MsalError.BrokerResponseHashMismatch = "broker_r const Microsoft.Identity.Client.MsalError.BrokerResponseReturnedError = "broker_response_returned_error" -> string const Microsoft.Identity.Client.MsalError.CannotAccessUserInformationOrUserNotDomainJoined = "user_information_access_failed" -> string const Microsoft.Identity.Client.MsalError.CannotInvokeBroker = "cannot_invoke_broker" -> string -const Microsoft.Identity.Client.MsalError.CertificateNotRsa = "certificaFwithcertificatete_not_rsa" -> string +const Microsoft.Identity.Client.MsalError.CertificateNotRsa = "certificate_not_rsa" const Microsoft.Identity.Client.MsalError.CertWithoutPrivateKey = "cert_without_private_key" -> string const Microsoft.Identity.Client.MsalError.ClientCredentialAuthenticationTypeMustBeDefined = "Client_Credentials_Required_In_Confidential_Client_Application" -> string const Microsoft.Identity.Client.MsalError.ClientCredentialAuthenticationTypesAreMutuallyExclusive = "Client_Credential_Authentication_Types_Are_Mutually_Exclusive" -> string @@ -1075,7 +1075,6 @@ Microsoft.Identity.Client.Extensibility.ExecutionResult.Result.get -> Microsoft. Microsoft.Identity.Client.Extensibility.ExecutionResult.Successful.get -> bool 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 Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.get -> string Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void From b12c1762a049c9ab890852ef04487f0c96eea92d Mon Sep 17 00:00:00 2001 From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:37:00 -0800 Subject: [PATCH 5/8] Fix typo --- .../PublicApi/net8.0-android/PublicAPI.Shipped.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt index 1f408bc861..497e3876d1 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt @@ -25,7 +25,7 @@ const Microsoft.Identity.Client.MsalError.BrokerResponseHashMismatch = "broker_r const Microsoft.Identity.Client.MsalError.BrokerResponseReturnedError = "broker_response_returned_error" -> string const Microsoft.Identity.Client.MsalError.CannotAccessUserInformationOrUserNotDomainJoined = "user_information_access_failed" -> string const Microsoft.Identity.Client.MsalError.CannotInvokeBroker = "cannot_invoke_broker" -> string -const Microsoft.Identity.Client.MsalError.CertificateNotRsa = "certificate_not_rsa" +const Microsoft.Identity.Client.MsalError.CertificateNotRsa = "certificate_not_rsa" -> string const Microsoft.Identity.Client.MsalError.CertWithoutPrivateKey = "cert_without_private_key" -> string const Microsoft.Identity.Client.MsalError.ClientCredentialAuthenticationTypeMustBeDefined = "Client_Credentials_Required_In_Confidential_Client_Application" -> string const Microsoft.Identity.Client.MsalError.ClientCredentialAuthenticationTypesAreMutuallyExclusive = "Client_Credential_Authentication_Types_Are_Mutually_Exclusive" -> string From 083560fbbea1a837861c836a68313cb0afb862b6 Mon Sep 17 00:00:00 2001 From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:56:33 -0800 Subject: [PATCH 6/8] Fix tests --- .../ConfidentialClientApplicationBuilderExtensions.cs | 4 ++++ .../PublicApiTests/ClientCredentialWithCertTest.cs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs index 159bca7b34..05c150b079 100644 --- a/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs +++ b/src/client/Microsoft.Identity.Client/Extensibility/ConfidentialClientApplicationBuilderExtensions.cs @@ -62,6 +62,8 @@ public static ConfidentialClientApplicationBuilder WithCertificate( Func> certificateProvider, CertificateOptions certificateOptions) { + builder.ValidateUseOfExperimentalFeature(); + if (certificateProvider == null) { throw new ArgumentNullException(nameof(certificateProvider)); @@ -122,6 +124,8 @@ public static ConfidentialClientApplicationBuilder OnMsalServiceFailure( this ConfidentialClientApplicationBuilder builder, Func> onMsalServiceFailure) { + builder.ValidateUseOfExperimentalFeature(); + if (onMsalServiceFailure == null) throw new ArgumentNullException(nameof(onMsalServiceFailure)); diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs index b2e845041b..0d377aefd7 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs @@ -976,6 +976,8 @@ public async Task EnsureCertificateSerialNumberIsAddedToCacheKeyTestAsync(bool u } [DataTestMethod] + [DataRow(true)] + [DataRow(false)] public async Task EnsureDefaultCacheKeyBehaviorWhenCertSerialNumberIsNotUsedTestAsync(bool useCertificateOptions) { using (var httpManager = new MockHttpManager()) From 9b12cb21cbb39d61d7a22da55e8f149b998d6486 Mon Sep 17 00:00:00 2001 From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:29:08 -0800 Subject: [PATCH 7/8] Address comments --- .../AppConfig/CertificateOptions.cs | 6 +++--- .../ConfidentialClientApplicationBuilder.cs | 2 +- .../IsExternalInit.cs | 11 +++++++++++ .../Microsoft.Identity.Client.csproj | 5 +++++ .../PublicApi/net462/PublicAPI.Unshipped.txt | 7 +++---- .../PublicApi/net472/PublicAPI.Unshipped.txt | 8 +++----- .../net8.0-android/PublicAPI.Unshipped.txt | 7 +++---- .../net8.0-ios/PublicAPI.Unshipped.txt | 7 +++---- .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 8 +++----- .../netstandard2.0/PublicAPI.Unshipped.txt | 7 +++---- ...alClientApplicationExtensibilityApiTests.cs | 18 +++++++++--------- .../ClientCredentialWithCertTest.cs | 2 +- 12 files changed, 48 insertions(+), 40 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/IsExternalInit.cs diff --git a/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs b/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs index d1d76fd81f..0993225bca 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs @@ -6,7 +6,7 @@ namespace Microsoft.Identity.Client.AppConfig /// /// Represents configuration options for certificate handling or management. /// - public class CertificateOptions + public record CertificateOptions { /// /// Gets or sets a value indicating whether the X.509 certificate chain (x5c) should be included in the token @@ -14,7 +14,7 @@ public class CertificateOptions /// /// Set this property to to include X5C in the token request /// otherwise, set it to . - public bool SendX5C { get; set; } = false; + public bool SendX5C { get; init; } = false; /// /// Gets or sets a value indicating if the application tokens acquired from Azure AD are associated with the certificate serial number. @@ -23,6 +23,6 @@ public class CertificateOptions /// Set this property to to indicate that the tokens acquired from Azure AD are associated with the certificate serial number, /// by default it is set to /> /// - public bool AssociateTokensWithCertificateSerialNumber { get; set; } = false; + public bool AssociateTokensWithCertificate { get; init; } = false; } } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs index dd36ffc447..212fecbfe1 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs @@ -156,7 +156,7 @@ public ConfidentialClientApplicationBuilder WithCertificate(X509Certificate2 cer throw new MsalClientException(MsalError.CertWithoutPrivateKey, MsalErrorMessage.CertMustHavePrivateKey(nameof(certificate))); } - if (certificateOptions?.AssociateTokensWithCertificateSerialNumber ?? false) + if (certificateOptions?.AssociateTokensWithCertificate ?? false) { Config.CertificateIdToAssociateWithToken = certificate.SerialNumber; } diff --git a/src/client/Microsoft.Identity.Client/IsExternalInit.cs b/src/client/Microsoft.Identity.Client/IsExternalInit.cs new file mode 100644 index 0000000000..ce2769ba9f --- /dev/null +++ b/src/client/Microsoft.Identity.Client/IsExternalInit.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if NETSTANDARD || NET462 || NET472 +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit + { + } +} +#endif diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index 7c08fe3762..e94f8735bb 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 @@ + @@ -162,4 +163,8 @@ + + + + \ No newline at end of file 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 a5874d495d..f3b0925c82 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1,9 +1,8 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void -Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.init -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.init -> void Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder 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 4d31c27150..a76e8c6337 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1,12 +1,10 @@ Microsoft.Identity.Client.ManagedIdentityPopExtensions Microsoft.Identity.Client.AppConfig.CertificateOptions -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void -Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.init -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.init -> void Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder -Microsoft.Identity.Client.ManagedIdentityPopExtensions static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAttributes(string attributeJson) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder 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 82b0d21146..44f8580444 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,9 +1,8 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void -Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.init -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.init -> void Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions 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 82b0d21146..44f8580444 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,9 +1,8 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void -Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.init -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.init -> void Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions 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 46cc13cc25..44f8580444 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,11 +1,9 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void -Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.init -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.init -> void Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder -Microsoft.Identity.Client.ManagedIdentityPopExtensions static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder 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 82b0d21146..44f8580444 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,9 +1,8 @@ Microsoft.Identity.Client.AppConfig.CertificateOptions -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificateSerialNumber.set -> void -Microsoft.Identity.Client.AppConfig.CertificateOptions.CertificateOptions() -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.get -> bool +Microsoft.Identity.Client.AppConfig.CertificateOptions.AssociateTokensWithCertificate.init -> void Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.get -> bool -Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.set -> void +Microsoft.Identity.Client.AppConfig.CertificateOptions.SendX5C.init -> void Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ManagedIdentityPopExtensions diff --git a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs index 7af1274840..e775df78d5 100644 --- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs @@ -170,7 +170,7 @@ public void WithCertificate_NullCertificateOptions_DefaultsToSendX5C_False() public void WithCertificate_CertificateOptions_AssociateTokensWithCertificateSerialNumber_True_IsStored() { // Arrange - var certificateOptions = new CertificateOptions { AssociateTokensWithCertificateSerialNumber = true }; + var certificateOptions = new CertificateOptions { AssociateTokensWithCertificate = true }; Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate()); // Act @@ -183,15 +183,15 @@ public void WithCertificate_CertificateOptions_AssociateTokensWithCertificateSer // Assert var config = app.AppConfig as ApplicationConfiguration; Assert.IsNotNull(config); - Assert.IsTrue(certificateOptions.AssociateTokensWithCertificateSerialNumber, - "CertificateOptions.AssociateTokensWithCertificateSerialNumber should be true"); + Assert.IsTrue(certificateOptions.AssociateTokensWithCertificate, + "CertificateOptions.AssociateTokensWithCertificate should be true"); } [TestMethod] public void WithCertificate_CertificateOptions_AssociateTokensWithCertificateSerialNumber_False_IsStored() { // Arrange - var certificateOptions = new CertificateOptions { AssociateTokensWithCertificateSerialNumber = false }; + var certificateOptions = new CertificateOptions { AssociateTokensWithCertificate = false }; Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate()); // Act @@ -204,8 +204,8 @@ public void WithCertificate_CertificateOptions_AssociateTokensWithCertificateSer // Assert var config = app.AppConfig as ApplicationConfiguration; Assert.IsNotNull(config); - Assert.IsFalse(certificateOptions.AssociateTokensWithCertificateSerialNumber, - "CertificateOptions.AssociateTokensWithCertificateSerialNumber should be false"); + Assert.IsFalse(certificateOptions.AssociateTokensWithCertificate, + "CertificateOptions.AssociateTokensWithCertificate should be false"); } [TestMethod] @@ -215,7 +215,7 @@ public void WithCertificate_CertificateOptions_BothPropertiesSet_AreStored() var certificateOptions = new CertificateOptions { SendX5C = true, - AssociateTokensWithCertificateSerialNumber = true + AssociateTokensWithCertificate = true }; Task certificateProvider(AssertionRequestOptions options) => Task.FromResult(GetTestCertificate()); @@ -230,8 +230,8 @@ public void WithCertificate_CertificateOptions_BothPropertiesSet_AreStored() var config = app.AppConfig as ApplicationConfiguration; Assert.IsNotNull(config); Assert.IsTrue(config.SendX5C, "SendX5C should be true"); - Assert.IsTrue(certificateOptions.AssociateTokensWithCertificateSerialNumber, - "AssociateTokensWithCertificateSerialNumber should be true"); + Assert.IsTrue(certificateOptions.AssociateTokensWithCertificate, + "AssociateTokensWithCertificate should be true"); } #endregion diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs index 0d377aefd7..dd4954e5a1 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs @@ -878,7 +878,7 @@ public async Task EnsureCertificateSerialNumberIsAddedToCacheKeyTestAsync(bool u CertificateOptions certificateOptions = new CertificateOptions() { SendX5C = true, - AssociateTokensWithCertificateSerialNumber = true + AssociateTokensWithCertificate = true }; builder.WithCertificate(certificate, certificateOptions); From 5e74da60caaf78a96ff3b6ce8d3e16dc80581898 Mon Sep 17 00:00:00 2001 From: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:43:49 -0800 Subject: [PATCH 8/8] Undo changes to csproj --- .../Microsoft.Identity.Client.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index e94f8735bb..7c08fe3762 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -80,7 +80,6 @@ - @@ -163,8 +162,4 @@ - - - - \ No newline at end of file