Skip to content

Commit

Permalink
Implementation for SNI + MTLS Flow in MSAL (#4965)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Apply suggestions from code review

Co-authored-by: Den Delimarsky <[email protected]>

* 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 <[email protected]>
Co-authored-by: Den Delimarsky <[email protected]>
  • Loading branch information
3 people authored Jan 2, 2025
1 parent eb59900 commit aef501f
Show file tree
Hide file tree
Showing 74 changed files with 1,325 additions and 209 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,11 +44,11 @@ public override Task<AuthenticationResult> ExecuteAsync(CancellationToken cancel
/// </summary>
/// <exception cref="MsalClientException"></exception>
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(
Expand All @@ -73,12 +70,14 @@ protected override void Validate()
/// <returns>The builder.</returns>
/// <remarks>
/// <list type="bullet">
/// <item><description>An Authentication header is automatically added to the request.</description></item>
/// <item><description>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).</description></item>
/// <item><description>MSAL creates, reads and stores a key in memory that will be cycled every 8 hours.</description></item>
/// <item><description>This is an experimental API. The method signature may change in the future without involving a major version upgrade.</description></item>
/// </list>
/// </remarks>
[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();
Expand All @@ -89,5 +88,28 @@ public T WithProofOfPossession(PoPAuthenticationConfiguration popAuthenticationC

return this as T;
}

/// <summary>
/// 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 <see href="https://aka.ms/mtls-pop"/> for details.
/// </summary>
/// <param name="popAuthenticationConfiguration">Configuration properties used to construct a Proof-of-Possession request.</param>
/// <returns>The builder.</returns>
/// <remarks>
/// <list type="bullet">
/// <item><description>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.</description></item>
/// <item><description>MSAL creates, reads, and stores a key in memory that will be cycled every 8 hours.</description></item>
/// <item><description>This is an experimental API. The method signature may change in the future without involving a major version upgrade.</description></item>
/// </list>
/// </remarks>
public T WithSignedHttpRequestProofOfPossession(PoPAuthenticationConfiguration popAuthenticationConfiguration)
{
CommonParameters.PopAuthenticationConfiguration = popAuthenticationConfiguration ?? throw new ArgumentNullException(nameof(popAuthenticationConfiguration));

CommonParameters.AuthenticationOperation = new PopAuthenticationOperation(CommonParameters.PopAuthenticationConfiguration, ServiceBundle);

return this as T;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -74,6 +78,30 @@ public AcquireTokenForClientParameterBuilder WithSendX5C(bool withSendX5C)
return this;
}

/// <summary>
/// Specifies that the certificate provided will be used for PoP tokens with mTLS (Mutual TLS) authentication.
/// For more information, refer to the <see href="https://aka.ms/mtls-pop">Proof-of-Possession documentation</see>.
/// </summary>
/// <returns>The current instance of <see cref="AcquireTokenForClientParameterBuilder"/> to enable method chaining.</returns>
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;
}

/// <summary>
/// Please use WithAzureRegion on the ConfidentialClientApplicationBuilder object
/// </summary>
Expand Down Expand Up @@ -103,7 +131,34 @@ internal override Task<AuthenticationResult> ExecuteInternalAsync(CancellationTo
/// <inheritdoc/>
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public async Task<AuthenticationResult> 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,
Expand All @@ -46,7 +46,7 @@ public async Task<AuthenticationResult> 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<string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -33,9 +34,9 @@ public async Task<AuthenticationResult> 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);
Expand All @@ -54,9 +55,9 @@ public async Task<AuthenticationResult> 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);
Expand All @@ -76,9 +77,9 @@ public async Task<AuthenticationResult> 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);
Expand All @@ -100,9 +101,9 @@ public async Task<Uri> 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);
Expand Down Expand Up @@ -136,9 +137,9 @@ public async Task<AuthenticationResult> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public async Task<AuthenticationResult> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public async Task<AuthenticationResult> 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,
Expand All @@ -47,7 +47,7 @@ public async Task<AuthenticationResult> 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,
Expand All @@ -67,7 +67,7 @@ public async Task<AuthenticationResult> 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,
Expand All @@ -87,7 +87,7 @@ public async Task<AuthenticationResult> 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ public string ClientVersion

public Func<AppTokenProviderParameters, Task<AppTokenProviderResult>> AppTokenProvider;

#region ClientCredentials
#region ClientCredentials

// Indicates if claims or assertions are used within the configuration
public IClientCredential ClientCredential { get; internal set; }

/// <summary>
Expand Down
Loading

0 comments on commit aef501f

Please sign in to comment.