Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public async Task<AuthenticationResult> ExecuteAsync(
AcquireTokenForClientParameters clientParameters,
CancellationToken cancellationToken)
{
await commonParameters.InitMtlsPopParametersAsync(ServiceBundle, cancellationToken)
await commonParameters.TryInitMtlsPopParametersAsync(ServiceBundle, cancellationToken)
.ConfigureAwait(false);

RequestContext requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,81 +42,25 @@ internal class AcquireTokenCommonParameters
public string ClientAssertionFmiPath { get; internal set; }
public bool IsMtlsPopRequested { get; set; }
public string ExtraClientAssertionClaims { get; internal set; }

/// <summary>
/// Optional delegate for obtaining attestation JWT for Credential Guard keys.
/// Set by the KeyAttestation package via .WithAttestationSupport().
/// Returns null for non-attested flows.
/// </summary>
public Func<string, SafeHandle, string, CancellationToken, Task<string>> AttestationTokenProvider { get; set; }

internal async Task InitMtlsPopParametersAsync(IServiceBundle serviceBundle, CancellationToken ct)
{
if (!IsMtlsPopRequested)
{
return; // PoP not requested
}

// ────────────────────────────────────
// Case 1 – Certificate credential
// ────────────────────────────────────
if (serviceBundle.Config.ClientCredential is CertificateClientCredential certCred)
{
if (certCred.Certificate == null)
{
throw new MsalClientException(
MsalError.MtlsCertificateNotProvided,
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
}

return;
}

// ────────────────────────────────────
// Case 2 – Client‑assertion delegate
// ────────────────────────────────────
if (serviceBundle.Config.ClientCredential is ClientAssertionDelegateCredential cadc)
{
var opts = new AssertionRequestOptions
{
ClientID = serviceBundle.Config.ClientId,
ClientCapabilities = serviceBundle.Config.ClientCapabilities,
Claims = Claims,
CancellationToken = ct
};

ClientSignedAssertion ar = await cadc.GetAssertionAsync(opts, ct).ConfigureAwait(false);

if (ar.TokenBindingCertificate == null)
{
throw new MsalClientException(
MsalError.MtlsCertificateNotProvided,
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
}

InitMtlsPopParameters(ar.TokenBindingCertificate, serviceBundle);
return;
}

// ────────────────────────────────────
// Case 3 – Any other credential (client‑secret etc.)
// ────────────────────────────────────
throw new MsalClientException(
MsalError.MtlsCertificateNotProvided,
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
}

private void InitMtlsPopParameters(X509Certificate2 cert, IServiceBundle serviceBundle)
/// <summary>
/// This tries to see if the token request should be done over mTLS or over normal HTTP
/// and set the correct parameters
/// </summary>
/// <param name="serviceBundle"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="MsalClientException"></exception>
internal async Task TryInitMtlsPopParametersAsync(IServiceBundle serviceBundle, CancellationToken ct)
{
// region check (AAD only)
if (serviceBundle.Config.Authority.AuthorityInfo.AuthorityType == AuthorityType.Aad &&
serviceBundle.Config.AzureRegion == null)
{
throw new MsalClientException(MsalError.MtlsPopWithoutRegion, MsalErrorMessage.MtlsPopWithoutRegion);
}

AuthenticationOperation = new MtlsPopAuthenticationOperation(cert);
MtlsCertificate = cert;
await MtlsPopParametersInitializer.TryInitAsync(this, serviceBundle, ct).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Threading;
using System.Threading.Tasks;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Client.AuthScheme.PoP;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Internal.ClientCredential;
using Microsoft.Identity.Client.TelemetryCore;

namespace Microsoft.Identity.Client.ApiConfig.Parameters
{
/// <summary>
/// Encapsulates the mTLS/PoP initialization logic for token requests.
/// Keeps AcquireTokenCommonParameters lean and makes the init logic testable in isolation.
/// </summary>
internal static class MtlsPopParametersInitializer
{
internal static async Task TryInitAsync(
AcquireTokenCommonParameters p,
IServiceBundle serviceBundle,
CancellationToken ct)
{
if (p.IsMtlsPopRequested)
{
await InitExplicitMtlsPopAsync(p, serviceBundle, ct).ConfigureAwait(false);
return;
}

await TryInitImplicitBearerOverMtlsAsync(p, serviceBundle, ct).ConfigureAwait(false);
}

/// <summary>
/// NON-PoP request:
/// We may still need mTLS transport if the credential can return a TokenBindingCertificate.
/// </summary>
private static async Task TryInitImplicitBearerOverMtlsAsync(
AcquireTokenCommonParameters tokenParameters,
IServiceBundle serviceBundle,
CancellationToken ct)
{
if (tokenParameters.MtlsCertificate != null)
{
ThrowIfRegionMissingForImplicitMtls(serviceBundle);
return;
}

// Only cert-capable credentials implement this capability interface.
if (serviceBundle.Config.ClientCredential is IClientSignedAssertionProvider signedProvider)
{
var opts = CreateAssertionRequestOptions(tokenParameters, serviceBundle, ct);

ClientSignedAssertion ar =
await signedProvider.GetAssertionAsync(opts, ct).ConfigureAwait(false);

if (ar?.TokenBindingCertificate != null)
{
tokenParameters.MtlsCertificate = ar.TokenBindingCertificate;
ThrowIfRegionMissingForImplicitMtls(serviceBundle);
}
}
}

/// <summary>
/// EXPLICIT PoP requested:
/// Validate and initialize PoP parameters (auth scheme + cert + region check).
/// </summary>
private static async Task InitExplicitMtlsPopAsync(
AcquireTokenCommonParameters p,
IServiceBundle serviceBundle,
CancellationToken ct)
{
// Case 1 – Certificate credential
if (serviceBundle.Config.ClientCredential is CertificateClientCredential certCred)
{
if (certCred.Certificate == null)
{
throw new MsalClientException(
MsalError.MtlsCertificateNotProvided,
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
}

InitMtlsPopParameters(p, certCred.Certificate, serviceBundle);
return;
}

// Case 2 – Signed assertion provider (JWT + optional cert)
if (serviceBundle.Config.ClientCredential is IClientSignedAssertionProvider signedProvider)
{
var opts = CreateAssertionRequestOptions(p, serviceBundle, ct);

ClientSignedAssertion ar =
await signedProvider.GetAssertionAsync(opts, ct).ConfigureAwait(false);

if (ar?.TokenBindingCertificate == null)
{
throw new MsalClientException(
MsalError.MtlsCertificateNotProvided,
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
}

InitMtlsPopParameters(p, ar.TokenBindingCertificate, serviceBundle);
return;
}

// Case 3 – Any other credential (client-secret etc.)
throw new MsalClientException(
MsalError.MtlsCertificateNotProvided,
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
}

private static AssertionRequestOptions CreateAssertionRequestOptions(
AcquireTokenCommonParameters p,
IServiceBundle serviceBundle,
CancellationToken ct)
{
return new AssertionRequestOptions
{
ClientID = serviceBundle.Config.ClientId,
ClientCapabilities = serviceBundle.Config.ClientCapabilities,
Claims = p.Claims,
CancellationToken = ct,
ClientAssertionFmiPath = p.ClientAssertionFmiPath,

// Best-effort context. IMPORTANT: use AbsoluteUri, not Uri.Authority (host only).
TokenEndpoint = serviceBundle.Config.Authority.AuthorityInfo.CanonicalAuthority.AbsoluteUri
};
}

private static void InitMtlsPopParameters(
AcquireTokenCommonParameters p,
X509Certificate2 cert,
IServiceBundle serviceBundle)
{
// region check (AAD only)
if (serviceBundle.Config.Authority.AuthorityInfo.AuthorityType == AuthorityType.Aad &&
serviceBundle.Config.AzureRegion == null)
{
throw new MsalClientException(
MsalError.MtlsPopWithoutRegion,
MsalErrorMessage.MtlsPopWithoutRegion);
}

p.AuthenticationOperation = new MtlsPopAuthenticationOperation(cert);
p.MtlsCertificate = cert;
}

private static void ThrowIfRegionMissingForImplicitMtls(IServiceBundle serviceBundle)
{
// Implicit bearer-over-mTLS requires region only for AAD
if (serviceBundle.Config.Authority.AuthorityInfo.AuthorityType == AuthorityType.Aad &&
serviceBundle.Config.AzureRegion == null)
{
throw new MsalClientException(
MsalError.MtlsBearerWithoutRegion,
MsalErrorMessage.MtlsBearerWithoutRegion);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -274,12 +274,9 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(Func<string> cli
throw new ArgumentNullException(nameof(clientAssertionDelegate));
}

// String assertion => cannot return TokenBindingCertificate => use string credential
return WithClientAssertionInternal(
(opts, ct) =>
Task.FromResult(new ClientSignedAssertion
{
Assertion = clientAssertionDelegate() // bearer
}));
(opts, ct) => Task.FromResult(clientAssertionDelegate()));
}

/// <summary>
Expand All @@ -297,12 +294,9 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(Func<Cancellatio
throw new ArgumentNullException(nameof(clientAssertionAsyncDelegate));
}

// String assertion => cannot return TokenBindingCertificate => use string credential
return WithClientAssertionInternal(
async (opts, ct) =>
{
string jwt = await clientAssertionAsyncDelegate(ct).ConfigureAwait(false);
return new ClientSignedAssertion { Assertion = jwt }; // bearer
});
(opts, ct) => clientAssertionAsyncDelegate(ct));
}

/// <summary>
Expand All @@ -319,12 +313,9 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(Func<AssertionRe
throw new ArgumentNullException(nameof(clientAssertionAsyncDelegate));
}

// String assertion => cannot return TokenBindingCertificate => use string credential
return WithClientAssertionInternal(
async (opts, _) =>
{
string jwt = await clientAssertionAsyncDelegate(opts).ConfigureAwait(false);
return new ClientSignedAssertion { Assertion = jwt }; // bearer
});
(opts, ct) => clientAssertionAsyncDelegate(opts));
}

/// <summary>
Expand All @@ -343,7 +334,8 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(Func<AssertionRe
CancellationToken, Task<ClientSignedAssertion>> clientSignedAssertionProvider)
{
ValidateUseOfExperimentalFeature();
return WithClientAssertionInternal(clientSignedAssertionProvider);
return WithClientAssertionInternal(
clientSignedAssertionProvider: clientSignedAssertionProvider);
}

/// <summary>
Expand All @@ -358,6 +350,18 @@ internal ConfidentialClientApplicationBuilder WithClientAssertionInternal(
return this;
}

/// <summary>
/// Internal helper to set the client assertion provider that returns a string.
/// </summary>
/// <param name="clientAssertionProvider"></param>
/// <returns></returns>
internal ConfidentialClientApplicationBuilder WithClientAssertionInternal(
Func<AssertionRequestOptions, CancellationToken, Task<string>> clientAssertionProvider)
{
Config.ClientCredential = new ClientAssertionStringDelegateCredential(clientAssertionProvider);
return this;
}

/// <summary>
/// Instructs MSAL to use an Azure regional token service. This feature is currently available to
/// first-party applications only.
Expand Down
Loading