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..0993225bca
--- /dev/null
+++ b/src/client/Microsoft.Identity.Client/AppConfig/CertificateOptions.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Identity.Client.AppConfig
+{
+ ///
+ /// Represents configuration options for certificate handling or management.
+ ///
+ public record 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; init; } = 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 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 f724eacf93..fa4b88c6b6 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;
@@ -130,6 +131,42 @@ 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. 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)
+ {
+ if (certificate == null)
+ {
+ throw new ArgumentNullException(nameof(certificate));
+ }
+
+ if (!certificate.HasPrivateKey)
+ {
+ throw new MsalClientException(MsalError.CertWithoutPrivateKey, MsalErrorMessage.CertMustHavePrivateKey(nameof(certificate)));
+ }
+
+ if (certificateOptions?.AssociateTokensWithCertificate ?? false)
+ {
+ Config.CertificateIdToAssociateWithToken = certificate.SerialNumber;
+ }
+
+ 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..05c150b079 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,8 +59,11 @@ public static ConfidentialClientApplicationBuilder WithAppTokenProvider(
///
public static ConfidentialClientApplicationBuilder WithCertificate(
this ConfidentialClientApplicationBuilder builder,
- Func> certificateProvider)
+ Func> certificateProvider,
+ CertificateOptions certificateOptions)
{
+ builder.ValidateUseOfExperimentalFeature();
+
if (certificateProvider == null)
{
throw new ArgumentNullException(nameof(certificateProvider));
@@ -69,7 +73,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;
}
@@ -118,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/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/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 38101497be..e0db4ab2d2 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,11 @@
+Microsoft.Identity.Client.AppConfig.CertificateOptions
+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.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.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraClientAssertionClaims(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string clientAssertionClaims) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder
Microsoft.Identity.Client.ManagedIdentityPopExtensions
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
+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
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 38101497be..27bb35f952 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,11 @@
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraClientAssertionClaims(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string clientAssertionClaims) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder
Microsoft.Identity.Client.ManagedIdentityPopExtensions
+Microsoft.Identity.Client.AppConfig.CertificateOptions
+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.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
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.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Shipped.txt
index 2a0890082a..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
@@ -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
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 38101497be..2c4a88e00f 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,3 +1,10 @@
+Microsoft.Identity.Client.AppConfig.CertificateOptions
+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.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
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraClientAssertionClaims(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string clientAssertionClaims) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder
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/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 38101497be..2c4a88e00f 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,3 +1,10 @@
+Microsoft.Identity.Client.AppConfig.CertificateOptions
+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.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
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraClientAssertionClaims(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string clientAssertionClaims) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder
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/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 38101497be..2c4a88e00f 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,3 +1,10 @@
+Microsoft.Identity.Client.AppConfig.CertificateOptions
+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.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
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraClientAssertionClaims(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string clientAssertionClaims) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder
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.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 38101497be..2c4a88e00f 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,3 +1,10 @@
+Microsoft.Identity.Client.AppConfig.CertificateOptions
+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.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
static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension.WithExtraClientAssertionClaims(this Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder builder, string clientAssertionClaims) -> Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder
Microsoft.Identity.Client.ManagedIdentityPopExtensions
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..e775df78d5 100644
--- a/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
+++ b/tests/Microsoft.Identity.Test.Unit/AppConfigTests/ConfidentialClientApplicationExtensibilityApiTests.cs
@@ -5,8 +5,10 @@
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.Client.RP;
using Microsoft.Identity.Test.Common;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -17,6 +19,7 @@ namespace Microsoft.Identity.Test.Unit.AppConfigTests
public class ConfidentialClientApplicationExtensibilityApiTests
{
private X509Certificate2 _certificate;
+ private CertificateOptions _certificateOptions = new CertificateOptions();
[TestInitialize]
public void TestInitialize()
@@ -47,7 +50,7 @@ Task certificateProvider(AssertionRequestOptions options)
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
- .WithCertificate(certificateProvider)
+ .WithCertificate(certificateProvider, _certificateOptions)
.BuildConcrete();
// Assert
@@ -64,7 +67,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 +96,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 +107,133 @@ 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");
+ }
+
+ [TestMethod]
+ public void WithCertificate_CertificateOptions_AssociateTokensWithCertificateSerialNumber_True_IsStored()
+ {
+ // Arrange
+ var certificateOptions = new CertificateOptions { AssociateTokensWithCertificate = 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.AssociateTokensWithCertificate,
+ "CertificateOptions.AssociateTokensWithCertificate should be true");
+ }
+
+ [TestMethod]
+ public void WithCertificate_CertificateOptions_AssociateTokensWithCertificateSerialNumber_False_IsStored()
+ {
+ // Arrange
+ var certificateOptions = new CertificateOptions { AssociateTokensWithCertificate = 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.AssociateTokensWithCertificate,
+ "CertificateOptions.AssociateTokensWithCertificate should be false");
+ }
+
+ [TestMethod]
+ public void WithCertificate_CertificateOptions_BothPropertiesSet_AreStored()
+ {
+ // Arrange
+ var certificateOptions = new CertificateOptions
+ {
+ SendX5C = true,
+ AssociateTokensWithCertificate = 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.AssociateTokensWithCertificate,
+ "AssociateTokensWithCertificate should be true");
+ }
+
#endregion
#region OnMsalServiceFailure Tests
@@ -258,7 +388,7 @@ public void AllThreeExtensibilityPoints_CanBeConfiguredTogether()
var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
- .WithCertificate(certificateProvider)
+ .WithCertificate(certificateProvider, _certificateOptions)
.OnMsalServiceFailure(onMsalServiceFailure)
.OnCompletion(onSuccess)
.BuildConcrete();
@@ -284,7 +414,7 @@ public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
.WithExperimentalFeatures()
.OnCompletion(onSuccess)
.OnMsalServiceFailure(onMsalServiceFailure)
- .WithCertificate(certificateProvider)
+ .WithCertificate(certificateProvider, _certificateOptions)
.BuildConcrete();
// Act - Order: OnMsalServiceFailure, Certificate, OnCompletion
@@ -292,7 +422,7 @@ public void ExtensibilityPoints_CanBeConfiguredInAnyOrder()
.Create(TestConstants.ClientId)
.WithExperimentalFeatures()
.OnMsalServiceFailure(onMsalServiceFailure)
- .WithCertificate(certificateProvider)
+ .WithCertificate(certificateProvider, _certificateOptions)
.OnCompletion(onSuccess)
.BuildConcrete();
@@ -325,7 +455,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/ClientCredentialWithCertTest.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs
index 02a068d152..872287fbf9 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;
@@ -865,20 +866,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,
+ AssociateTokensWithCertificate = true
+ };
+
+ builder.WithCertificate(certificate, certificateOptions);
+ }
+ else
+ {
+ builder.WithCertificate(certificate, true, true);
+ }
+
+ var app = builder.BuildConcrete();
app.AppTokenCache.SetBeforeAccess(BeforeCacheAccess);
app.AppTokenCache.SetAfterAccess(AfterCacheAccess);
@@ -965,8 +984,10 @@ public async Task EnsureCertificateSerialNumberIsAddedToCacheKeyTestAsync()
}
}
- [TestMethod]
- public async Task EnsureDefaultCacheKeyBehaviorWhenCertSerialNumberIsNotUsedTestAsync()
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task EnsureDefaultCacheKeyBehaviorWhenCertSerialNumberIsNotUsedTestAsync(bool useCertificateOptions)
{
using (var httpManager = new MockHttpManager())
{
@@ -981,6 +1002,9 @@ public async Task EnsureDefaultCacheKeyBehaviorWhenCertSerialNumberIsNotUsedTest
.WithExperimentalFeatures()
.BuildConcrete();
+
+
+
app.AppTokenCache.SetBeforeAccess(BeforeCacheAccess);
app.AppTokenCache.SetAfterAccess(AfterCacheAccess);
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