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