From aef501f4b2381932469d6a7bab4d3cdba3641626 Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:32:30 -0800 Subject: [PATCH] Implementation for SNI + MTLS Flow in MSAL (#4965) * very very draft * address comments * pr comments * validations and tests * test app * add unit tests * pr comments * pr comments * Apply suggestions from code review code comments Co-authored-by: Den Delimarsky <53200638+localden@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Den Delimarsky <53200638+localden@users.noreply.github.com> * gov cloud * consolidate * test fix * temp * disable test * test fix * enhance * test fix * IgnoreOnClassicPipeline * test updates * set ClientCredential to null * more tests * test update * UnExpectedPostData * address comments * pr comments * merge conflicts * pr comments * pr comments * publicapi * NonStandardCloud Mtls test * tenantidfix * public api * fix public api --------- Co-authored-by: Gladwin Johnson Co-authored-by: Den Delimarsky <53200638+localden@users.noreply.github.com> --- ...ntialClientAcquireTokenParameterBuilder.cs | 34 +- .../AcquireTokenForClientParameterBuilder.cs | 55 ++ .../ApiConfig/Executors/AbstractExecutor.cs | 5 +- .../ClientApplicationBaseExecutor.cs | 4 +- .../Executors/ConfidentialClientExecutor.cs | 21 +- .../Executors/ManagedIdentityExecutor.cs | 2 +- .../Executors/PublicClientExecutor.cs | 8 +- .../AcquireTokenForClientParameters.cs | 1 + .../AppConfig/ApplicationConfiguration.cs | 3 +- .../AppConfig/AuthorityInfo.cs | 34 +- .../ConfidentialClientApplicationBuilder.cs | 2 +- .../PoP/MtlsPopAuthenticationOperation.cs | 59 ++ .../SSHCertAuthenticationOperation.cs | 2 +- .../AuthenticationResult.cs | 2 +- .../ClientApplicationBase.cs | 11 +- .../ConfidentialClientApplication.cs | 2 +- .../Discovery/InstanceDiscoveryManager.cs | 2 +- .../Discovery/KnownMetadataProvider.cs | 2 +- ...r.cs => RegionAndMtlsDiscoveryProvider.cs} | 37 +- .../CertificateAndClaimsClientCredential.cs | 35 +- .../ClientCredential/IClientCredential.cs | 6 +- .../SecretStringClientCredential.cs | 8 +- .../SignedAssertionClientCredential.cs | 8 +- ...SignedAssertionDelegateClientCredential.cs | 10 +- .../Internal/Constants.cs | 5 +- .../Internal/RequestContext.cs | 8 +- .../AuthenticationRequestParameters.cs | 2 +- .../Requests/ClientCredentialRequest.cs | 1 + .../Internal/TelemetryTokenTypeConstants.cs | 2 + .../Microsoft.Identity.Client/MsalError.cs | 27 + .../MsalErrorMessage.cs | 7 + .../OAuth2/TokenClient.cs | 6 +- .../PublicApi/net462/PublicAPI.Unshipped.txt | 6 + .../PublicApi/net472/PublicAPI.Unshipped.txt | 6 + .../net8.0-android/PublicAPI.Unshipped.txt | 6 + .../net8.0-ios/PublicAPI.Unshipped.txt | 6 + .../PublicApi/net8.0/PublicAPI.Unshipped.txt | 6 + .../netstandard2.0/PublicAPI.Unshipped.txt | 6 + .../Core/Helpers/HttpSnifferClientFactory.cs | 17 +- .../Core/Mocks/MockHttpAndServiceBundle.cs | 2 +- .../Core/Mocks/MockHttpMessageHandler.cs | 23 + .../TestCommon.cs | 2 +- .../ClientCredentialsMtlsPopTests.cs | 69 ++ .../ClientCredentialsTests.NetFwk.cs | 1 - .../InstanceDiscoveryIntegrationTests.cs | 2 +- .../HeadlessTests/PoPTests.NetFwk.cs | 42 +- .../Infrastructure/MsalExtensions.cs | 2 +- .../ApiConfigTests/AuthorityTests.cs | 3 +- .../BrokerTests/RuntimeBrokerTests.cs | 2 +- .../CacheTests/CacheKeyFactoryTests.cs | 12 +- .../CacheTests/TokenCacheTests.cs | 10 +- .../CacheTests/UnifiedCacheTests.cs | 2 +- .../InstanceTests/AadAuthorityTests.cs | 6 +- .../InstanceTests/B2cAuthorityTests.cs | 3 +- .../InstanceTests/DstsAuthorityTests.cs | 3 +- .../InstanceDiscoveryManagerTests.cs | 8 +- .../OAuth2Tests/TokenResponseTests.cs | 2 +- .../CoreTests/RegionDiscoveryProviderTests.cs | 21 +- .../CoreTests/WsTrustTests/MexParserTests.cs | 2 +- .../CoreTests/WsTrustTests/WsTrustTests.cs | 8 +- .../ExperimentalFeatureTests.cs | 9 +- .../ServiceFabricTests.cs | 2 +- .../OAuthClientTests.cs | 2 +- .../ClientCredentialWithRegionTests.cs | 40 ++ .../PublicApiTests/MtlsPopTests.cs | 672 ++++++++++++++++++ .../RequestsTests/SilentRequestTests.cs | 2 +- .../TelemetryTests/HttpTelemetryTests.cs | 2 +- .../WebUITests/DefaultOsBrowserWebUiTests.cs | 6 +- .../WebUITests/NetCoreWebUIFactoryTests.cs | 2 +- .../WebUITests/NetDesktopWebUIFactoryTests.cs | 2 +- .../WebUITests/WebView2WebUiFactoryTests.cs | 6 +- .../pop/PoPTests.cs | 20 +- .../pop/PopAuthenticationOperationTests.cs | 7 +- tests/devapps/NetCoreTestApp/Program.cs | 73 +- 74 files changed, 1325 insertions(+), 209 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs rename src/client/Microsoft.Identity.Client/Instance/Discovery/{RegionDiscoveryProvider.cs => RegionAndMtlsDiscoveryProvider.cs} (63%) create mode 100644 tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs create mode 100644 tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs index e27c606c10..5ae00be31e 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs @@ -2,12 +2,9 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -using Microsoft.Identity.Client.ApiConfig; using Microsoft.Identity.Client.ApiConfig.Executors; using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.AuthScheme.PoP; @@ -47,11 +44,11 @@ public override Task ExecuteAsync(CancellationToken cancel /// /// protected override void Validate() - { + { // Confidential client must have a credential if (ServiceBundle?.Config.ClientCredential == null && CommonParameters.OnBeforeTokenRequestHandler == null && - ServiceBundle?.Config.AppTokenProvider == null + ServiceBundle?.Config.AppTokenProvider == null ) { throw new MsalClientException( @@ -73,12 +70,14 @@ protected override void Validate() /// The builder. /// /// - /// An Authentication header is automatically added to the request. /// The PoP token is bound to the HTTP request, more specifically to the HTTP method (GET, POST, etc.) and to the Uri (path and query, but not query parameters). /// MSAL creates, reads and stores a key in memory that will be cycled every 8 hours. /// This is an experimental API. The method signature may change in the future without involving a major version upgrade. /// /// + [EditorBrowsable(EditorBrowsableState.Never)] // Soft deprecate + [Obsolete("WithProofOfPossession is deprecated. Use WithSignedHttpRequestProofOfPossession for SHR Proof-of-Possession functionality. " + + "For more details and to learn about other Proof-of-Possession MSAL supports, see the MSAL documentation: https://aka.ms/msal-net-pop")] public T WithProofOfPossession(PoPAuthenticationConfiguration popAuthenticationConfiguration) { ValidateUseOfExperimentalFeature(); @@ -89,5 +88,28 @@ public T WithProofOfPossession(PoPAuthenticationConfiguration popAuthenticationC return this as T; } + + /// + /// Modifies the request to acquire a Signed HTTP Request (SHR) Proof-of-Possession (PoP) token, rather than a Bearer. + /// SHR PoP tokens are bound to the HTTP request and to a cryptographic key, which MSAL manages on Windows. + /// SHR PoP tokens are different from mTLS PoP tokens, which are used for Mutual TLS (mTLS) authentication. See for details. + /// + /// Configuration properties used to construct a Proof-of-Possession request. + /// The builder. + /// + /// + /// The SHR PoP token is bound to the HTTP request, specifically to the HTTP method (for example, `GET` or `POST`) and to the URI path and query, excluding query parameters. + /// MSAL creates, reads, and stores a key in memory that will be cycled every 8 hours. + /// This is an experimental API. The method signature may change in the future without involving a major version upgrade. + /// + /// + public T WithSignedHttpRequestProofOfPossession(PoPAuthenticationConfiguration popAuthenticationConfiguration) + { + CommonParameters.PopAuthenticationConfiguration = popAuthenticationConfiguration ?? throw new ArgumentNullException(nameof(popAuthenticationConfiguration)); + + CommonParameters.AuthenticationOperation = new PopAuthenticationOperation(CommonParameters.PopAuthenticationConfiguration, ServiceBundle); + + return this as T; + } } } diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs index 4135347f05..b5ced441ad 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs @@ -4,10 +4,14 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.ApiConfig.Executors; using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.AuthScheme.PoP; +using Microsoft.Identity.Client.Internal; +using Microsoft.Identity.Client.Internal.ClientCredential; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; using Microsoft.Identity.Client.Utils; @@ -74,6 +78,30 @@ public AcquireTokenForClientParameterBuilder WithSendX5C(bool withSendX5C) return this; } + /// + /// Specifies that the certificate provided will be used for PoP tokens with mTLS (Mutual TLS) authentication. + /// For more information, refer to the Proof-of-Possession documentation. + /// + /// The current instance of to enable method chaining. + public AcquireTokenForClientParameterBuilder WithMtlsProofOfPossession() + { + ValidateUseOfExperimentalFeature(); + + if (ServiceBundle.Config.ClientCredential is not CertificateClientCredential certificateCredential) + { + throw new MsalClientException( + MsalError.MtlsCertificateNotProvided, + MsalErrorMessage.MtlsCertificateNotProvidedMessage); + } + else + { + CommonParameters.AuthenticationOperation = new MtlsPopAuthenticationOperation(certificateCredential.Certificate); + CommonParameters.MtlsCertificate = certificateCredential.Certificate; + } + + return this; + } + /// /// Please use WithAzureRegion on the ConfidentialClientApplicationBuilder object /// @@ -103,7 +131,34 @@ internal override Task ExecuteInternalAsync(CancellationTo /// protected override void Validate() { + if (CommonParameters.MtlsCertificate != null) + { + string authorityUri = ServiceBundle.Config.Authority.AuthorityInfo.CanonicalAuthority.AbsoluteUri; + + if (ServiceBundle.Config.Authority.AuthorityInfo.AuthorityType != AuthorityType.Aad) + { + throw new MsalClientException( + MsalError.InvalidAuthorityType, + MsalErrorMessage.MtlsInvalidAuthorityTypeMessage); + } + + if (authorityUri.Contains("/common", StringComparison.OrdinalIgnoreCase)) + { + throw new MsalClientException( + MsalError.MissingTenantedAuthority, + MsalErrorMessage.MtlsNonTenantedAuthorityNotAllowedMessage); + } + + if (string.IsNullOrEmpty(ServiceBundle.Config.AzureRegion)) + { + throw new MsalClientException( + MsalError.MtlsPopWithoutRegion, + MsalErrorMessage.MtlsPopWithoutRegion); + } + } + base.Validate(); + if (Parameters.SendX5C == null) { Parameters.SendX5C = this.ServiceBundle.Config.SendX5C; diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/AbstractExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/AbstractExecutor.cs index b16d52e5cf..0d775b031f 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/AbstractExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/AbstractExecutor.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.Security.Cryptography.X509Certificates; using System.Threading; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Internal; @@ -19,9 +20,9 @@ protected AbstractExecutor(IServiceBundle serviceBundle) public IServiceBundle ServiceBundle { get; } - protected RequestContext CreateRequestContextAndLogVersionInfo(Guid correlationId, CancellationToken userCancellationToken = default) + protected RequestContext CreateRequestContextAndLogVersionInfo(Guid correlationId, X509Certificate2 mtlsCertificate, CancellationToken userCancellationToken = default) { - var requestContext = new RequestContext(ServiceBundle, correlationId, userCancellationToken); + var requestContext = new RequestContext(ServiceBundle, correlationId, mtlsCertificate, userCancellationToken); requestContext.Logger.Info( () => string.Format( diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ClientApplicationBaseExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ClientApplicationBaseExecutor.cs index 2b3c43af02..383244480c 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ClientApplicationBaseExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ClientApplicationBaseExecutor.cs @@ -28,7 +28,7 @@ public async Task ExecuteAsync( AcquireTokenSilentParameters silentParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); var requestParameters = await _clientApplicationBase.CreateRequestParametersAsync( commonParameters, @@ -46,7 +46,7 @@ public async Task ExecuteAsync( AcquireTokenByRefreshTokenParameters refreshTokenParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); if (commonParameters.Scopes == null || !commonParameters.Scopes.Any()) { commonParameters.Scopes = new SortedSet diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs index 6ae2e3858d..7c0d7fe342 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.ApiConfig.Parameters; +using Microsoft.Identity.Client.AuthScheme.PoP; using Microsoft.Identity.Client.Instance.Discovery; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Internal.Requests; @@ -33,9 +34,9 @@ public async Task ExecuteAsync( AcquireTokenByAuthorizationCodeParameters authorizationCodeParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + RequestContext requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); - var requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( + AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, _confidentialClientApplication.UserTokenCacheInternal).ConfigureAwait(false); @@ -54,9 +55,9 @@ public async Task ExecuteAsync( AcquireTokenForClientParameters clientParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + RequestContext requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); - var requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( + AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, _confidentialClientApplication.AppTokenCacheInternal).ConfigureAwait(false); @@ -76,9 +77,9 @@ public async Task ExecuteAsync( AcquireTokenOnBehalfOfParameters onBehalfOfParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + RequestContext requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); - var requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( + AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, _confidentialClientApplication.UserTokenCacheInternal).ConfigureAwait(false); @@ -100,9 +101,9 @@ public async Task ExecuteAsync( GetAuthorizationRequestUrlParameters authorizationRequestUrlParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + RequestContext requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); - var requestParameters = await _confidentialClientApplication.CreateRequestParametersAsync( + AuthenticationRequestParameters requestParameters = await _confidentialClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, _confidentialClientApplication.UserTokenCacheInternal).ConfigureAwait(false); @@ -136,9 +137,9 @@ public async Task ExecuteAsync( AcquireTokenByUsernamePasswordParameters usernamePasswordParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + RequestContext requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); - var requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( + AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync( commonParameters, requestContext, _confidentialClientApplication.UserTokenCacheInternal).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs index 87cbbdc278..6b56a13a0b 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/ManagedIdentityExecutor.cs @@ -33,7 +33,7 @@ public async Task ExecuteAsync( AcquireTokenForManagedIdentityParameters managedIdentityParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); var requestParams = await _managedIdentityApplication.CreateRequestParametersAsync( commonParameters, diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/PublicClientExecutor.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/PublicClientExecutor.cs index 0fa7d21dcd..f839fd1fc7 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Executors/PublicClientExecutor.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Executors/PublicClientExecutor.cs @@ -26,7 +26,7 @@ public async Task ExecuteAsync( AcquireTokenInteractiveParameters interactiveParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); AuthenticationRequestParameters requestParams = await _publicClientApplication.CreateRequestParametersAsync( commonParameters, @@ -47,7 +47,7 @@ public async Task ExecuteAsync( AcquireTokenWithDeviceCodeParameters deviceCodeParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); var requestParams = await _publicClientApplication.CreateRequestParametersAsync( commonParameters, @@ -67,7 +67,7 @@ public async Task ExecuteAsync( AcquireTokenByIntegratedWindowsAuthParameters integratedWindowsAuthParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); var requestParams = await _publicClientApplication.CreateRequestParametersAsync( commonParameters, @@ -87,7 +87,7 @@ public async Task ExecuteAsync( AcquireTokenByUsernamePasswordParameters usernamePasswordParameters, CancellationToken cancellationToken) { - var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, cancellationToken); + var requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken); var requestParams = await _publicClientApplication.CreateRequestParametersAsync( commonParameters, diff --git a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForClientParameters.cs b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForClientParameters.cs index be7cfa4b21..685dd9b4b6 100644 --- a/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForClientParameters.cs +++ b/src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenForClientParameters.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Security.Cryptography.X509Certificates; using System.Text; using Microsoft.Identity.Client.Core; diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs index 95a181a4e4..91c89cffc5 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs @@ -124,8 +124,9 @@ public string ClientVersion public Func> AppTokenProvider; -#region ClientCredentials + #region ClientCredentials + // Indicates if claims or assertions are used within the configuration public IClientCredential ClientCredential { get; internal set; } /// diff --git a/src/client/Microsoft.Identity.Client/AppConfig/AuthorityInfo.cs b/src/client/Microsoft.Identity.Client/AppConfig/AuthorityInfo.cs index 4bd3af2778..608ff406f7 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/AuthorityInfo.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/AuthorityInfo.cs @@ -43,7 +43,7 @@ public AuthorityInfo( case AuthorityType.Generic: CanonicalAuthority = authorityUri; break; - + case AuthorityType.B2C: string[] pathSegments = GetPathSegments(authorityUri.AbsolutePath); @@ -99,7 +99,7 @@ private AuthorityInfo( CanonicalAuthority = canonicalAuthority; AuthorityType = authorityType; UserRealmUriPrefix = userRealmUriPrefix; - ValidateAuthority = validateAuthority; + ValidateAuthority = validateAuthority; } public string Host => CanonicalAuthority.Host; @@ -126,10 +126,10 @@ private AuthorityInfo( /// B2C doesn't allow multi-tenancy scenarios, but the authority itself is tenanted. /// For CIAM, we allow multi-tenancy scenarios, and expect the STS to fail. /// - internal bool CanBeTenanted => - AuthorityType == AuthorityType.Aad || + internal bool CanBeTenanted => + AuthorityType == AuthorityType.Aad || AuthorityType == AuthorityType.Dsts || - AuthorityType == AuthorityType.B2C || + AuthorityType == AuthorityType.B2C || AuthorityType == AuthorityType.Ciam; internal bool IsClientInfoSupported => @@ -327,12 +327,12 @@ internal Authority CreateAuthority() case AuthorityType.Dsts: return new DstsAuthority(this); - case AuthorityType.Ciam: + case AuthorityType.Ciam: return new CiamAuthority(this); case AuthorityType.Generic: return new GenericAuthority(this); - + default: throw new MsalClientException( MsalError.InvalidAuthorityType, @@ -477,7 +477,7 @@ public static IAuthorityValidator CreateAuthorityValidator(AuthorityInfo authori case AuthorityType.B2C: case AuthorityType.Dsts: case AuthorityType.Ciam: - case AuthorityType.Generic: + case AuthorityType.Generic: return new NullAuthorityValidator(); default: throw new InvalidOperationException("Invalid AuthorityType"); @@ -549,11 +549,11 @@ public static async Task CreateAuthorityForRequestAsync(RequestContex return updateEnvironment ? CreateAuthorityWithTenant( CreateAuthorityWithEnvironment(configAuthorityInfo, account.Environment), - account?.HomeAccountId?.TenantId, + account?.HomeAccountId?.TenantId, forceSpecifiedTenant: false) : CreateAuthorityWithTenant( - configAuthority, - account?.HomeAccountId?.TenantId, + configAuthority, + account?.HomeAccountId?.TenantId, forceSpecifiedTenant: false); } @@ -567,7 +567,7 @@ public static async Task CreateAuthorityForRequestAsync(RequestContex var requestAuthority = updateEnvironment ? new AadAuthority(CreateAuthorityWithEnvironment(requestAuthorityInfo, account?.Environment).AuthorityInfo) : new AadAuthority(requestAuthorityInfo); - if (!requestAuthority.IsCommonOrganizationsOrConsumersTenant() || + if (!requestAuthority.IsCommonOrganizationsOrConsumersTenant() || requestAuthority.IsOrganizationsTenantWithMsaPassthroughEnabled(requestContext.ServiceBundle.Config.IsBrokerEnabled && requestContext.ServiceBundle.Config.BrokerOptions != null && requestContext.ServiceBundle.Config.BrokerOptions.MsaPassthrough, account?.HomeAccountId?.TenantId)) { return requestAuthority; @@ -576,13 +576,13 @@ public static async Task CreateAuthorityForRequestAsync(RequestContex return updateEnvironment ? CreateAuthorityWithTenant( CreateAuthorityWithEnvironment(configAuthorityInfo, account.Environment), - account?.HomeAccountId?.TenantId, + account?.HomeAccountId?.TenantId, forceSpecifiedTenant: false) : CreateAuthorityWithTenant( - configAuthority, + configAuthority, account?.HomeAccountId?.TenantId, forceSpecifiedTenant: false); - + default: throw new MsalClientException( MsalError.InvalidAuthorityType, @@ -591,7 +591,7 @@ public static async Task CreateAuthorityForRequestAsync(RequestContex } internal static Authority CreateAuthorityWithTenant(Authority authority, string tenantId, bool forceSpecifiedTenant) - { + { string tenantedAuthority = authority.GetTenantedAuthority(tenantId, forceSpecifiedTenant); return Authority.CreateAuthority(tenantedAuthority, authority.AuthorityInfo.ValidateAuthority); @@ -635,7 +635,7 @@ private static async Task ValidateSameHostAsync(AuthorityInfo requestAuthorityIn } // Do not try to be smart here, let the STS figure it out - if ( requestAuthorityInfo.AuthorityType == AuthorityType.Ciam || + if (requestAuthorityInfo.AuthorityType == AuthorityType.Ciam || requestAuthorityInfo.AuthorityType == AuthorityType.Generic) { return; diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs index 78606f8282..b8dc43b488 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs @@ -208,7 +208,7 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(string signedCli throw new ArgumentNullException(nameof(signedClientAssertion)); } - Config.ClientCredential = new SignedAssertionClientCredential(signedClientAssertion); + Config.ClientCredential = new SignedAssertionClientCredential(signedClientAssertion); return this; } diff --git a/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs b/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs new file mode 100644 index 0000000000..a5533ce2f6 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Identity.Client.Internal; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.Utils; + +namespace Microsoft.Identity.Client.AuthScheme.PoP +{ + internal class MtlsPopAuthenticationOperation : IAuthenticationOperation + { + private readonly X509Certificate2 _mtlsCert; + + public MtlsPopAuthenticationOperation(X509Certificate2 mtlsCert) + { + _mtlsCert = mtlsCert; + KeyId = ComputeX5tS256KeyId(_mtlsCert); + } + + public int TelemetryTokenType => TelemetryTokenTypeConstants.MtlsPop; + + public string AuthorizationHeaderPrefix => Constants.MtlsPoPAuthHeaderPrefix; + + public string AccessTokenType => Constants.MtlsPoPTokenType; + + public string KeyId { get; } + + public IReadOnlyDictionary GetTokenRequestParams() + { + return new Dictionary + { + { OAuth2Parameter.TokenType, Constants.MtlsPoPTokenType } + }; + } + + public void FormatResult(AuthenticationResult authenticationResult) + { + //no-op + } + + private static string ComputeX5tS256KeyId(X509Certificate2 certificate) + { + // Extract the raw bytes of the certificate’s public key. + var publicKey = certificate.GetPublicKey(); + + // Compute the SHA-256 hash of the public key. + using (var sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(publicKey); + + // Return the hash encoded in Base64 URL format. + return Base64UrlHelpers.Encode(hash); + } + } + } +} diff --git a/src/client/Microsoft.Identity.Client/AuthScheme/SSHCertificates/SSHCertAuthenticationOperation.cs b/src/client/Microsoft.Identity.Client/AuthScheme/SSHCertificates/SSHCertAuthenticationOperation.cs index 51f9c4ea11..cc64c2db61 100644 --- a/src/client/Microsoft.Identity.Client/AuthScheme/SSHCertificates/SSHCertAuthenticationOperation.cs +++ b/src/client/Microsoft.Identity.Client/AuthScheme/SSHCertificates/SSHCertAuthenticationOperation.cs @@ -31,7 +31,7 @@ public SSHCertAuthenticationOperation(string keyId, string jwk) } public int TelemetryTokenType => TelemetryTokenTypeConstants.SshCert; - + public string AuthorizationHeaderPrefix => throw new MsalClientException( MsalError.SSHCertUsedAsHttpHeader, diff --git a/src/client/Microsoft.Identity.Client/AuthenticationResult.cs b/src/client/Microsoft.Identity.Client/AuthenticationResult.cs index edd1623876..c75c921375 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationResult.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationResult.cs @@ -54,7 +54,7 @@ public partial class AuthenticationResult IEnumerable scopes, Guid correlationId, string tokenType = "Bearer", - AuthenticationResultMetadata authenticationResultMetadata = null, + AuthenticationResultMetadata authenticationResultMetadata = null, ClaimsPrincipal claimsPrincipal = null, string spaAuthCode = null, IReadOnlyDictionary additionalResponseParameters = null) diff --git a/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs b/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs index 36f590c332..b66188ae6f 100644 --- a/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs +++ b/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.ApiConfig.Executors; @@ -135,7 +136,7 @@ public Task RemoveAsync(IAccount account) public async Task RemoveAsync(IAccount account, CancellationToken cancellationToken = default) { Guid correlationId = Guid.NewGuid(); - RequestContext requestContext = CreateRequestContext(correlationId, cancellationToken); + RequestContext requestContext = CreateRequestContext(correlationId, null, cancellationToken); requestContext.ApiEvent = new ApiEvent(correlationId); requestContext.ApiEvent.ApiId = ApiIds.RemoveAccount; @@ -170,7 +171,7 @@ public async Task RemoveAsync(IAccount account, CancellationToken cancellationTo private async Task> GetAccountsInternalAsync(ApiIds apiId, string homeAccountIdFilter, CancellationToken cancellationToken) { Guid correlationId = Guid.NewGuid(); - RequestContext requestContext = CreateRequestContext(correlationId, cancellationToken); + RequestContext requestContext = CreateRequestContext(correlationId, null, cancellationToken); if (requestContext.Logger.IsLoggingEnabled(LogLevel.Info)) { @@ -259,7 +260,7 @@ private async Task> FilterBrokerAccountsByEnvAsync(IEnumer var instanceMetadata = await ServiceBundle.InstanceDiscoveryManager.GetMetadataEntryTryAvoidNetworkAsync( AuthorityInfo, allEnvs, - CreateRequestContext(Guid.NewGuid(), cancellationToken)).ConfigureAwait(false); + CreateRequestContext(Guid.NewGuid(), null, cancellationToken)).ConfigureAwait(false); brokerAccounts = brokerAccounts.Where(acc => instanceMetadata.Aliases.ContainsOrdinalIgnoreCase(acc.Environment)); @@ -294,9 +295,9 @@ private IEnumerable MergeAccounts( // This implementation should ONLY be called for cases where we aren't participating in // MATS telemetry but still need a requestcontext/logger, such as "GetAccounts()". // For service calls, the request context should be created in the **Executor classes as part of request execution. - internal RequestContext CreateRequestContext(Guid correlationId, CancellationToken cancellationToken) + internal RequestContext CreateRequestContext(Guid correlationId, X509Certificate2 mtlsCertificate, CancellationToken cancellationToken) { - return new RequestContext(ServiceBundle, correlationId, cancellationToken); + return new RequestContext(ServiceBundle, correlationId, mtlsCertificate, cancellationToken); } #endregion diff --git a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs index 2a547fb079..2443a865a7 100644 --- a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs +++ b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs @@ -138,7 +138,7 @@ public async Task StopLongRunningProcessInWebApiAsync(string longRunningPr } Guid correlationId = Guid.NewGuid(); - RequestContext requestContext = base.CreateRequestContext(correlationId, cancellationToken); + RequestContext requestContext = base.CreateRequestContext(correlationId, null, cancellationToken); requestContext.ApiEvent = new ApiEvent(correlationId); requestContext.ApiEvent.ApiId = ApiIds.RemoveOboTokens; diff --git a/src/client/Microsoft.Identity.Client/Instance/Discovery/InstanceDiscoveryManager.cs b/src/client/Microsoft.Identity.Client/Instance/Discovery/InstanceDiscoveryManager.cs index b14d0e7def..8dfd3603cd 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Discovery/InstanceDiscoveryManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Discovery/InstanceDiscoveryManager.cs @@ -72,7 +72,7 @@ public InstanceDiscoveryManager( userProvidedInstanceDiscoveryUri); _regionDiscoveryProvider = regionDiscoveryProvider ?? - new RegionDiscoveryProvider(_httpManager, shouldClearCaches); + new RegionAndMtlsDiscoveryProvider(_httpManager, shouldClearCaches); if (shouldClearCaches) { diff --git a/src/client/Microsoft.Identity.Client/Instance/Discovery/KnownMetadataProvider.cs b/src/client/Microsoft.Identity.Client/Instance/Discovery/KnownMetadataProvider.cs index 29fe6359a0..f28fda5356 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Discovery/KnownMetadataProvider.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Discovery/KnownMetadataProvider.cs @@ -108,7 +108,7 @@ public InstanceDiscoveryMetadataEntry GetMetadata( if (canUseProvider) { s_knownEntries.TryGetValue(environment, out InstanceDiscoveryMetadataEntry entry); - logger.Verbose(()=>$"[Instance Discovery] Tried to use known metadata provider for {environment}. Success? {entry != null}. "); + logger.Verbose(() => $"[Instance Discovery] Tried to use known metadata provider for {environment}. Success? {entry != null}. "); return entry; } diff --git a/src/client/Microsoft.Identity.Client/Instance/Discovery/RegionDiscoveryProvider.cs b/src/client/Microsoft.Identity.Client/Instance/Discovery/RegionAndMtlsDiscoveryProvider.cs similarity index 63% rename from src/client/Microsoft.Identity.Client/Instance/Discovery/RegionDiscoveryProvider.cs rename to src/client/Microsoft.Identity.Client/Instance/Discovery/RegionAndMtlsDiscoveryProvider.cs index b77a52d060..dd3dfb2e66 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Discovery/RegionDiscoveryProvider.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Discovery/RegionAndMtlsDiscoveryProvider.cs @@ -10,12 +10,13 @@ namespace Microsoft.Identity.Client.Region { - internal class RegionDiscoveryProvider : IRegionDiscoveryProvider + internal class RegionAndMtlsDiscoveryProvider : IRegionDiscoveryProvider { private readonly IRegionManager _regionManager; public const string PublicEnvForRegional = "login.microsoft.com"; + public const string PublicEnvForRegionalMtlsAuth = "mtlsauth.microsoft.com"; - public RegionDiscoveryProvider(IHttpManager httpManager, bool clearCache) + public RegionAndMtlsDiscoveryProvider(IHttpManager httpManager, bool clearCache) { _regionManager = new RegionManager(httpManager, shouldClearStaticCache: clearCache); } @@ -23,6 +24,8 @@ public RegionDiscoveryProvider(IHttpManager httpManager, bool clearCache) public async Task GetMetadataAsync(Uri authority, RequestContext requestContext) { string region = null; + bool isMtlsEnabled = requestContext.MtlsCertificate != null; + if (requestContext.ApiEvent?.ApiId == TelemetryCore.Internal.Events.ApiEvent.ApiIds.AcquireTokenForClient) { region = await _regionManager.GetAzureRegionAsync(requestContext).ConfigureAwait(false); @@ -30,6 +33,15 @@ public async Task GetMetadataAsync(Uri authority if (string.IsNullOrEmpty(region)) { + if (isMtlsEnabled) + { + requestContext.Logger.Info("[Region discovery] Region discovery failed during mTLS Pop. "); + + throw new MsalServiceException( + MsalError.RegionRequiredForMtlsPop, + MsalErrorMessage.RegionRequiredForMtlsPopMessage); + } + requestContext.Logger.Info("[Region discovery] Not using a regional authority. "); return null; } @@ -61,8 +73,16 @@ private static string GetRegionalizedEnvironment(Uri authority, string region, R if (KnownMetadataProvider.IsPublicEnvironment(host)) { - requestContext.Logger.Info(() => $"[Region discovery] Regionalized Environment is : {region}.{PublicEnvForRegional}. "); - return $"{region}.{PublicEnvForRegional}"; + if (requestContext.MtlsCertificate != null) + { + requestContext.Logger.Info(() => $"[Region discovery] Using MTLS regional environment: {region}.{PublicEnvForRegionalMtlsAuth}"); + return $"{region}.{PublicEnvForRegionalMtlsAuth}"; + } + else + { + requestContext.Logger.Info(() => $"[Region discovery] Regionalized Environment is : {region}.{PublicEnvForRegional}. "); + return $"{region}.{PublicEnvForRegional}"; + } } // Regional business rule - use the PreferredNetwork value for public and sovereign clouds @@ -72,6 +92,15 @@ private static string GetRegionalizedEnvironment(Uri authority, string region, R host = preferredNetworkEnv; } + if (requestContext.MtlsCertificate != null) + { + // Modify the host to replace "login" with "mtlsauth" for mTLS scenarios + if (host.StartsWith("login")) + { + host = "mtlsauth" + host.Substring("login".Length); + } + } + requestContext.Logger.Info(() => $"[Region discovery] Regionalized Environment is : {region}.{host}. "); return $"{region}.{host}"; } diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs index 961265299a..035571f7f2 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Internal.Requests; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; using Microsoft.Identity.Client.TelemetryCore; @@ -38,26 +39,40 @@ public CertificateAndClaimsClientCredential( } public Task AddConfidentialClientParametersAsync( - OAuth2Client oAuth2Client, - ILoggerAdapter logger, + OAuth2Client oAuth2Client, + AuthenticationRequestParameters requestParameters, ICryptographyManager cryptographyManager, - string clientId, - string tokenEndpoint, - bool sendX5C, - bool useSha2AndPss, + string tokenEndpoint, CancellationToken cancellationToken) { - var jwtToken = new JsonWebToken( + string clientId = requestParameters.AppConfig.ClientId; + + // Log the incoming request parameters for diagnostic purposes + requestParameters.RequestContext.Logger.Verbose(() => $"Building assertion from certificate with clientId: {clientId} at endpoint: {tokenEndpoint}"); + + if (requestParameters.MtlsCertificate == null) + { + requestParameters.RequestContext.Logger.Verbose(() => "Proceeding with JWT token creation and adding client assertion."); + + bool useSha2 = requestParameters.AuthorityManager.Authority.AuthorityInfo.IsSha2CredentialSupported; + + var jwtToken = new JsonWebToken( cryptographyManager, clientId, tokenEndpoint, _claimsToSign, _appendDefaultClaims); - string assertion = jwtToken.Sign(Certificate, sendX5C, useSha2AndPss); + string assertion = jwtToken.Sign(Certificate, requestParameters.SendX5C, useSha2); - oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer); - oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, assertion); + oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer); + oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, assertion); + } + else + { + // Log that MTLS PoP is required and JWT token creation is skipped + requestParameters.RequestContext.Logger.Verbose(() => "MTLS PoP Client credential request. Skipping client assertion."); + } return Task.CompletedTask; } diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/IClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/IClientCredential.cs index 7ab578f345..83302502cd 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/IClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/IClientCredential.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Internal.Requests; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; using Microsoft.Identity.Client.TelemetryCore; @@ -22,12 +23,9 @@ internal interface IClientCredential Task AddConfidentialClientParametersAsync( OAuth2Client oAuth2Client, - ILoggerAdapter logger, + AuthenticationRequestParameters authenticationRequestParameters, ICryptographyManager cryptographyManager, - string clientId, string tokenEndpoint, - bool sendX5C, - bool useSha2, CancellationToken cancellationToken); } } diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SecretStringClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SecretStringClientCredential.cs index ce5f26d7fc..86c003d398 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SecretStringClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SecretStringClientCredential.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Internal.Requests; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; using Microsoft.Identity.Client.TelemetryCore; @@ -23,13 +24,10 @@ public SecretStringClientCredential(string secret) } public Task AddConfidentialClientParametersAsync( - OAuth2Client oAuth2Client, - ILoggerAdapter logger, + OAuth2Client oAuth2Client, + AuthenticationRequestParameters requestParameters, ICryptographyManager cryptographyManager, - string clientId, string tokenEndpoint, - bool sendX5C, - bool useSha2, CancellationToken cancellationToken) { oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientSecret, Secret); diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionClientCredential.cs index 2af6124fcf..0cf788f71c 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionClientCredential.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Internal.Requests; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; using Microsoft.Identity.Client.TelemetryCore; @@ -23,13 +24,10 @@ public SignedAssertionClientCredential(string signedAssertion) } public Task AddConfidentialClientParametersAsync( - OAuth2Client oAuth2Client, - ILoggerAdapter logger, + OAuth2Client oAuth2Client, + AuthenticationRequestParameters requestParameters, ICryptographyManager cryptographyManager, - string clientId, string tokenEndpoint, - bool sendX5C, - bool useSha2, CancellationToken cancellationToken) { oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer); diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs index bb7337b4ad..7cda74e5cb 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Internal.Requests; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; using Microsoft.Identity.Client.TelemetryCore; @@ -29,20 +30,17 @@ public SignedAssertionDelegateClientCredential(Func AuthorityManager.Authority.AuthorityInfo; - public AuthorityInfo AuthorityOverride => _commonParameters.AuthorityOverride; + public AuthorityInfo AuthorityOverride => _commonParameters.AuthorityOverride; #endregion diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs index c23ce12a25..3e54575b1a 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/ClientCredentialRequest.cs @@ -231,6 +231,7 @@ private Dictionary GetBodyParameters() [OAuth2Parameter.GrantType] = OAuth2GrantType.ClientCredentials, [OAuth2Parameter.Scope] = AuthenticationRequestParameters.Scope.AsSingleString() }; + return dict; } diff --git a/src/client/Microsoft.Identity.Client/Internal/TelemetryTokenTypeConstants.cs b/src/client/Microsoft.Identity.Client/Internal/TelemetryTokenTypeConstants.cs index 2e20dc3f02..fad03cb2e2 100644 --- a/src/client/Microsoft.Identity.Client/Internal/TelemetryTokenTypeConstants.cs +++ b/src/client/Microsoft.Identity.Client/Internal/TelemetryTokenTypeConstants.cs @@ -19,5 +19,7 @@ internal static class TelemetryTokenTypeConstants /// Extension token type for telemetry. This is used for custom token types added to MSAL as extensions through IAuthenticationOperation. public const int Extension = 5; + + public const int MtlsPop = 6; } } diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs index 719e3eae02..459cb35134 100644 --- a/src/client/Microsoft.Identity.Client/MsalError.cs +++ b/src/client/Microsoft.Identity.Client/MsalError.cs @@ -161,6 +161,15 @@ public static class MsalError /// public const string InvalidAuthorityType = "invalid_authority_type"; + /// + /// Missing Tenanted Authority. + /// MSAL.NET requires a tenanted authority (i.e., an authority with a tenant ID) for mTLS to be specified when the application is built, but none was provided. + /// Mitigation + /// Ensure that a tenanted authority, which includes a specific tenant ID, is specified during the application configuration. + /// For example, use "https://login.microsoftonline.com/{tenantId}" or a similar URL structure. + /// + public const string MissingTenantedAuthority = "missing_tenanted_authority"; + /// /// The client is unauthorized to access resource. /// This commonly happens when Mobile App Management (MAM) policies are enabled. MSAL will throw an exception in that case with protection_policy_required sub-error. @@ -1145,5 +1154,23 @@ public static class MsalError /// A cryptographic exception occurred when trying to use the provided certificate /// public const string CryptographicError = "cryptographic_error"; + + /// + /// What happened?mTLS Proof of Possession (mTLS PoP) is configured but a region was not specified. + /// MitigationEnsure that the AzureRegion configuration is set when using mTLS PoP as it requires a regional endpoint. + /// + public const string MtlsPopWithoutRegion = "mtls_pop_without_region"; + + /// + /// What happened? mTLS Proof of Possession (mTLS PoP) is configured but a certificate was not provided. + /// Mitigation Ensure that a valid certificate is provided in the configuration when using mTLS PoP as it is required for secure authentication. + /// + public const string MtlsCertificateNotProvided = "mtls_certificate_not_provided"; + + /// + /// What happened? mTLS Proof of Possession (mTLS PoP) requires a specific Azure region to be specified. + /// Mitigation: Ensure that the AzureRegion configuration is set when using mTLS PoP as it requires a regional endpoint. + /// + public const string RegionRequiredForMtlsPop = "region_required_for_mtls_pop"; } } diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index fdbbc32602..21c17fcaa5 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -259,8 +259,10 @@ public static string InvalidRedirectUriReceived(string invalidRedirectUri) public const string AuthorityDoesNotHaveTwoSegments = "Authority should be in the form /, for example https://login.microsoftonline.com/common. "; + public const string DstsAuthorityDoesNotHaveThreeSegments = "Authority should be in the form //, for example https://login.microsoftonline.com/dsts/. "; + public const string AzureAdMyOrgRequiresSpecifyingATenant = "When specifying AadAuthorityAudience.AzureAdMyOrg, you must also specify a tenant domain or tenant GUID. "; public const string CustomWebUiReturnedInvalidUri = "ICustomWebUi returned an invalid URI - it is empty or has no query. "; @@ -428,5 +430,10 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName) public const string SetCiamAuthorityAtRequestLevelNotSupported = "Setting the CIAM authority (ex. \"{tenantName}.ciamlogin.com\") at the request level is not supported. The CIAM authority must be set during application creation"; public const string ClaimsChallenge = "The returned error contains a claims challenge. For additional info on how to handle claims related to multifactor authentication, Conditional Access, and incremental consent, see https://aka.ms/msal-conditional-access-claims. If you are using the On-Behalf-Of flow, see https://aka.ms/msal-conditional-access-claims-obo for details."; public const string CryptographicError = "A cryptographic exception occurred. Possible cause: the certificate has been disposed. See inner exception for full details."; + public const string MtlsPopWithoutRegion = "mTLS Proof of Possession requires a region to be specified. Please set AzureRegion in the configuration at the application level."; + public const string MtlsCertificateNotProvidedMessage = "mTLS Proof of Possession requires a certificate to be configured. Please provide a certificate at the application level using the .WithCertificate() instead of passing an assertion. See https://aka.ms/msal-net-pop for details."; + public const string MtlsInvalidAuthorityTypeMessage = "mTLS PoP is only supported for AAD authority type. See https://aka.ms/msal-net-pop for details."; + public const string MtlsNonTenantedAuthorityNotAllowedMessage = "mTLS authentication requires a tenanted authority. Using 'common', 'organizations', or similar non-tenanted authorities is not allowed. Please provide an authority with a specific tenant ID (e.g., 'https://login.microsoftonline.com/{tenantId}'). See https://aka.ms/msal-net-pop for details."; + public const string RegionRequiredForMtlsPopMessage = "Regional auto-detect failed. mTLS Proof-of-Possession requires a region to be specified, as there is no global endpoint for mTLS. See https://aka.ms/msal-net-pop for details."; } } diff --git a/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs b/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs index cc9d8c9b46..6bdecdb8ff 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/TokenClient.cs @@ -135,15 +135,11 @@ private async Task AddBodyParamsAndHeadersAsync( var tokenEndpoint = await _requestParams.Authority.GetTokenEndpointAsync(_requestParams.RequestContext).ConfigureAwait(false); - bool useSha2 = _requestParams.AuthorityManager.Authority.AuthorityInfo.IsSha2CredentialSupported; await _serviceBundle.Config.ClientCredential.AddConfidentialClientParametersAsync( _oAuth2Client, - _requestParams.RequestContext.Logger, + _requestParams, _serviceBundle.PlatformProxy.CryptographyManager, - _requestParams.AppConfig.ClientId, tokenEndpoint, - _requestParams.SendX5C, - useSha2, cancellationToken).ConfigureAwait(false); _requestParams.RequestContext.Logger.Verbose( 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 e69de29bb2..074cba040e 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -0,0 +1,6 @@ +Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder.WithSignedHttpRequestProofOfPossession(Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration popAuthenticationConfiguration) -> T +Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithMtlsProofOfPossession() -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MissingTenantedAuthority = "missing_tenanted_authority" -> string +const Microsoft.Identity.Client.MsalError.MtlsCertificateNotProvided = "mtls_certificate_not_provided" -> string +const Microsoft.Identity.Client.MsalError.MtlsPopWithoutRegion = "mtls_pop_without_region" -> string +const Microsoft.Identity.Client.MsalError.RegionRequiredForMtlsPop = "region_required_for_mtls_pop" -> string 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 e69de29bb2..074cba040e 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -0,0 +1,6 @@ +Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder.WithSignedHttpRequestProofOfPossession(Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration popAuthenticationConfiguration) -> T +Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithMtlsProofOfPossession() -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MissingTenantedAuthority = "missing_tenanted_authority" -> string +const Microsoft.Identity.Client.MsalError.MtlsCertificateNotProvided = "mtls_certificate_not_provided" -> string +const Microsoft.Identity.Client.MsalError.MtlsPopWithoutRegion = "mtls_pop_without_region" -> string +const Microsoft.Identity.Client.MsalError.RegionRequiredForMtlsPop = "region_required_for_mtls_pop" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index e69de29bb2..074cba040e 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 @@ -0,0 +1,6 @@ +Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder.WithSignedHttpRequestProofOfPossession(Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration popAuthenticationConfiguration) -> T +Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithMtlsProofOfPossession() -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MissingTenantedAuthority = "missing_tenanted_authority" -> string +const Microsoft.Identity.Client.MsalError.MtlsCertificateNotProvided = "mtls_certificate_not_provided" -> string +const Microsoft.Identity.Client.MsalError.MtlsPopWithoutRegion = "mtls_pop_without_region" -> string +const Microsoft.Identity.Client.MsalError.RegionRequiredForMtlsPop = "region_required_for_mtls_pop" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index e69de29bb2..074cba040e 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 @@ -0,0 +1,6 @@ +Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder.WithSignedHttpRequestProofOfPossession(Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration popAuthenticationConfiguration) -> T +Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithMtlsProofOfPossession() -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MissingTenantedAuthority = "missing_tenanted_authority" -> string +const Microsoft.Identity.Client.MsalError.MtlsCertificateNotProvided = "mtls_certificate_not_provided" -> string +const Microsoft.Identity.Client.MsalError.MtlsPopWithoutRegion = "mtls_pop_without_region" -> string +const Microsoft.Identity.Client.MsalError.RegionRequiredForMtlsPop = "region_required_for_mtls_pop" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index e69de29bb2..074cba040e 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 @@ -0,0 +1,6 @@ +Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder.WithSignedHttpRequestProofOfPossession(Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration popAuthenticationConfiguration) -> T +Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithMtlsProofOfPossession() -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MissingTenantedAuthority = "missing_tenanted_authority" -> string +const Microsoft.Identity.Client.MsalError.MtlsCertificateNotProvided = "mtls_certificate_not_provided" -> string +const Microsoft.Identity.Client.MsalError.MtlsPopWithoutRegion = "mtls_pop_without_region" -> string +const Microsoft.Identity.Client.MsalError.RegionRequiredForMtlsPop = "region_required_for_mtls_pop" -> string 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 e69de29bb2..074cba040e 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 @@ -0,0 +1,6 @@ +Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder.WithSignedHttpRequestProofOfPossession(Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration popAuthenticationConfiguration) -> T +Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithMtlsProofOfPossession() -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MissingTenantedAuthority = "missing_tenanted_authority" -> string +const Microsoft.Identity.Client.MsalError.MtlsCertificateNotProvided = "mtls_certificate_not_provided" -> string +const Microsoft.Identity.Client.MsalError.MtlsPopWithoutRegion = "mtls_pop_without_region" -> string +const Microsoft.Identity.Client.MsalError.RegionRequiredForMtlsPop = "region_required_for_mtls_pop" -> string diff --git a/tests/Microsoft.Identity.Test.Common/Core/Helpers/HttpSnifferClientFactory.cs b/tests/Microsoft.Identity.Test.Common/Core/Helpers/HttpSnifferClientFactory.cs index ab0bb959d0..0cd6178df8 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Helpers/HttpSnifferClientFactory.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Helpers/HttpSnifferClientFactory.cs @@ -1,14 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Net.Http; +using System.Security.Cryptography.X509Certificates; using Microsoft.Identity.Client; namespace Microsoft.Identity.Test.Common { - public class HttpSnifferClientFactory : IMsalHttpClientFactory + public class HttpSnifferClientFactory : IMsalMtlsHttpClientFactory { readonly HttpClient _httpClient; @@ -38,5 +40,18 @@ public HttpClient GetHttpClient() { return _httpClient; } + + public HttpClient GetHttpClient(X509Certificate2 x509Certificate2) + { + if (x509Certificate2 == null) + { + return GetHttpClient(); + } + + var handler = new HttpClientHandler(); + handler.ClientCertificates.Add(x509Certificate2); + + return new HttpClient(handler); + } } } diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs index 9a2c9956bc..4406491080 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs @@ -67,7 +67,7 @@ public AuthenticationRequestParameters CreateAuthenticationRequestParameters( }; var authorityObj = Authority.CreateAuthority(authority, validateAuthority); - var requestContext = new RequestContext(ServiceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(ServiceBundle, Guid.NewGuid(), commonParameters.MtlsCertificate); AuthenticationRequestParameters authenticationRequestParameters = new AuthenticationRequestParameters( ServiceBundle, diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpMessageHandler.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpMessageHandler.cs index 654b819214..1b25c4cb14 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpMessageHandler.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpMessageHandler.cs @@ -24,6 +24,7 @@ internal class MockHttpMessageHandler : HttpClientHandler public IDictionary ExpectedPostData { get; set; } public IDictionary ExpectedRequestHeaders { get; set; } public IList UnexpectedRequestHeaders { get; set; } + public IDictionary UnExpectedPostData { get; set; } public HttpMethod ExpectedMethod { get; set; } public Exception ExceptionToThrow { get; set; } @@ -67,6 +68,8 @@ protected override Task SendAsync(HttpRequestMessage reques ValidatePostDataAsync(request); + ValidateNotExpectedPostData(); + ValidateHeaders(request); AdditionalRequestValidation?.Invoke(request); @@ -114,6 +117,26 @@ private async Task ValidatePostDataAsync(HttpRequestMessage request) } } + private void ValidateNotExpectedPostData() + { + if (UnExpectedPostData != null) + { + List unexpectedKeysFound = new List(); + + // Check each key in the unexpected post data dictionary + foreach (var key in UnExpectedPostData.Keys) + { + if (ActualRequestPostData.ContainsKey(key)) + { + unexpectedKeysFound.Add(key); + } + } + + // Assert that no unexpected keys were found, reporting all violations at once + Assert.IsTrue(unexpectedKeysFound.Count == 0, $"Did not expect to find post data keys: {string.Join(", ", unexpectedKeysFound)}"); + } + } + private void ValidateHeaders(HttpRequestMessage request) { ActualRequestHeaders = request.Headers; diff --git a/tests/Microsoft.Identity.Test.Common/TestCommon.cs b/tests/Microsoft.Identity.Test.Common/TestCommon.cs index 730259fc64..95772c3da7 100644 --- a/tests/Microsoft.Identity.Test.Common/TestCommon.cs +++ b/tests/Microsoft.Identity.Test.Common/TestCommon.cs @@ -124,7 +124,7 @@ public static AuthenticationRequestParameters CreateAuthenticationRequestParamet }; authority ??= Authority.CreateAuthority(TestConstants.AuthorityTestTenant); - requestContext ??= new RequestContext(serviceBundle, Guid.NewGuid()) + requestContext ??= new RequestContext(serviceBundle, Guid.NewGuid(), commonParameters.MtlsCertificate) { ApiEvent = new Client.TelemetryCore.Internal.Events.ApiEvent(Guid.NewGuid()) }; diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs new file mode 100644 index 0000000000..55c924df51 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Internal; +using Microsoft.Identity.Test.Common; +using Microsoft.Identity.Test.Integration.Infrastructure; +using Microsoft.Identity.Test.Integration.NetFx.Infrastructure; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Identity.Test.Integration.HeadlessTests +{ + // Tests in this class will run on .NET Core + [TestClass] + public class ClientCredentialsMtlsPopTests + { + private const string MsiAllowListedAppIdforSNI = "163ffef9-a313-45b4-ab2f-c7e2f5e0e23e"; + + [TestInitialize] + public void TestInitialize() + { + TestCommon.ResetInternalStaticCaches(); + } + + [TestMethod] + public async Task Sni_Gets_Pop_Token_Successfully_TestAsync() + { + // Arrange: Use the public cloud settings for testing + IConfidentialAppSettings settings = ConfidentialAppSettings.GetSettings(Cloud.Public); + + // Retrieve the certificate from settings + X509Certificate2 cert = settings.GetCertificate(); + + // Build Confidential Client Application with SNI certificate at App level + IConfidentialClientApplication confidentialApp = ConfidentialClientApplicationBuilder.Create(MsiAllowListedAppIdforSNI) + .WithAuthority("https://login.microsoftonline.com/bea21ebe-8b64-4d06-9f6d-6a889b120a7c") + .WithAzureRegion("westus3") //test slice region + .WithCertificate(cert, true) + .WithExperimentalFeatures() + .WithTestLogging() + .Build(); + + // Act: Acquire token with MTLS Proof of Possession at Request level + AuthenticationResult authResult = await confidentialApp + .AcquireTokenForClient(settings.AppScopes) + .WithMtlsProofOfPossession() + .WithExtraQueryParameters("dc=ESTSR-PUB-WUS3-AZ1-TEST1&slice=TestSlice") //Feature in test slice + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert: Check that the MTLS PoP token acquisition was successful + Assert.IsNotNull(authResult, "The authentication result should not be null."); + Assert.AreEqual(Constants.MtlsPoPTokenType, authResult.TokenType, "Token type should be MTLS PoP"); + Assert.IsNotNull(authResult.AccessToken, "Access token should not be null"); + + // Simulate cache retrieval to verify MTLS configuration is cached properly + authResult = await confidentialApp + .AcquireTokenForClient(settings.AppScopes) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert: Verify that the token was fetched from cache on the second request + Assert.AreEqual(TokenSource.Cache, authResult.AuthenticationResultMetadata.TokenSource, "Token should be retrieved from cache"); + } + } +} diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsTests.NetFwk.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsTests.NetFwk.cs index 27e443c9b3..233b45aadf 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsTests.NetFwk.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsTests.NetFwk.cs @@ -503,7 +503,6 @@ private static string GetSignedClientAssertionManual( }; } - var headerBytes = JsonSerializer.SerializeToUtf8Bytes(header); var claimsBytes = JsonSerializer.SerializeToUtf8Bytes(claims); string token = Base64UrlHelpers.Encode(headerBytes) + "." + Base64UrlHelpers.Encode(claimsBytes); diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/InstanceDiscoveryIntegrationTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/InstanceDiscoveryIntegrationTests.cs index 6b517c3d76..ec50567398 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/InstanceDiscoveryIntegrationTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/InstanceDiscoveryIntegrationTests.cs @@ -149,7 +149,7 @@ public async Task KnownInstanceMetadataIsUpToDateAsync() } } - IDictionary expectedMetadata = + IDictionary expectedMetadata = KnownMetadataProvider.GetAllEntriesForTest(); CoreAssert.AssertDictionariesAreEqual( diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/PoPTests.NetFwk.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/PoPTests.NetFwk.cs index b60c70f865..6f49f3ded2 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/PoPTests.NetFwk.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/PoPTests.NetFwk.cs @@ -78,14 +78,13 @@ public async Task HappyPath_Async() var confidentialApp = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()) .WithTestLogging() .Build(); var result = await confidentialApp.AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -107,7 +106,6 @@ private async Task BearerAndPoP_CanCoexist_Async() var cca = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithClientSecret(settings.GetSecret()) .WithTestLogging() .WithAuthority(settings.Authority).Build(); @@ -117,7 +115,7 @@ private async Task BearerAndPoP_CanCoexist_Async() Trace.WriteLine("Getting a PoP token"); AuthenticationResult result = await cca .AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync() .ConfigureAwait(false); @@ -155,7 +153,6 @@ private async Task MultipleKeys_Async() var settings = ConfidentialAppSettings.GetSettings(Cloud.Public); var cca = ConfidentialClientApplicationBuilder.Create(settings.ClientId) - .WithExperimentalFeatures() .WithTestLogging() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()).Build(); @@ -163,7 +160,7 @@ private async Task MultipleKeys_Async() var result = await cca .AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig1) + .WithSignedHttpRequestProofOfPossession(popConfig1) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -177,7 +174,6 @@ private async Task MultipleKeys_Async() // recreate the pca to ensure that the silent call is served from the cache, i.e. the key remains stable cca = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()) .WithHttpClientFactory(new NoAccessHttpClientFactory()) // token should be served from the cache, no network access necessary @@ -186,7 +182,7 @@ private async Task MultipleKeys_Async() result = await cca .AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig1) + .WithSignedHttpRequestProofOfPossession(popConfig1) .ExecuteAsync() .ConfigureAwait(false); @@ -202,7 +198,7 @@ private async Task MultipleKeys_Async() // Call some other Uri - the same pop assertion can be reused, i.e. no need to call Evo result = await cca .AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig2) + .WithSignedHttpRequestProofOfPossession(popConfig2) .ExecuteAsync() .ConfigureAwait(false); @@ -223,7 +219,6 @@ public async Task PopTestWithConfigObjectAsync() var confidentialApp = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()) .WithTestLogging() @@ -234,7 +229,7 @@ public async Task PopTestWithConfigObjectAsync() popConfig.HttpMethod = HttpMethod.Get; var result = await confidentialApp.AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -260,7 +255,6 @@ public async Task PopTestWithRSAAsync() var confidentialApp = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()) .Build(); @@ -271,7 +265,7 @@ public async Task PopTestWithRSAAsync() popConfig.HttpMethod = HttpMethod.Get; var result = await confidentialApp.AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -298,7 +292,6 @@ public async Task ROPC_PopTestWithRSAAsync() var confidentialApp = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()) .Build(); @@ -309,7 +302,7 @@ public async Task ROPC_PopTestWithRSAAsync() popConfig.HttpMethod = HttpMethod.Get; var result = await (confidentialApp as IByUsernameAndPassword).AcquireTokenByUsernamePassword(s_ropcScope, labResponse.User.Upn, labResponse.User.GetOrFetchPassword()) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -329,7 +322,6 @@ public async Task PopTest_ExternalWilsonSigning_Async() var confidentialApp = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()) .Build(); @@ -345,7 +337,7 @@ public async Task PopTest_ExternalWilsonSigning_Async() }; var result = await confidentialApp.AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -380,7 +372,7 @@ public async Task PopTest_ExternalWilsonSigning_Async() req, "pop"); var result2 = await confidentialApp.AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); Assert.AreEqual( @@ -395,7 +387,6 @@ public async Task PopTestWithECDAsync() var confidentialApp = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()) .Build(); @@ -406,7 +397,7 @@ public async Task PopTestWithECDAsync() popConfig.HttpMethod = HttpMethod.Post; var result = await confidentialApp.AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -527,7 +518,6 @@ public async Task InMemoryCryptoProvider_AlgIsPS256() var confidentialApp = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()) .Build(); @@ -553,7 +543,7 @@ public async Task InMemoryCryptoProvider_AlgIsPS256() }; var result = await confidentialApp.AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -580,7 +570,6 @@ public async Task InMemoryCryptoProvider_WithGraph() var confidentialApp = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()) .Build(); @@ -606,7 +595,7 @@ public async Task InMemoryCryptoProvider_WithGraph() }; var result = await confidentialApp.AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -643,7 +632,7 @@ public async Task InMemoryCryptoProvider_WithGraph() }; var resultWithNonce = await confidentialApp.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" }) - .WithProofOfPossession(popConfigWithNonce) + .WithSignedHttpRequestProofOfPossession(popConfigWithNonce) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -668,7 +657,6 @@ public async Task PoPToken_ShouldHaveCorrectAlgorithm_PS256_Async() var settings = ConfidentialAppSettings.GetSettings(Cloud.Public); var confidentialApp = ConfidentialClientApplicationBuilder .Create(settings.ClientId) - .WithExperimentalFeatures() .WithAuthority(settings.Authority) .WithClientSecret(settings.GetSecret()) .Build(); @@ -681,7 +669,7 @@ public async Task PoPToken_ShouldHaveCorrectAlgorithm_PS256_Async() // Act var result = await confidentialApp.AcquireTokenForClient(s_keyvaultScope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsalExtensions.cs b/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsalExtensions.cs index 07af7483f2..839fc14520 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsalExtensions.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsalExtensions.cs @@ -37,7 +37,7 @@ var popAuthenticationConfiguration PopCryptoProvider = new SigningCredentialsToPopCryptoProviderAdapter(popCredentials, assertNotSigned: true) }; - return builder.WithProofOfPossession(popAuthenticationConfiguration); + return builder.WithSignedHttpRequestProofOfPossession(popAuthenticationConfiguration); } } diff --git a/tests/Microsoft.Identity.Test.Unit/ApiConfigTests/AuthorityTests.cs b/tests/Microsoft.Identity.Test.Unit/ApiConfigTests/AuthorityTests.cs index 522ed06362..a4d47e0717 100644 --- a/tests/Microsoft.Identity.Test.Unit/ApiConfigTests/AuthorityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ApiConfigTests/AuthorityTests.cs @@ -39,7 +39,8 @@ public override void TestInitialize() _harness = base.CreateTestHarness(); _testRequestContext = new RequestContext( _harness.ServiceBundle, - Guid.NewGuid()); + Guid.NewGuid(), + null); } [TestCleanup] diff --git a/tests/Microsoft.Identity.Test.Unit/BrokerTests/RuntimeBrokerTests.cs b/tests/Microsoft.Identity.Test.Unit/BrokerTests/RuntimeBrokerTests.cs index b10d24acbd..01f55872c9 100644 --- a/tests/Microsoft.Identity.Test.Unit/BrokerTests/RuntimeBrokerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/BrokerTests/RuntimeBrokerTests.cs @@ -128,7 +128,7 @@ public async Task ATS_CallsLog_When_CalledAsync() { // Arrange var appTokenCache = new TokenCache(_serviceBundle, isApplicationTokenCache: false); - var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid(), null); var tenantAuthority = AuthorityInfo.FromAadAuthority(AzureCloudInstance.AzurePublic, tenant: TestConstants.AadTenantId, validateAuthority: false); var acquireTokenCommonParameters = new AcquireTokenCommonParameters { diff --git a/tests/Microsoft.Identity.Test.Unit/CacheTests/CacheKeyFactoryTests.cs b/tests/Microsoft.Identity.Test.Unit/CacheTests/CacheKeyFactoryTests.cs index fdaba50c47..289511f921 100644 --- a/tests/Microsoft.Identity.Test.Unit/CacheTests/CacheKeyFactoryTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CacheTests/CacheKeyFactoryTests.cs @@ -32,7 +32,7 @@ public void TestCacheKeyForManagedIdentity() { // Arrange var appTokenCache = new TokenCache(_serviceBundle, isApplicationTokenCache: true); - var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid(), null); var authority = Authority.CreateAuthority("https://login.microsoft.com/managed_identity", true); requestContext.ServiceBundle.Config.Authority = authority; requestContext.ServiceBundle.Config.ClientId = Constants.ManagedIdentityDefaultClientId; @@ -63,7 +63,7 @@ public void TestCacheKeyForADFSAuthority() { // Arrange var appTokenCache = new TokenCache(_serviceBundle, isApplicationTokenCache: true); - var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid(), null); var authority = Authority.CreateAuthority(TestConstants.ADFSAuthority, true); requestContext.ServiceBundle.Config.Authority = authority; @@ -94,7 +94,7 @@ public void TestCacheKeyForTenantAuthority() { // Arrange var appTokenCache = new TokenCache(_serviceBundle, isApplicationTokenCache: true); - var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid(), null); var tenantAuthority = AuthorityInfo.FromAadAuthority(AzureCloudInstance.AzurePublic, tenant: TestConstants.AadTenantId, validateAuthority: false); var acquireTokenCommonParameters = new AcquireTokenCommonParameters { @@ -123,7 +123,7 @@ public void TestCacheKeyForRemoveAccount() { // Arrange var appTokenCache = new TokenCache(_serviceBundle, isApplicationTokenCache: true); - var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid(), null); var tenantAuthority = AuthorityInfo.FromAadAuthority(AzureCloudInstance.AzurePublic, tenant: TestConstants.AadTenantId, validateAuthority: false); var acquireTokenCommonParameters = new AcquireTokenCommonParameters { @@ -180,7 +180,7 @@ public void TestCacheKeyForObo() { // Arrange var userTokenCache = new TokenCache(_serviceBundle, isApplicationTokenCache: false); - var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid(), null); var tenantAuthority = AuthorityInfo.FromAadAuthority(AzureCloudInstance.AzurePublic, tenant: TestConstants.AadTenantId, validateAuthority: false); var acquireTokenCommonParameters = new AcquireTokenCommonParameters { @@ -213,7 +213,7 @@ public void TestCacheKeyForObo_WithCacheKey() { // Arrange var userTokenCache = new TokenCache(_serviceBundle, isApplicationTokenCache: false); - var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(_serviceBundle, Guid.NewGuid(), null); var tenantAuthority = AuthorityInfo.FromAadAuthority(AzureCloudInstance.AzurePublic, tenant: TestConstants.AadTenantId, validateAuthority: false); var acquireTokenCommonParameters = new AcquireTokenCommonParameters { diff --git a/tests/Microsoft.Identity.Test.Unit/CacheTests/TokenCacheTests.cs b/tests/Microsoft.Identity.Test.Unit/CacheTests/TokenCacheTests.cs index 0d7eae42c8..5c76a905a7 100644 --- a/tests/Microsoft.Identity.Test.Unit/CacheTests/TokenCacheTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CacheTests/TokenCacheTests.cs @@ -61,7 +61,7 @@ public async Task WithLegacyCacheCompatibilityTest_Async( var legacyCachePersistence = Substitute.For(); mockHttpManager.AddInstanceDiscoveryMockHandler(); var serviceBundle = TestCommon.CreateServiceBundleWithCustomHttpManager(mockHttpManager, isLegacyCacheEnabled: enableLegacyCacheCompatibility); - var requestContext = new RequestContext(serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(serviceBundle, Guid.NewGuid(), null); var response = TestConstants.CreateMsalTokenResponse(); ITokenCacheInternal cache = new TokenCache(serviceBundle, false, legacyCachePersistence); @@ -112,7 +112,7 @@ public async Task WithMultiCloudSupportTest_Async( var serviceBundle = TestCommon.CreateServiceBundleWithCustomHttpManager( mockHttpManager, isMultiCloudSupportEnabled: multiCloudSupportEnabled); - var requestContext = new RequestContext(serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(serviceBundle, Guid.NewGuid(), null); var response = TestConstants.CreateMsalTokenResponse(); ITokenCacheInternal cache = new TokenCache(serviceBundle, false); @@ -1179,7 +1179,7 @@ public async Task SerializeDeserializeCacheTestAsync() MsalTokenResponse response = TestConstants.CreateMsalTokenResponse(); - var requestContext = new RequestContext(serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(serviceBundle, Guid.NewGuid(), null); var requestParams = TestCommon.CreateAuthenticationRequestParameters( serviceBundle, authority: Authority.CreateAuthority(TestConstants.AuthorityUtidTenant), @@ -1234,7 +1234,7 @@ public async Task SerializeDeserializeCache_ClearCacheTrueWithNoSerializedCache_ MsalTokenResponse response = TestConstants.CreateMsalTokenResponse(); - var requestContext = new RequestContext(serviceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(serviceBundle, Guid.NewGuid(), null); var requestParams = TestCommon.CreateAuthenticationRequestParameters( serviceBundle, authority: Authority.CreateAuthority(TestConstants.AuthorityUtidTenant), @@ -1300,7 +1300,7 @@ public void CacheB2CTokenTest() // creating IDToken with empty tenantID and displayableID/PreferredUserName for B2C scenario MsalTokenResponse response = TestConstants.CreateMsalTokenResponse(); - var requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null); var requestParams = TestCommon.CreateAuthenticationRequestParameters( harness.ServiceBundle, diff --git a/tests/Microsoft.Identity.Test.Unit/CacheTests/UnifiedCacheTests.cs b/tests/Microsoft.Identity.Test.Unit/CacheTests/UnifiedCacheTests.cs index 64a7011bfc..1e9bcc7e71 100644 --- a/tests/Microsoft.Identity.Test.Unit/CacheTests/UnifiedCacheTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CacheTests/UnifiedCacheTests.cs @@ -60,7 +60,7 @@ public void UnifiedCache_MsalStoresToAndReadRtFromAdalCache() Assert.IsTrue(adalCacheDictionary.Count == 1); - var requestContext = new RequestContext(app.ServiceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(app.ServiceBundle, Guid.NewGuid(), null); var authority = Microsoft.Identity.Client.Instance.Authority.CreateAuthorityForRequestAsync( requestContext, null).Result; diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/AadAuthorityTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/AadAuthorityTests.cs index 44bb7cde6a..bb4dc0aec1 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/AadAuthorityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/AadAuthorityTests.cs @@ -38,7 +38,7 @@ public void ImmutableTest() public async Task CreateEndpointsWithCommonTenantAsync() { using var harness = CreateTestHarness(); - RequestContext requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid()); + RequestContext requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null); Authority instance = Authority.CreateAuthority("https://login.microsoftonline.com/common"); Assert.IsNotNull(instance); @@ -92,7 +92,7 @@ public async Task FailedValidationTestAsync(bool isInstanceDiscoveryEnabled) TestCommon.CreateServiceBundleWithCustomHttpManager(harness.HttpManager, authority: instance.AuthorityInfo.CanonicalAuthority.ToString(), validateAuthority: true); try { - AuthorityManager am = new AuthorityManager(new RequestContext(harness.ServiceBundle, Guid.NewGuid()), instance); + AuthorityManager am = new AuthorityManager(new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null), instance); await am.RunInstanceDiscoveryAndValidationAsync().ConfigureAwait(false); if (isInstanceDiscoveryEnabled) @@ -339,7 +339,7 @@ public async Task CreateAuthorityForRequestAsync_MSAPassthroughAsync() var testAccount = new Account("TEST_ID.9188040d-6c67-4c5b-b112-36a304b66dad", "username", Authority.CreateAuthority("https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad").AuthorityInfo.Host); using var harness = CreateTestHarness(); - RequestContext requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid()); + RequestContext requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null); requestContext.ServiceBundle.Config.IsBrokerEnabled = true; requestContext.ServiceBundle.Config.BrokerOptions = new BrokerOptions(BrokerOptions.OperatingSystems.Windows); requestContext.ServiceBundle.Config.BrokerOptions.MsaPassthrough = true; diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/B2cAuthorityTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/B2cAuthorityTests.cs index d5e965a749..ecdebb59f0 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/B2cAuthorityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/B2cAuthorityTests.cs @@ -49,7 +49,8 @@ public void B2CLoginAuthorityEndpoints_Success() var _harness = base.CreateTestHarness(); var _testRequestContext = new RequestContext( _harness.ServiceBundle, - Guid.NewGuid()); + Guid.NewGuid(), + null); Assert.IsNotNull(instance); Assert.AreEqual(instance.AuthorityInfo.AuthorityType, AuthorityType.B2C); diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/DstsAuthorityTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/DstsAuthorityTests.cs index 5e9b895fb7..27c25c3156 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/DstsAuthorityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/DstsAuthorityTests.cs @@ -162,7 +162,8 @@ public void DstsEndpointsTest(string authority) var _harness = base.CreateTestHarness(); var _testRequestContext = new RequestContext( _harness.ServiceBundle, - Guid.NewGuid()); + Guid.NewGuid(), + null); Assert.AreEqual($"{authority}oauth2/v2.0/token", instance.GetTokenEndpointAsync(_testRequestContext).Result); Assert.AreEqual($"{authority}oauth2/v2.0/authorize", instance.GetAuthorizationEndpointAsync(_testRequestContext).Result); diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/InstanceDiscoveryManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/InstanceDiscoveryManagerTests.cs index d2ac58ff29..89a910abd1 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/InstanceDiscoveryManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/InstanceTests/InstanceDiscoveryManagerTests.cs @@ -59,7 +59,7 @@ private void InitializeTestObjects(bool isInstanceDiscoveryEnabled = true) _harness = base.CreateTestHarness(isInstanceDiscoveryEnabled: isInstanceDiscoveryEnabled); - _testRequestContext = new RequestContext(_harness.ServiceBundle, Guid.NewGuid()); + _testRequestContext = new RequestContext(_harness.ServiceBundle, Guid.NewGuid(), null); _discoveryManager = new InstanceDiscoveryManager( _harness.HttpManager, false, @@ -411,7 +411,7 @@ public async Task ValidateAuthorityFalse_SkipsNetworkCall_Async() var serviceBundle = ServiceBundle.Create(appConfig); - RequestContext requestContext = new RequestContext(serviceBundle, Guid.NewGuid()); + RequestContext requestContext = new RequestContext(serviceBundle, Guid.NewGuid(), null); // network fails with invalid_instance exception _networkMetadataProvider @@ -433,14 +433,14 @@ private async Task ValidateSelfEntryAsync(Uri authority, RequestContext requestC InstanceDiscoveryMetadataEntry entry = await harness.ServiceBundle.InstanceDiscoveryManager .GetMetadataEntryAsync( AuthorityInfo.FromAuthorityUri(authority.AbsoluteUri, true), - requestContext ?? new RequestContext(harness.ServiceBundle, Guid.NewGuid())) + requestContext ?? new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null)) .ConfigureAwait(false); InstanceDiscoveryMetadataEntry entry2 = await harness.ServiceBundle.InstanceDiscoveryManager .GetMetadataEntryTryAvoidNetworkAsync( AuthorityInfo.FromAuthorityUri(authority.AbsoluteUri, true), new[] { "some_env" }, - requestContext ?? new RequestContext(harness.ServiceBundle, Guid.NewGuid())) + requestContext ?? new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null)) .ConfigureAwait(false); ValidateSingleEntryMetadata(authority, entry); diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/OAuth2Tests/TokenResponseTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/OAuth2Tests/TokenResponseTests.cs index 5d81d2ce49..3dcb91391c 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/OAuth2Tests/TokenResponseTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/OAuth2Tests/TokenResponseTests.cs @@ -25,7 +25,7 @@ public void JsonDeserializationTest() Task task = client.GetTokenAsync( new Uri(TestConstants.AuthorityCommonTenant + "oauth2/v2.0/token"), - new RequestContext(harness.ServiceBundle, Guid.NewGuid()), addCommonHeaders: true, onBeforePostRequestHandler: null); + new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null), addCommonHeaders: true, onBeforePostRequestHandler: null); MsalTokenResponse response = task.Result; Assert.IsNotNull(response); } diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/RegionDiscoveryProviderTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/RegionDiscoveryProviderTests.cs index 18d9fcffc3..be14ea3590 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/RegionDiscoveryProviderTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/RegionDiscoveryProviderTests.cs @@ -40,11 +40,12 @@ public override void TestInitialize() _testRequestContext = new RequestContext( _harness.ServiceBundle, Guid.NewGuid(), + null, _userCancellationTokenSource.Token); _apiEvent = new ApiEvent(Guid.NewGuid()); _apiEvent.ApiId = ApiEvent.ApiIds.AcquireTokenForClient; _testRequestContext.ApiEvent = _apiEvent; - _regionDiscoveryProvider = new RegionDiscoveryProvider(_httpManager, true); + _regionDiscoveryProvider = new RegionAndMtlsDiscoveryProvider(_httpManager, true); } [TestCleanup] @@ -52,7 +53,7 @@ public override void TestCleanup() { Environment.SetEnvironmentVariable(TestConstants.RegionName, ""); _harness?.Dispose(); - _regionDiscoveryProvider = new RegionDiscoveryProvider(_httpManager, true); + _regionDiscoveryProvider = new RegionAndMtlsDiscoveryProvider(_httpManager, true); _httpManager.Dispose(); base.TestCleanup(); } @@ -151,12 +152,12 @@ public async Task SuccessfulResponseFromUserProvidedRegionAsync() _testRequestContext.ServiceBundle.Config.AzureRegion = TestConstants.Region; - IRegionDiscoveryProvider regionDiscoveryProvider = new RegionDiscoveryProvider(_httpManager, true); + IRegionDiscoveryProvider regionDiscoveryProvider = new RegionAndMtlsDiscoveryProvider(_httpManager, true); InstanceDiscoveryMetadataEntry regionalMetadata = await regionDiscoveryProvider.GetMetadataAsync( new Uri("https://login.microsoftonline.com/common/"), _testRequestContext).ConfigureAwait(false); Assert.IsNotNull(regionalMetadata); - Assert.AreEqual($"centralus.{RegionDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork); + Assert.AreEqual($"centralus.{RegionAndMtlsDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork); Assert.AreEqual(TestConstants.Region, _testRequestContext.ApiEvent.RegionUsed); Assert.AreEqual(RegionAutodetectionSource.FailedAutoDiscovery, _testRequestContext.ApiEvent.RegionAutodetectionSource); @@ -174,7 +175,7 @@ public async Task ResponseFromUserProvidedRegionSameAsRegionDetectedAsync() InstanceDiscoveryMetadataEntry regionalMetadata = await _regionDiscoveryProvider.GetMetadataAsync(new Uri("https://login.microsoftonline.com/common/"), _testRequestContext).ConfigureAwait(false); Assert.IsNotNull(regionalMetadata); - Assert.AreEqual($"centralus.{RegionDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork); + Assert.AreEqual($"centralus.{RegionAndMtlsDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork); Assert.AreEqual(TestConstants.Region, _testRequestContext.ApiEvent.RegionUsed); Assert.AreEqual(RegionAutodetectionSource.EnvVariable, _testRequestContext.ApiEvent.RegionAutodetectionSource); Assert.AreEqual(RegionOutcome.UserProvidedValid, _testRequestContext.ApiEvent.RegionOutcome); @@ -193,7 +194,7 @@ public async Task ResponseFromUserProvidedRegionDifferentFromRegionDetectedAsync _testRequestContext).ConfigureAwait(false); Assert.IsNotNull(regionalMetadata); - Assert.AreEqual($"user_region.{RegionDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork); + Assert.AreEqual($"user_region.{RegionAndMtlsDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork); Assert.AreEqual("user_region", _testRequestContext.ApiEvent.RegionUsed); Assert.AreEqual(RegionAutodetectionSource.EnvVariable, _testRequestContext.ApiEvent.RegionAutodetectionSource); Assert.AreEqual(RegionOutcome.UserProvidedInvalid, _testRequestContext.ApiEvent.RegionOutcome); @@ -211,7 +212,7 @@ public async Task RegionInEnvVariableIsProperlyTransformedAsync() _testRequestContext).ConfigureAwait(false); Assert.IsNotNull(regionalMetadata); - Assert.AreEqual($"regionwithspaces.{RegionDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork); + Assert.AreEqual($"regionwithspaces.{RegionAndMtlsDiscoveryProvider.PublicEnvForRegional}", regionalMetadata.PreferredNetwork); Assert.AreEqual("regionwithspaces", _testRequestContext.ApiEvent.RegionUsed); Assert.AreEqual(RegionAutodetectionSource.EnvVariable, _testRequestContext.ApiEvent.RegionAutodetectionSource); Assert.AreEqual(RegionOutcome.AutodetectSuccess, _testRequestContext.ApiEvent.RegionOutcome); @@ -221,7 +222,7 @@ public async Task RegionInEnvVariableIsProperlyTransformedAsync() [TestMethod] public async Task SuccessfulResponseFromRegionalizedAuthorityAsync() { - var regionalizedAuthority = new Uri($"https://{TestConstants.Region}.{RegionDiscoveryProvider.PublicEnvForRegional}/common/"); + var regionalizedAuthority = new Uri($"https://{TestConstants.Region}.{RegionAndMtlsDiscoveryProvider.PublicEnvForRegional}/common/"); _testRequestContext.ServiceBundle.Config.AzureRegion = ConfidentialClientApplication.AttemptRegionDiscovery; Environment.SetEnvironmentVariable(TestConstants.RegionName, TestConstants.Region); @@ -423,9 +424,9 @@ private void ValidateInstanceMetadata(InstanceDiscoveryMetadataEntry entry, stri { InstanceDiscoveryMetadataEntry expectedEntry = new InstanceDiscoveryMetadataEntry() { - Aliases = new[] { $"{region}.{RegionDiscoveryProvider.PublicEnvForRegional}", "login.microsoftonline.com" }, + Aliases = new[] { $"{region}.{RegionAndMtlsDiscoveryProvider.PublicEnvForRegional}", "login.microsoftonline.com" }, PreferredCache = "login.microsoftonline.com", - PreferredNetwork = $"{region}.{RegionDiscoveryProvider.PublicEnvForRegional}" + PreferredNetwork = $"{region}.{RegionAndMtlsDiscoveryProvider.PublicEnvForRegional}" }; CollectionAssert.AreEquivalent(expectedEntry.Aliases, entry.Aliases); diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/WsTrustTests/MexParserTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/WsTrustTests/MexParserTests.cs index 613e9f8d8d..64c9466986 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/WsTrustTests/MexParserTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/WsTrustTests/MexParserTests.cs @@ -57,7 +57,7 @@ public async Task MexEndpointFailsToResolveTestAsync() try { await harness.ServiceBundle.WsTrustWebRequestManager.GetMexDocumentAsync("http://somehost", - new RequestContext(harness.ServiceBundle, Guid.NewGuid())) + new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null)) .ConfigureAwait(false); Assert.Fail("We expect an exception to be thrown here"); } diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/WsTrustTests/WsTrustTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/WsTrustTests/WsTrustTests.cs index 02723bdce9..5a3480417a 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/WsTrustTests/WsTrustTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/WsTrustTests/WsTrustTests.cs @@ -57,7 +57,7 @@ public async Task WsTrustRequestTestAsync() } }); - var requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null); var wsTrustRequest = endpoint.BuildTokenRequestMessageWindowsIntegratedAuth("urn:federation:SomeAudience"); var wsTrustResponse = await harness.ServiceBundle.WsTrustWebRequestManager.GetWsTrustResponseAsync(endpoint, wsTrustRequest, requestContext) .ConfigureAwait(false); @@ -77,7 +77,7 @@ public async Task WsTrustRequestFailureTestAsync() { harness.HttpManager.AddMockHandlerContentNotFound(HttpMethod.Post, url: uri); - var requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null); try { var message = endpoint.BuildTokenRequestMessageWindowsIntegratedAuth("urn:federation:SomeAudience"); @@ -116,7 +116,7 @@ public async Task WsTrustRequestParseErrorTestAsync() } }); - var requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null); try { var message = endpoint.BuildTokenRequestMessageWindowsIntegratedAuth("urn:federation:SomeAudience"); @@ -155,7 +155,7 @@ public async Task WsTrustRequestTokenNotFoundInResponseTestAsync() } }); - var requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null); var message = endpoint.BuildTokenRequestMessageWindowsIntegratedAuth("urn:federation:SomeAudience"); diff --git a/tests/Microsoft.Identity.Test.Unit/ExceptionTests/ExperimentalFeatureTests.cs b/tests/Microsoft.Identity.Test.Unit/ExceptionTests/ExperimentalFeatureTests.cs index cedeffe2f1..5ad4b1a385 100644 --- a/tests/Microsoft.Identity.Test.Unit/ExceptionTests/ExperimentalFeatureTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ExceptionTests/ExperimentalFeatureTests.cs @@ -17,18 +17,21 @@ namespace Microsoft.Identity.Test.Unit.ExceptionTests [TestClass] public class ExperimentalFeatureTests { + private static readonly string[] s_scopes = ["scope"]; #if NETFRAMEWORK [TestMethod] public async Task ExperimentalFeatureExceptionAsync() { - var cca = ConfidentialClientApplicationBuilder.Create(Guid.NewGuid().ToString()).WithClientSecret("some-secret").Build(); + IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder + .Create(Guid.NewGuid().ToString()) + .WithCertificate(CertHelper.GetOrCreateTestCert()).Build(); + MsalClientException ex = await AssertException.TaskThrowsAsync( - () => cca.AcquireTokenForClient(new[] { "scope" }).WithProofOfPossession(null).ExecuteAsync()) + () => cca.AcquireTokenForClient(s_scopes).WithMtlsProofOfPossession().ExecuteAsync()) .ConfigureAwait(false); Assert.AreEqual(MsalError.ExperimentalFeature, ex.ErrorCode); } #endif - } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs index 9a544005d7..7a97e473d9 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs @@ -83,7 +83,7 @@ public void ValidateServerCertificateCallback_ServerCertificateValidationCallbac var mi = miBuilder.BuildConcrete(); - RequestContext requestContext = new RequestContext(mi.ServiceBundle, Guid.NewGuid()); + RequestContext requestContext = new RequestContext(mi.ServiceBundle, Guid.NewGuid(), null); var sf = ServiceFabricManagedIdentitySource.Create(requestContext); diff --git a/tests/Microsoft.Identity.Test.Unit/OAuthClientTests.cs b/tests/Microsoft.Identity.Test.Unit/OAuthClientTests.cs index d73e1a422d..f9f48b95be 100644 --- a/tests/Microsoft.Identity.Test.Unit/OAuthClientTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/OAuthClientTests.cs @@ -250,7 +250,7 @@ private async Task ValidateOathClientAsync( () => client.ExecuteRequestAsync( requestUri, HttpMethod.Post, - new RequestContext(harness.ServiceBundle, Guid.NewGuid())), + new RequestContext(harness.ServiceBundle, Guid.NewGuid(), null)), allowDerived: true) .ConfigureAwait(false); diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithRegionTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithRegionTests.cs index 52a3df0cfa..d17541943f 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithRegionTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithRegionTests.cs @@ -814,5 +814,45 @@ private static HttpResponseMessage CreateResponse(bool clientCredentialFlow) MockHelpers.CreateClientInfo(TestConstants.Uid, TestConstants.Utid)); } + [TestMethod] + public async Task RegionFallbackToGlobal_WhenImdsFailsAndNoEnvVarSet() + { + using (var httpManager = new MockHttpManager()) + { + httpManager.AddRegionDiscoveryMockHandlerNotFound(); + httpManager.AddInstanceDiscoveryMockHandler(); + httpManager.AddMockHandler(CreateTokenResponseHttpHandler(false)); + + IConfidentialClientApplication app = CreateCca( + httpManager, + ConfidentialClientApplication.AttemptRegionDiscovery); + + AuthenticationResult result = await app + .AcquireTokenForClient(TestConstants.s_scope) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.IsNotNull(result.AccessToken); + Assert.IsNull(result.ApiEvent.RegionUsed); + Assert.AreEqual(RegionOutcome.FallbackToGlobal, result.ApiEvent.RegionOutcome); + Assert.AreEqual(RegionOutcome.FallbackToGlobal, result.AuthenticationResultMetadata.RegionDetails.RegionOutcome); + } + } + + [TestMethod] + public void RegionDiscoveryThrowsException_WhenCustomMetadataAndRegionDiscoveryEnabled() + { + using (var httpManager = new MockHttpManager()) + { + var ex = Assert.ThrowsException(() => CreateCca( + httpManager, + ConfidentialClientApplication.AttemptRegionDiscovery, + hasCustomInstanceMetadata: true)); + + Assert.AreEqual(MsalError.RegionDiscoveryWithCustomInstanceMetadata, ex.ErrorCode); + Assert.AreEqual(MsalErrorMessage.RegionDiscoveryWithCustomInstanceMetadata, ex.Message); + } + } + } } diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs new file mode 100644 index 0000000000..ed4a4fc36e --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs @@ -0,0 +1,672 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AuthScheme.PoP; +using Microsoft.Identity.Client.Internal; +using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.Region; +using Microsoft.Identity.Client.Utils; +using Microsoft.Identity.Test.Common.Core.Helpers; +using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Identity.Test.Unit +{ + [TestClass] + public class MtlsPopTests : TestBase + { + public const string EastUsRegion = "eastus"; + private static X509Certificate2 s_testCertificate; + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + // Create a self-signed test certificate for testing + s_testCertificate = CertHelper.GetOrCreateTestCert(); + + // Ensure the certificate is valid + if (s_testCertificate == null || string.IsNullOrEmpty(s_testCertificate.Thumbprint)) + { + throw new InvalidOperationException("Failed to initialize a valid test certificate."); + } + } + + [ClassCleanup] + public static void ClassCleanup() + { + s_testCertificate.Dispose(); + } + + [TestMethod] + public async Task MtlsPop_AadAuthorityWithoutCertificateAsync() + { + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority("https://login.microsoftonline.com/123456-1234-2345-1234561234") + .WithExperimentalFeatures() + .Build(); + + MsalClientException ex = await AssertException.TaskThrowsAsync(() => + app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() // Enables MTLS PoP + .ExecuteAsync()) + .ConfigureAwait(false); + + Assert.AreEqual(MsalError.MtlsCertificateNotProvided, ex.ErrorCode); + Assert.AreEqual(MsalErrorMessage.MtlsCertificateNotProvidedMessage, ex.Message); + } + + [TestMethod] + public async Task MtlsPopWithoutCertificateAsync() + { + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithClientSecret(TestConstants.ClientSecret) + .WithExperimentalFeatures() + .Build(); + + // Set WithMtlsProofOfPossession on the request without a certificate + MsalClientException ex = await AssertException.TaskThrowsAsync(() => + app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() // Enables MTLS PoP + .ExecuteAsync()) + .ConfigureAwait(false); + + Assert.AreEqual(MsalError.MtlsCertificateNotProvided, ex.ErrorCode); + } + + [TestMethod] + public async Task MtlsPopWithoutCertificateWithClientClaimsAsync() + { + var ipAddress = new Dictionary + { + { "client_ip", "192.168.1.2" } + }; + + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithClientClaims(s_testCertificate, ipAddress) + .WithExperimentalFeatures() + .Build(); + + // Expecting an exception because MTLS PoP requires a certificate to sign the claims + MsalClientException ex = await Assert.ThrowsExceptionAsync(() => + app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() // Enables MTLS PoP + .ExecuteAsync()) + .ConfigureAwait(false); + + Assert.AreEqual(MsalError.MtlsCertificateNotProvided, ex.ErrorCode); + } + + [TestMethod] + public async Task MtlsPopWithoutCertificateWithClientAssertionAsync() + { + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithClientAssertion(() => { return TestConstants.DefaultClientAssertion; }) + .WithExperimentalFeatures() + .Build(); + + // Expecting an exception because MTLS PoP requires a certificate to sign the claims + MsalClientException ex = await Assert.ThrowsExceptionAsync(() => + app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() // Enables MTLS PoP + .ExecuteAsync()) + .ConfigureAwait(false); + + Assert.AreEqual(MsalError.MtlsCertificateNotProvided, ex.ErrorCode); + } + + [TestMethod] + public async Task MtlsPopWithoutRegionAsync() + { + using (var envContext = new EnvVariableContext()) + { + Environment.SetEnvironmentVariable("REGION_NAME", null); // Ensure no region is set + + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityTenant) + .WithCertificate(s_testCertificate) + .WithExperimentalFeatures() + .Build(); + + // Set WithMtlsProofOfPossession on the request without specifying a region + MsalClientException ex = await AssertException.TaskThrowsAsync(() => + app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() // Enables MTLS PoP + .ExecuteAsync()) + .ConfigureAwait(false); + + Assert.AreEqual(MsalError.MtlsPopWithoutRegion, ex.ErrorCode); + } + } + + [TestMethod] + public async Task MtlsPopWithoutTenantIdAsync() + { + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithCertificate(s_testCertificate) + .WithExperimentalFeatures() + .Build(); + + // Set WithMtlsProofOfPossession on the request without specifying an authority + MsalClientException ex = await AssertException.TaskThrowsAsync(() => + app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync()) + .ConfigureAwait(false); + + Assert.AreEqual(MsalError.MissingTenantedAuthority, ex.ErrorCode); + } + + [TestMethod] + public void Constructor_ValidCertificate() + { + var scheme = new MtlsPopAuthenticationOperation(s_testCertificate); + + // Compute the expected KeyId using SHA-256 on the public key + var expectedKeyId = ComputeExpectedKeyId(s_testCertificate); + + Assert.AreEqual(expectedKeyId, scheme.KeyId); + Assert.AreEqual(Constants.MtlsPoPTokenType, scheme.AccessTokenType); + } + + private static string ComputeExpectedKeyId(X509Certificate2 certificate) + { + // Get the raw public key bytes + var publicKey = certificate.GetPublicKey(); + + // Compute the SHA-256 hash of the public key + using (var sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(publicKey); + return Base64UrlHelpers.Encode(hash); + } + } + + [TestMethod] + public void GetTokenRequestParams_ExpectedValues() + { + var scheme = new MtlsPopAuthenticationOperation(s_testCertificate); + System.Collections.Generic.IReadOnlyDictionary parameters = scheme.GetTokenRequestParams(); + + Assert.AreEqual(Constants.MtlsPoPTokenType, parameters[OAuth2Parameter.TokenType]); + } + + [TestMethod] + public async Task AcquireTokenForClient_WithMtlsProofOfPossession_SuccessAsync() + { + const string region = "eastus"; + + using (var envContext = new EnvVariableContext()) + { + Environment.SetEnvironmentVariable("REGION_NAME", region); + + // Set the expected mTLS endpoint for public cloud + string globalEndpoint = "mtlsauth.microsoft.com"; + string expectedTokenEndpoint = $"https://{region}.{globalEndpoint}/123456-1234-2345-1234561234/oauth2/v2.0/token"; + + using (var httpManager = new MockHttpManager()) + { + // Set up mock handler with expected token endpoint URL + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage( + tokenType: "mtls_pop"); + + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithCertificate(s_testCertificate) + .WithAuthority($"https://login.microsoftonline.com/123456-1234-2345-1234561234") + .WithAzureRegion(ConfidentialClientApplication.AttemptRegionDiscovery) + .WithExperimentalFeatures() + .WithHttpManager(httpManager) + .BuildConcrete(); + + // First token acquisition - should hit the identity provider + AuthenticationResult result = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("header.payload.signature", result.AccessToken); + Assert.AreEqual(Constants.MtlsPoPAuthHeaderPrefix, result.TokenType); + Assert.AreEqual(region, result.AuthenticationResultMetadata.RegionDetails.RegionUsed); + Assert.AreEqual(expectedTokenEndpoint, result.AuthenticationResultMetadata.TokenEndpoint); + + // Second token acquisition - should retrieve from cache + AuthenticationResult secondResult = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("header.payload.signature", secondResult.AccessToken); + Assert.AreEqual(Constants.MtlsPoPAuthHeaderPrefix, secondResult.TokenType); + Assert.AreEqual(TokenSource.Cache, secondResult.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(expectedTokenEndpoint, result.AuthenticationResultMetadata.TokenEndpoint); + } + } + } + + [TestMethod] + public async Task AcquireMtlsPopTokenForClientWithTenantId_SuccessAsync() + { + const string region = "eastus"; + + using (var envContext = new EnvVariableContext()) + { + Environment.SetEnvironmentVariable("REGION_NAME", region); + + // Set the expected mTLS endpoint for public cloud + string globalEndpoint = "mtlsauth.microsoft.com"; + string expectedTokenEndpoint = $"https://{region}.{globalEndpoint}/123456-1234-2345-1234561234/oauth2/v2.0/token"; + + using (var httpManager = new MockHttpManager()) + { + // Set up mock handler with expected token endpoint URL + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage( + tokenType: "mtls_pop"); + + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithCertificate(s_testCertificate) + .WithTenantId("123456-1234-2345-1234561234") + .WithAzureRegion(ConfidentialClientApplication.AttemptRegionDiscovery) + .WithExperimentalFeatures() + .WithHttpManager(httpManager) + .BuildConcrete(); + + // First token acquisition - should hit the identity provider + AuthenticationResult result = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("header.payload.signature", result.AccessToken); + Assert.AreEqual(Constants.MtlsPoPAuthHeaderPrefix, result.TokenType); + Assert.AreEqual(region, result.AuthenticationResultMetadata.RegionDetails.RegionUsed); + Assert.AreEqual(expectedTokenEndpoint, result.AuthenticationResultMetadata.TokenEndpoint); + + // Second token acquisition - should retrieve from cache + AuthenticationResult secondResult = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("header.payload.signature", secondResult.AccessToken); + Assert.AreEqual(Constants.MtlsPoPAuthHeaderPrefix, secondResult.TokenType); + Assert.AreEqual(TokenSource.Cache, secondResult.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(expectedTokenEndpoint, result.AuthenticationResultMetadata.TokenEndpoint); + } + } + } + + [TestMethod] + public async Task MtlsPop_KnownRegionAsync() + { + const string region = "centralus"; + string authorityUrl = "https://login.microsoftonline.com/123456-1234-2345-1234561234"; + string globalEndpoint = "mtlsauth.microsoft.com"; + string expectedTokenEndpoint = $"https://{region}.{globalEndpoint}/123456-1234-2345-1234561234/oauth2/v2.0/token"; + + using (var httpManager = new MockHttpManager()) + { + httpManager.AddRegionDiscoveryMockHandler(region); + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(tokenType: "mtls_pop"); + + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithCertificate(s_testCertificate) + .WithAuthority(authorityUrl) + .WithAzureRegion(region) + .WithHttpManager(httpManager) + .WithExperimentalFeatures() + .BuildConcrete(); + + AuthenticationResult result = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("header.payload.signature", result.AccessToken); + Assert.AreEqual(region, result.AuthenticationResultMetadata.RegionDetails.RegionUsed); + Assert.AreEqual(RegionOutcome.UserProvidedValid, result.ApiEvent.RegionOutcome); + Assert.AreEqual(expectedTokenEndpoint, result.AuthenticationResultMetadata.TokenEndpoint); + } + } + + [TestMethod] + public async Task MtlsPop_RegionalTokenCacheInterchangeabilityAsync() + { + const string region = "centralus"; + string authority = "https://login.microsoftonline.com/123456-1234-2345-1234561234"; + string globalEndpoint = "mtlsauth.microsoft.com"; + string expectedTokenEndpoint = $"https://{region}.{globalEndpoint}/123456-1234-2345-1234561234/oauth2/v2.0/token"; + + using (var httpManager = new MockHttpManager()) + { + httpManager.AddRegionDiscoveryMockHandler(region); + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(tokenType: "mtls_pop"); + + IConfidentialClientApplication regionalApp1 = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithCertificate(s_testCertificate) + .WithAzureRegion(region) + .WithAuthority(authority) + .WithHttpManager(httpManager) + .WithExperimentalFeatures() + .BuildConcrete(); + + IConfidentialClientApplication regionalApp2 = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithCertificate(s_testCertificate) + .WithAzureRegion(region) + .WithAuthority(authority) + .WithHttpManager(httpManager) + .WithExperimentalFeatures() + .BuildConcrete(); + + var memoryTokenCache = new InMemoryTokenCache(); + memoryTokenCache.Bind(regionalApp1.AppTokenCache); + memoryTokenCache.Bind(regionalApp2.AppTokenCache); + + // Acquire a token with one regional configuration + var regionalResult1 = await regionalApp1.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual(region, regionalResult1.AuthenticationResultMetadata.RegionDetails.RegionUsed); + Assert.AreEqual(TokenSource.IdentityProvider, regionalResult1.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(expectedTokenEndpoint, regionalResult1.AuthenticationResultMetadata.TokenEndpoint); + + // Attempt acquisition with the other regional app, should retrieve from cache + var regionalResult2 = await regionalApp2.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual(TokenSource.Cache, regionalResult2.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(region, regionalResult2.AuthenticationResultMetadata.RegionDetails.RegionUsed); + } + } + + [TestMethod] + public async Task MtlsPop_ThrowsExceptionWhenRegionAutoDetectFailsAsync() + { + using (var envContext = new EnvVariableContext()) + { + Environment.SetEnvironmentVariable("REGION_NAME", null); // Ensure no region is set + + using (var httpManager = new MockHttpManager()) + { + httpManager.AddRegionDiscoveryMockHandlerNotFound(); + + ConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithCertificate(s_testCertificate) + .WithAuthority("https://login.microsoftonline.com/123456-1234-2345-1234561234") + .WithAzureRegion(ConfidentialClientApplication.AttemptRegionDiscovery) + .WithExperimentalFeatures() + .WithHttpManager(httpManager) + .BuildConcrete(); + + // Expect an MsalServiceException due to missing region for MTLS POP + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false)) + .ConfigureAwait(false); + + Assert.AreEqual(MsalError.RegionRequiredForMtlsPop, ex.ErrorCode); + Assert.AreEqual(MsalErrorMessage.RegionRequiredForMtlsPopMessage, ex.Message); + } + } + } + + [TestMethod] + [DataTestMethod] + [DataRow("https://contoso.b2clogin.com/tfp/contoso.onmicrosoft.com/B2C_1_signupsignin", "B2C Authority")] + [DataRow("https://contoso.adfs.contoso.com/adfs", "ADFS Authority")] + public async Task MtlsPop_NonAadAuthorityAsync(string authorityUrl, string authorityType) + { + IConfidentialClientApplication app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithCertificate(s_testCertificate) + .WithAuthority(authorityUrl) + .WithExperimentalFeatures() + .Build(); + + // Set WithMtlsProofOfPossession on the request with a non-AAD authority + MsalClientException ex = await AssertException.TaskThrowsAsync(() => + app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() // Enables MTLS PoP + .ExecuteAsync()) + .ConfigureAwait(false); + + Assert.AreEqual(MsalError.InvalidAuthorityType, ex.ErrorCode); + Assert.AreEqual(MsalErrorMessage.MtlsInvalidAuthorityTypeMessage, ex.Message, $"{authorityType} test failed."); + } + + [DataTestMethod] + [DataRow("https://login.microsoftonline.com", "Public Cloud")] + [DataRow("https://login.microsoftonline.us", "Azure Government")] + [DataRow("https://login.partner.microsoftonline.cn", "Azure China")] + public async Task MtlsPop_WithCommonAsync(string authorityUrl, string cloudType) + { + const string region = "eastus"; + + using (var envContext = new EnvVariableContext()) + { + Environment.SetEnvironmentVariable("REGION_NAME", region); + + using (var httpManager = new MockHttpManager()) + { + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithCertificate(s_testCertificate) + .WithAuthority($"{authorityUrl}/common") + .WithAzureRegion(ConfidentialClientApplication.AttemptRegionDiscovery) + .WithExperimentalFeatures() + .WithHttpManager(httpManager) + .BuildConcrete(); + + // Expect an exception due to using /common with MTLS PoP + MsalClientException ex = await Assert.ThrowsExceptionAsync(async () => + await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false)) + .ConfigureAwait(false); + + Assert.AreEqual(MsalError.MissingTenantedAuthority, ex.ErrorCode); + Assert.AreEqual(MsalErrorMessage.MtlsNonTenantedAuthorityNotAllowedMessage, ex.Message); + } + } + } + + [TestMethod] + public async Task MtlsPop_ValidateExpectedUrlAsync() + { + string authorityUrl = "https://login.microsoftonline.com/123456-1234-2345-1234561234"; + + using (var envContext = new EnvVariableContext()) + { + // Arrange + Environment.SetEnvironmentVariable("REGION_NAME", EastUsRegion); + + using (var harness = new MockHttpAndServiceBundle()) + { + var tokenHttpCallHandler = new MockHttpMessageHandler() + { + ExpectedUrl = $"https://eastus.mtlsauth.microsoft.com/123456-1234-2345-1234561234/oauth2/v2.0/token", + ExpectedMethod = HttpMethod.Post, + ResponseMessage = CreateResponse(tokenType : "mtls_pop"), + ExpectedPostData = new Dictionary + { + { OAuth2Parameter.ClientId, "d3adb33f-c0de-ed0c-c0de-deadb33fc0d3" }, + { OAuth2Parameter.Scope, TestConstants.s_scope.AsSingleString() }, + { OAuth2Parameter.GrantType, OAuth2GrantType.ClientCredentials }, + { "token_type", "mtls_pop" } + }, + UnExpectedPostData = new Dictionary + { + { "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" }, + { "client_assertion", "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCIsIng1dCNTMjU2IjoiSnBmTm1PM1lpR2pHQ1pWY..." } + } + }; + + harness.HttpManager.AddMockHandler(tokenHttpCallHandler); + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(authorityUrl) + .WithHttpManager(harness.HttpManager) + .WithAzureRegion(ConfidentialClientApplication.AttemptRegionDiscovery) + .WithCertificate(s_testCertificate) + .WithExperimentalFeatures(true) + .Build(); + + // Act + var result = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.IsNotNull(result.AccessToken); + Assert.AreEqual(EastUsRegion, result.AuthenticationResultMetadata.RegionDetails.RegionUsed); + Assert.AreEqual(RegionOutcome.AutodetectSuccess, result.AuthenticationResultMetadata.RegionDetails.RegionOutcome); + } + } + } + + [DataTestMethod] + [DataRow("login.microsoftonline.com", "mtlsauth.microsoft.com")] + [DataRow("login.microsoftonline.us", "mtlsauth.microsoftonline.us")] + [DataRow("login.usgovcloudapi.net", "mtlsauth.microsoftonline.us")] + [DataRow("login.partner.microsoftonline.cn", "mtlsauth.partner.microsoftonline.cn")] + [DataRow("login.chinacloudapi.cn", "mtlsauth.partner.microsoftonline.cn")] + public async Task PublicAndSovereignCloud_UsesPreferredNetwork_AndNoDiscovery_Async(string inputEnv, string expectedEnv) + { + // Append the input environment to create the authority URL + string authorityUrl = $"https://{inputEnv}/17b189bc-2b81-4ec5-aa51-3e628cbc931b"; + + using (var envContext = new EnvVariableContext()) + { + Environment.SetEnvironmentVariable("REGION_NAME", EastUsRegion); + + using (var harness = new MockHttpAndServiceBundle()) + { + var tokenHttpCallHandler = new MockHttpMessageHandler() + { + ExpectedUrl = $"https://{EastUsRegion}.{expectedEnv}/17b189bc-2b81-4ec5-aa51-3e628cbc931b/oauth2/v2.0/token", + ExpectedMethod = HttpMethod.Post, + ResponseMessage = CreateResponse(tokenType: "mtls_pop") + }; + harness.HttpManager.AddMockHandler(tokenHttpCallHandler); + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(authorityUrl) + .WithHttpManager(harness.HttpManager) + .WithAzureRegion(ConfidentialClientApplication.AttemptRegionDiscovery) + .WithCertificate(s_testCertificate) + .WithExperimentalFeatures(true) + .Build(); + + AuthenticationResult result = await app + .AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("eastus", result.ApiEvent.RegionUsed); + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + + // Verify that the full token endpoint URL was used correctly + string expectedTokenEndpoint = $"https://{EastUsRegion}.{expectedEnv}/17b189bc-2b81-4ec5-aa51-3e628cbc931b/oauth2/v2.0/token"; + Assert.AreEqual(expectedTokenEndpoint, tokenHttpCallHandler.ExpectedUrl); + + // Second token acquisition - should retrieve from cache + AuthenticationResult secondResult = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("header.payload.signature", secondResult.AccessToken); + Assert.AreEqual(Constants.MtlsPoPAuthHeaderPrefix, secondResult.TokenType); + Assert.AreEqual(TokenSource.Cache, secondResult.AuthenticationResultMetadata.TokenSource); + Assert.AreEqual(EastUsRegion, result.ApiEvent.RegionUsed); + Assert.AreEqual(EastUsRegion, result.AuthenticationResultMetadata.RegionDetails.RegionUsed); + Assert.AreEqual(RegionOutcome.AutodetectSuccess, result.AuthenticationResultMetadata.RegionDetails.RegionOutcome); + Assert.AreEqual(null, result.AuthenticationResultMetadata.RegionDetails.AutoDetectionError); + } + } + } + + [TestMethod] + public async Task AcquireTokenForClient_WithMtlsPop_NonStandardCloudAsync() + { + string nonStandardAuthority = "https://login.myLocalAAD.com/123456-1234-2345-1234561234"; + string expectedRegionPrefix = "eastus"; + string mtlsSubdomain = "mtlsauth"; + + string expectedTokenEndpoint = $"https://{expectedRegionPrefix}.{mtlsSubdomain}.mylocalaad.com/123456-1234-2345-1234561234/oauth2/v2.0/token"; + + using (var envContext = new EnvVariableContext()) + { + Environment.SetEnvironmentVariable("REGION_NAME", EastUsRegion); + + using (var harness = new MockHttpAndServiceBundle()) + { + var tokenHttpCallHandler = new MockHttpMessageHandler() + { + ExpectedUrl = expectedTokenEndpoint, + ExpectedMethod = HttpMethod.Post, + ResponseMessage = CreateResponse(tokenType: "mtls_pop") + }; + harness.HttpManager.AddMockHandler(tokenHttpCallHandler); + + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(nonStandardAuthority) + .WithHttpManager(harness.HttpManager) + .WithAzureRegion(ConfidentialClientApplication.AttemptRegionDiscovery) + .WithCertificate(s_testCertificate) + .WithInstanceDiscovery(false) + .WithExperimentalFeatures(true) + .Build(); + + AuthenticationResult result = await app + .AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("header.payload.signature", result.AccessToken); + Assert.AreEqual(Constants.MtlsPoPAuthHeaderPrefix, result.TokenType); + Assert.AreEqual(expectedRegionPrefix, result.ApiEvent.RegionUsed); + Assert.AreEqual(expectedTokenEndpoint, tokenHttpCallHandler.ExpectedUrl); + Assert.AreEqual(RegionOutcome.AutodetectSuccess, result.AuthenticationResultMetadata.RegionDetails.RegionOutcome); + } + } + } + + private static HttpResponseMessage CreateResponse( + string tokenType, + string token = "header.payload.signature", + string expiresIn = "3599") + { + return MockHelpers.CreateSuccessfulClientCredentialTokenResponseMessage(token, expiresIn, tokenType); + } + } +} diff --git a/tests/Microsoft.Identity.Test.Unit/RequestsTests/SilentRequestTests.cs b/tests/Microsoft.Identity.Test.Unit/RequestsTests/SilentRequestTests.cs index bd1654b469..7218fbff17 100644 --- a/tests/Microsoft.Identity.Test.Unit/RequestsTests/SilentRequestTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/RequestsTests/SilentRequestTests.cs @@ -277,7 +277,7 @@ public AuthenticationRequestParameters CreateRequestParams( AuthorityOverride = authorityOverride }; - var requestContext = new RequestContext(ServiceBundle, Guid.NewGuid()); + var requestContext = new RequestContext(ServiceBundle, Guid.NewGuid(), commonParameters.MtlsCertificate); var authority = Microsoft.Identity.Client.Instance.Authority.CreateAuthorityForRequestAsync( requestContext, diff --git a/tests/Microsoft.Identity.Test.Unit/TelemetryTests/HttpTelemetryTests.cs b/tests/Microsoft.Identity.Test.Unit/TelemetryTests/HttpTelemetryTests.cs index 5e1dd55c2c..a0fc353f09 100644 --- a/tests/Microsoft.Identity.Test.Unit/TelemetryTests/HttpTelemetryTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/TelemetryTests/HttpTelemetryTests.cs @@ -752,7 +752,7 @@ private void AssertCurrentTelemetry( Assert.AreEqual(isLegacyCacheEnabled ? "1" : "0", telemetryCategories[2].Split(',')[1]); // is_legacy_cache_enabled - Assert.AreEqual(TelemetryTokenTypeConstants.Bearer.ToString(), telemetryCategories[2].Split(',')[2]); + Assert.AreEqual(TelemetryTokenTypeConstants.Bearer.ToString("D"), telemetryCategories[2].Split(',')[2]); Assert.AreEqual(callerSdkId, telemetryCategories[2].Split(',')[3]); diff --git a/tests/Microsoft.Identity.Test.Unit/WebUITests/DefaultOsBrowserWebUiTests.cs b/tests/Microsoft.Identity.Test.Unit/WebUITests/DefaultOsBrowserWebUiTests.cs index 760a6c82f2..f65a75fe4c 100644 --- a/tests/Microsoft.Identity.Test.Unit/WebUITests/DefaultOsBrowserWebUiTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WebUITests/DefaultOsBrowserWebUiTests.cs @@ -84,7 +84,7 @@ await _tcpInterceptor.Received(1).ListenToSingleRequestAndRespondAsync( public async Task HttpListenerException_Cancellation_Async() { var webUI = CreateTestWebUI(); - var requestContext = new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid()); + var requestContext = new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid(), null); CancellationTokenSource cts = new CancellationTokenSource(); _tcpInterceptor.When(x => x.ListenToSingleRequestAndRespondAsync( @@ -122,7 +122,7 @@ public async Task DefaultOsBrowserWebUi_CustomBrowser_Async() }; var webUI = CreateTestWebUI(options); - var requestContext = new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid()); + var requestContext = new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid(), null); var responseUri = new Uri(TestAuthorizationResponseUri); _tcpInterceptor.ListenToSingleRequestAndRespondAsync( @@ -168,7 +168,7 @@ private async Task AcquireAuthCodeAsync( string responseUriString = TestAuthorizationResponseUri) { // Arrange - var requestContext = new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid()); + var requestContext = new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid(), null); var responseUri = new Uri(responseUriString); _tcpInterceptor.ListenToSingleRequestAndRespondAsync( diff --git a/tests/Microsoft.Identity.Test.Unit/WebUITests/NetCoreWebUIFactoryTests.cs b/tests/Microsoft.Identity.Test.Unit/WebUITests/NetCoreWebUIFactoryTests.cs index e5688a9a98..d8882515d5 100644 --- a/tests/Microsoft.Identity.Test.Unit/WebUITests/NetCoreWebUIFactoryTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WebUITests/NetCoreWebUIFactoryTests.cs @@ -20,7 +20,7 @@ public class NetCoreWebUIFactoryTests { private readonly NetCoreWebUIFactory _webUIFactory = new NetCoreWebUIFactory(); private readonly CoreUIParent _parent = new CoreUIParent(); - private readonly RequestContext _requestContext = new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid()); + private readonly RequestContext _requestContext = new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid(), null); [TestMethod] public void NetCoreFactory_DefaultEmbedded() diff --git a/tests/Microsoft.Identity.Test.Unit/WebUITests/NetDesktopWebUIFactoryTests.cs b/tests/Microsoft.Identity.Test.Unit/WebUITests/NetDesktopWebUIFactoryTests.cs index 3247d0f7bf..074c383c9f 100644 --- a/tests/Microsoft.Identity.Test.Unit/WebUITests/NetDesktopWebUIFactoryTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WebUITests/NetDesktopWebUIFactoryTests.cs @@ -17,7 +17,7 @@ public class NetDesktopWebUIFactoryTests { private readonly NetDesktopWebUIFactory _webUIFactory = new NetDesktopWebUIFactory(); private readonly CoreUIParent _parent = new CoreUIParent(); - private readonly RequestContext _requestContext = new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid()); + private readonly RequestContext _requestContext = new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid(), null); [TestMethod] public void NetFxFactory_DefaultEmbedded() diff --git a/tests/Microsoft.Identity.Test.Unit/WebUITests/WebView2WebUiFactoryTests.cs b/tests/Microsoft.Identity.Test.Unit/WebUITests/WebView2WebUiFactoryTests.cs index 3380da1d94..18a0839026 100644 --- a/tests/Microsoft.Identity.Test.Unit/WebUITests/WebView2WebUiFactoryTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/WebUITests/WebView2WebUiFactoryTests.cs @@ -20,13 +20,13 @@ public class WebView2WebUiFactoryTests { private readonly CoreUIParent _parent = new CoreUIParent(); private readonly RequestContext _requestContextAad = - new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid()); + new RequestContext(TestCommon.CreateDefaultServiceBundle(), Guid.NewGuid(), null); private readonly RequestContext _requestContextB2C = - new RequestContext(TestCommon.CreateDefaultB2CServiceBundle(), Guid.NewGuid()); + new RequestContext(TestCommon.CreateDefaultB2CServiceBundle(), Guid.NewGuid(), null); private readonly RequestContext _requestContextAdfs = - new RequestContext(TestCommon.CreateDefaultAdfsServiceBundle(), Guid.NewGuid()); + new RequestContext(TestCommon.CreateDefaultAdfsServiceBundle(), Guid.NewGuid(), null); [TestMethod] #if ONEBRANCH_BUILD diff --git a/tests/Microsoft.Identity.Test.Unit/pop/PoPTests.cs b/tests/Microsoft.Identity.Test.Unit/pop/PoPTests.cs index 451427c8a5..0536b148b7 100644 --- a/tests/Microsoft.Identity.Test.Unit/pop/PoPTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/pop/PoPTests.cs @@ -59,7 +59,6 @@ public async Task POP_ShrValidation_Async() ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithClientSecret(TestConstants.ClientSecret) .WithHttpManager(httpManager) - .WithExperimentalFeatures(true) .BuildConcrete(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(ProtectedUrl)); @@ -72,7 +71,7 @@ public async Task POP_ShrValidation_Async() // Act var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) .WithTenantId(TestConstants.Utid) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync() .ConfigureAwait(false); @@ -93,7 +92,6 @@ public async Task POP_NoHttpRequest_Async() ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithClientSecret(TestConstants.ClientSecret) .WithHttpManager(httpManager) - .WithExperimentalFeatures(true) .BuildConcrete(); // no HTTP method binding, but custom nonce @@ -106,7 +104,7 @@ public async Task POP_NoHttpRequest_Async() // Act var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) .WithTenantId(TestConstants.Utid) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync() .ConfigureAwait(false); @@ -131,7 +129,6 @@ public async Task POP_WithCustomNonce_Async() ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithClientSecret(TestConstants.ClientSecret) .WithHttpManager(httpManager) - .WithExperimentalFeatures(true) .BuildConcrete(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(ProtectedUrl)); @@ -143,7 +140,7 @@ public async Task POP_WithCustomNonce_Async() var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) .WithTenantId(TestConstants.Utid) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync() .ConfigureAwait(false); @@ -407,7 +404,6 @@ public async Task CacheKey_Includes_POPKid_Async() ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithClientSecret(TestConstants.ClientSecret) .WithHttpManager(httpManager) - .WithExperimentalFeatures(true) .BuildConcrete(); var testTimeService = new TestTimeService(); PoPCryptoProviderFactory.TimeService = testTimeService; @@ -423,7 +419,7 @@ public async Task CacheKey_Includes_POPKid_Async() Trace.WriteLine("1. AcquireTokenForClient "); var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) .WithTenantId(TestConstants.Utid) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync() .ConfigureAwait(false); @@ -450,7 +446,7 @@ public async Task CacheKey_Includes_POPKid_Async() Trace.WriteLine("1. AcquireTokenForClient again, after time passes - expect POP key rotation"); result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) .WithTenantId(TestConstants.Utid) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync() .ConfigureAwait(false); @@ -584,7 +580,6 @@ public async Task POP_SignatureValidationWithPS256_Async() ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithClientSecret(TestConstants.ClientSecret) .WithHttpManager(httpManager) - .WithExperimentalFeatures(true) .BuildConcrete(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(ProtectedUrl)); @@ -600,7 +595,7 @@ public async Task POP_SignatureValidationWithPS256_Async() // Act var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) .WithTenantId(TestConstants.Utid) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync() .ConfigureAwait(false); @@ -653,7 +648,6 @@ public async Task TokenGenerationAndValidation_Async() ConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithClientSecret(TestConstants.ClientSecret) .WithHttpManager(httpManager) - .WithExperimentalFeatures(true) .BuildConcrete(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(ProtectedUrl)); @@ -666,7 +660,7 @@ public async Task TokenGenerationAndValidation_Async() // Act var result = await app.AcquireTokenForClient(TestConstants.s_scope.ToArray()) .WithTenantId(TestConstants.Utid) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync() .ConfigureAwait(false); diff --git a/tests/Microsoft.Identity.Test.Unit/pop/PopAuthenticationOperationTests.cs b/tests/Microsoft.Identity.Test.Unit/pop/PopAuthenticationOperationTests.cs index b512c6d8ef..7cacc8c614 100644 --- a/tests/Microsoft.Identity.Test.Unit/pop/PopAuthenticationOperationTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/pop/PopAuthenticationOperationTests.cs @@ -113,7 +113,6 @@ public async Task ValidateKeyExpirationAsync() var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithHttpManager(harness.HttpManager) - .WithExperimentalFeatures() .WithClientSecret("some-secret") .BuildConcrete(); @@ -132,7 +131,7 @@ public async Task ValidateKeyExpirationAsync() PoPCryptoProviderFactory.TimeService = testClock; var result = await app.AcquireTokenForClient(TestConstants.s_scope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); var initialToken = result.AccessToken; @@ -142,7 +141,7 @@ public async Task ValidateKeyExpirationAsync() PoPCryptoProviderFactory.TimeService = testClock; result = await app.AcquireTokenForClient(TestConstants.s_scope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); @@ -155,7 +154,7 @@ public async Task ValidateKeyExpirationAsync() PoPCryptoProviderFactory.TimeService = testClock; result = await app.AcquireTokenForClient(TestConstants.s_scope) - .WithProofOfPossession(popConfig) + .WithSignedHttpRequestProofOfPossession(popConfig) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); diff --git a/tests/devapps/NetCoreTestApp/Program.cs b/tests/devapps/NetCoreTestApp/Program.cs index 5244324ead..b9a85197e9 100644 --- a/tests/devapps/NetCoreTestApp/Program.cs +++ b/tests/devapps/NetCoreTestApp/Program.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Security; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; @@ -32,6 +33,9 @@ public class Program // App secret for app above private static string s_confidentialClientSecret; + // App certificate for app above + private static X509Certificate2 s_confidentialClientCertificate; + private static string s_ccaAuthority; private static readonly IEnumerable s_scopes = new[] { @@ -62,6 +66,7 @@ public static void Main(string[] args) s_clientIdForConfidentialApp = ccaSettings.ClientId; s_ccaAuthority = ccaSettings.Authority; s_confidentialClientSecret = ccaSettings.GetSecret(); + s_confidentialClientCertificate = ccaSettings.GetCertificate(); var pca = CreatePca(); RunConsoleAppLogicAsync(pca).Wait(); @@ -152,6 +157,7 @@ 11. AcquireTokenForClient with multiple threads 12. Acquire Token Interactive with Broker 13. Acquire Token using Managed Identity (VM) 14. Acquire Token using Managed Identity (VM) - multiple requests in parallel + 15. Acquire Confidential Client Token over MTLS SNI + MTLS 0. Exit App Enter your Selection: "); int.TryParse(Console.ReadLine(), out var selection); @@ -367,6 +373,20 @@ 0. Exit App break; + case 15: //acquire token with cert over MTLS SNI + MTLS + + var cca1 = CreateCcaForMtlsPop("westus3", s_confidentialClientCertificate); + + var resultX1 = await cca1.AcquireTokenForClient(GraphAppScope) + .WithMtlsProofOfPossession() + .WithExtraQueryParameters("dc=ESTSR-PUB-WUS3-AZ1-TEST1&slice=TestSlice") //Feature in test slice + .ExecuteAsync() + .ConfigureAwait(false); + + Console.WriteLine("Got a token"); + Console.WriteLine("Finished"); + break; + case 0: return; @@ -385,19 +405,56 @@ 0. Exit App } } - private static IConfidentialClientApplication CreateCca() + private static IConfidentialClientApplication CreateCca(X509Certificate2 certificate = null) { - IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder + ConfidentialClientApplicationBuilder ccaBuilder = ConfidentialClientApplicationBuilder .Create(s_clientIdForConfidentialApp) - .WithAuthority(s_ccaAuthority) - .WithClientSecret(s_confidentialClientSecret) - .Build(); + .WithAuthority(s_ccaAuthority); + + // Use WithCertificate if a certificate is provided; otherwise, use WithClientSecret. + if (certificate != null) + { + ccaBuilder = ccaBuilder.WithCertificate(certificate); + } + else + { + ccaBuilder = ccaBuilder.WithClientSecret(s_confidentialClientSecret); + } + + IConfidentialClientApplication ccapp = ccaBuilder.Build(); + + // Optionally set cache settings or other configurations if needed + // cca.AppTokenCache.SetBeforeAccess((t) => { }); + + return ccapp; + } + + private static IConfidentialClientApplication CreateCcaForMtlsPop(string region, X509Certificate2 certificate = null) + { + ConfidentialClientApplicationBuilder ccaBuilder = ConfidentialClientApplicationBuilder + .Create("163ffef9-a313-45b4-ab2f-c7e2f5e0e23e") + .WithAuthority("https://login.microsoftonline.com/bea21ebe-8b64-4d06-9f6d-6a889b120a7c") + .WithAzureRegion(region); + + // Use WithCertificate if a certificate is provided; otherwise, use WithClientSecret. + if (certificate != null) + { + ccaBuilder = ccaBuilder.WithCertificate(certificate, true); + + //Add Experimental feature for MTLS PoP + ccaBuilder = ccaBuilder.WithExperimentalFeatures(); + } + else + { + ccaBuilder = ccaBuilder.WithClientSecret(s_confidentialClientSecret); + } - //cca.AppTokenCache.SetBeforeAccess((t) => { }); + IConfidentialClientApplication ccapp = ccaBuilder.Build(); - //cca.AcquireTokenForClient(new[] "12345-123321-1111/default"); + // Optionally set cache settings or other configurations if needed + // cca.AppTokenCache.SetBeforeAccess((t) => { }); - return cca; + return ccapp; } private static async Task FetchTokenAndCallGraphAsync(IPublicClientApplication pca, Task authTask)