Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 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 @@ -48,149 +48,111 @@ public CertificateAndClaimsClientCredential(
Certificate = certificate;
}

public async Task<ClientCredentialApplicationResult> AddConfidentialClientParametersAsync(
OAuth2Client oAuth2Client,
AuthenticationRequestParameters requestParameters,
ICryptographyManager cryptographyManager,
string tokenEndpoint,
public async Task<CredentialMaterial> GetCredentialMaterialAsync(
CredentialRequestContext requestContext,
CancellationToken cancellationToken)
{
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 mTLS cert is not already set for the request, proceed with JWT bearer client assertion.
if (requestParameters.MtlsCertificate == null)
// Resolve the certificate via the provider
var opts = new AssertionRequestOptions
{
requestParameters.RequestContext.Logger.Verbose(
() => "Proceeding with JWT token creation and adding client assertion.");

// Resolve the certificate via the provider
X509Certificate2 certificate =
await ResolveCertificateAsync(requestParameters, tokenEndpoint, cancellationToken)
.ConfigureAwait(false);

// Store the resolved certificate in request parameters for later use (e.g., ExecutionResult)
requestParameters.ResolvedCertificate = certificate;

bool useSha2 = requestParameters.AuthorityManager.Authority.AuthorityInfo.IsSha2CredentialSupported;

JsonWebToken jwtToken;
if (string.IsNullOrEmpty(requestParameters.ExtraClientAssertionClaims))
{
jwtToken = new JsonWebToken(
cryptographyManager,
clientId,
tokenEndpoint,
_claimsToSign,
_appendDefaultClaims);
}
else
{
jwtToken = new JsonWebToken(
cryptographyManager,
clientId,
tokenEndpoint,
requestParameters.ExtraClientAssertionClaims,
_appendDefaultClaims);
}

string assertion = jwtToken.Sign(certificate, requestParameters.SendX5C, useSha2);

oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer);
oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, assertion);

// No extra outputs for the common case.
return ClientCredentialApplicationResult.None;
}

// mTLS path: a certificate is already set on the request (e.g., mTLS/PoP transport).
requestParameters.RequestContext.Logger.Verbose(
() => "mTLS certificate is set for this request. Skipping JWT client assertion generation.");

requestParameters.ResolvedCertificate = requestParameters.MtlsCertificate;

// Return the mTLS certificate via the result object so the pipeline can use it
// (HTTP handler + policy/region checks).
return new ClientCredentialApplicationResult
{
MtlsCertificate = requestParameters.MtlsCertificate,
UseJwtPopClientAssertion = false // no client assertion set here
CancellationToken = cancellationToken,
ClientID = requestContext.ClientId,
TokenEndpoint = requestContext.TokenEndpoint,
ClientCapabilities = requestContext.ClientCapabilities,
Claims = requestContext.Claims
};
}

/// <summary>
/// Resolves the certificate to use for signing the client assertion.
/// Invokes the certificate provider delegate to get the certificate.
/// </summary>
/// <param name="requestParameters">The authentication request parameters containing app config</param>
/// <param name="tokenEndpoint">The token endpoint URL</param>
/// <param name="cancellationToken">Cancellation token for the async operation</param>
/// <returns>The X509Certificate2 to use for signing</returns>
/// <exception cref="MsalClientException">Thrown if the certificate provider returns null or an invalid certificate</exception>
private async Task<X509Certificate2> ResolveCertificateAsync(
AuthenticationRequestParameters requestParameters,
string tokenEndpoint,
CancellationToken cancellationToken)
{
requestParameters.RequestContext.Logger.Verbose(
() => "[CertificateAndClaimsClientCredential] Resolving certificate from provider.");
X509Certificate2 cert = await _certificateProvider(opts).ConfigureAwait(false);

// Create AssertionRequestOptions for the callback
var options = new AssertionRequestOptions(
requestParameters.AppConfig,
tokenEndpoint,
requestParameters.AuthorityManager.Authority.TenantId)
if (cert == null)
{
Claims = requestParameters.Claims,
ClientCapabilities = requestParameters.AppConfig.ClientCapabilities,
CancellationToken = cancellationToken
};

// Invoke the provider to get the certificate
X509Certificate2 certificate = await _certificateProvider(options).ConfigureAwait(false);

// Validate the certificate returned by the provider
if (certificate == null)
{
requestParameters.RequestContext.Logger.Error(
"[CertificateAndClaimsClientCredential] Certificate provider returned null.");

throw new MsalClientException(
MsalError.InvalidClientAssertion,
"The certificate provider callback returned null. Ensure the callback returns a valid X509Certificate2 instance.");
}

// Validate certificate has private key (may throw CryptographicException if cert is disposed)
try
{
if (!certificate.HasPrivateKey)
if (!cert.HasPrivateKey)
{
requestParameters.RequestContext.Logger.Error(
"[CertificateAndClaimsClientCredential] Certificate from provider does not have a private key.");

throw new MsalClientException(
MsalError.CertWithoutPrivateKey,
MsalErrorMessage.CertMustHavePrivateKey(certificate.FriendlyName));
MsalErrorMessage.CertMustHavePrivateKey(cert.FriendlyName));
}
}
catch (System.Security.Cryptography.CryptographicException ex)
{
requestParameters.RequestContext.Logger.Error(
"[CertificateAndClaimsClientCredential] A cryptographic error occurred while accessing the certificate.");
throw new MsalClientException(
MsalError.CryptographicError,
MsalErrorMessage.CryptographicError,
ex);
}

// If in mTLS bearer mode, skip JWT assertion - certificate will be used for TLS client auth only
if (requestContext.MtlsBearerMode)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: its a bit confusing name requestContext Maybe renaming it to credentialContext brings clarity

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, not a nit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carried forward in #5835; CredentialRequestContext is gone and CredentialContext is the active type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the meaning of MtlsBearerMode?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which endpoint is used in this case? Mtls?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This terminology is clarified in #5835 through the credential-matrix tests and the OAuthMode enum, which makes the supported mode combinations explicit.

This was carried forward in #5835; the token endpoint is resolved once in TokenClient and then flowed through credential resolution.

{
return new CredentialMaterial(
tokenRequestParameters: new Dictionary<string, string>(), // Empty - no client_assertion
mtlsCertificate: cert,
metadata: new CredentialMaterialMetadata(
credentialType: CredentialType.ClientCertificate,
credentialSource: Certificate == null ? "dynamic" : "static",
mtlsCertificateIdHashPrefix: CredentialMaterialHelper.GetCertificateIdHashPrefix(cert),
mtlsCertificateRequested: requestContext.MtlsRequired,
resolutionTimeMs: 0));
}

// Build JWT assertion
JsonWebToken jwtToken;
if (!string.IsNullOrEmpty(requestContext.ExtraClientAssertionClaims))
{
// ExtraClientAssertionClaims takes precedence (e.g., for cache key binding)
jwtToken = new JsonWebToken(
requestContext.CryptographyManager,
requestContext.ClientId,
requestContext.TokenEndpoint,
requestContext.ExtraClientAssertionClaims,
_appendDefaultClaims);
}
else
{
jwtToken = new JsonWebToken(
requestContext.CryptographyManager,
requestContext.ClientId,
requestContext.TokenEndpoint,
_claimsToSign,
_appendDefaultClaims);
}

string assertion;
try
{
assertion = jwtToken.Sign(cert, requestContext.SendX5C, requestContext.UseSha2);
}
catch (System.Security.Cryptography.CryptographicException ex)
{
throw new MsalClientException(
MsalError.CryptographicError,
MsalErrorMessage.CryptographicError,
ex);
}

requestParameters.RequestContext.Logger.Info(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this log for logging if the cert was resolved. Can you add this back when the cert is resolved?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in #5835

() => $"[CertificateAndClaimsClientCredential] Successfully resolved certificate from provider. " +
$"Thumbprint: {certificate.Thumbprint}");
var tokenParameters = new Dictionary<string, string>
{
{ OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer },
{ OAuth2Parameter.ClientAssertion, assertion }
};

return certificate;
return new CredentialMaterial(
tokenRequestParameters: tokenParameters,
mtlsCertificate: cert,
metadata: new CredentialMaterialMetadata(
credentialType: CredentialType.ClientCertificate,
credentialSource: Certificate == null ? "dynamic" : "static",
mtlsCertificateIdHashPrefix: CredentialMaterialHelper.GetCertificateIdHashPrefix(cert),
mtlsCertificateRequested: requestContext.MtlsRequired,
resolutionTimeMs: 0));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;
Expand Down Expand Up @@ -40,27 +41,22 @@ Task<ClientSignedAssertion> IClientSignedAssertionProvider.GetAssertionAsync(

public AssertionType AssertionType => AssertionType.ClientAssertion;

// ──────────────────────────────────
// Main hook for token requests
// ──────────────────────────────────
public async Task<ClientCredentialApplicationResult> AddConfidentialClientParametersAsync(
OAuth2Client oAuth2Client,
AuthenticationRequestParameters p,
ICryptographyManager _,
string tokenEndpoint,
CancellationToken ct)
public async Task<CredentialMaterial> GetCredentialMaterialAsync(
CredentialRequestContext requestContext,
CancellationToken cancellationToken)
{
var opts = new AssertionRequestOptions
{
CancellationToken = ct,
ClientID = p.AppConfig.ClientId,
TokenEndpoint = tokenEndpoint,
ClientCapabilities = p.RequestContext.ServiceBundle.Config.ClientCapabilities,
Claims = p.Claims,
ClientAssertionFmiPath = p.ClientAssertionFmiPath
CancellationToken = cancellationToken,
ClientID = requestContext.ClientId,
TokenEndpoint = requestContext.TokenEndpoint,
ClientCapabilities = requestContext.ClientCapabilities,
Claims = requestContext.Claims,
ClientAssertionFmiPath = requestContext.ClientAssertionFmiPath
};

ClientSignedAssertion resp = await GetAssertionAsync(opts, ct).ConfigureAwait(false);
ClientSignedAssertion resp = await GetAssertionAsync(opts, cancellationToken)
.ConfigureAwait(false);

if (string.IsNullOrWhiteSpace(resp?.Assertion))
{
Expand All @@ -69,30 +65,29 @@ public async Task<ClientCredentialApplicationResult> AddConfidentialClientParame
MsalErrorMessage.InvalidClientAssertionEmpty);
}

bool hasCert = resp.TokenBindingCertificate != null;
// Use jwt-pop if TokenBindingCertificate is present (assertion contains confirmation claim)
// AAD requires jwt-pop when confirmation claim exists, regardless of MtlsRequired flag
string assertionType = resp.TokenBindingCertificate != null
? OAuth2AssertionType.JwtPop
: OAuth2AssertionType.JwtBearer;

// If PoP was explicitly requested, we must have a certificate.
// (Preflight should enforce this too, but keep this defensive.)
if (p.IsMtlsPopRequested && !hasCert)
var tokenParameters = new Dictionary<string, string>
{
throw new MsalClientException(
MsalError.MtlsCertificateNotProvided,
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
}

// JWT-PoP if explicit PoP was requested OR delegate returned a cert (implicit bearer-over-mTLS)
bool useJwtPop = p.IsMtlsPopRequested || hasCert;

oAuth2Client.AddBodyParameter(
OAuth2Parameter.ClientAssertionType,
useJwtPop ? OAuth2AssertionType.JwtPop : OAuth2AssertionType.JwtBearer);

oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, resp.Assertion);
{ OAuth2Parameter.ClientAssertionType, assertionType },
{ OAuth2Parameter.ClientAssertion, resp.Assertion }
};

// Only return a cert if we actually have one.
return hasCert
? new ClientCredentialApplicationResult(useJwtPopClientAssertion: useJwtPop, mtlsCertificate: resp.TokenBindingCertificate)
: ClientCredentialApplicationResult.None;
return new CredentialMaterial(
tokenRequestParameters: tokenParameters,
mtlsCertificate: resp.TokenBindingCertificate,
metadata: new CredentialMaterialMetadata(
credentialType: CredentialType.ClientAssertion,
credentialSource: "callback",
mtlsCertificateIdHashPrefix: resp.TokenBindingCertificate != null
? CredentialMaterialHelper.GetCertificateIdHashPrefix(resp.TokenBindingCertificate)
: null,
mtlsCertificateRequested: requestContext.MtlsRequired,
resolutionTimeMs: 0));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;
Expand All @@ -27,24 +28,22 @@ internal ClientAssertionStringDelegateCredential(

public AssertionType AssertionType => AssertionType.ClientAssertion;

public async Task<ClientCredentialApplicationResult> AddConfidentialClientParametersAsync(
OAuth2Client oAuth2Client,
AuthenticationRequestParameters p,
ICryptographyManager _,
string tokenEndpoint,
CancellationToken ct)
public async Task<CredentialMaterial> GetCredentialMaterialAsync(
CredentialRequestContext requestContext,
CancellationToken cancellationToken)
{
var opts = new AssertionRequestOptions
{
CancellationToken = ct,
ClientID = p.AppConfig.ClientId,
TokenEndpoint = tokenEndpoint,
ClientCapabilities = p.RequestContext.ServiceBundle.Config.ClientCapabilities,
Claims = p.Claims,
ClientAssertionFmiPath = p.ClientAssertionFmiPath
CancellationToken = cancellationToken,
ClientID = requestContext.ClientId,
TokenEndpoint = requestContext.TokenEndpoint,
ClientCapabilities = requestContext.ClientCapabilities,
Claims = requestContext.Claims,
ClientAssertionFmiPath = requestContext.ClientAssertionFmiPath
};

string assertion = await _provider(opts, ct).ConfigureAwait(false);
string assertion = await _provider(opts, cancellationToken)
.ConfigureAwait(false);

if (string.IsNullOrWhiteSpace(assertion))
{
Expand All @@ -53,10 +52,20 @@ public async Task<ClientCredentialApplicationResult> AddConfidentialClientParame
MsalErrorMessage.InvalidClientAssertionEmpty);
}

oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer);
oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, assertion);
var tokenParameters = new Dictionary<string, string>
{
{ OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer },
{ OAuth2Parameter.ClientAssertion, assertion }
};

return ClientCredentialApplicationResult.None;
return new CredentialMaterial(
tokenRequestParameters: tokenParameters,
mtlsCertificate: null,
metadata: new CredentialMaterialMetadata(
credentialType: CredentialType.ClientAssertion,
credentialSource: "callback",
mtlsCertificateRequested: requestContext.MtlsRequired,
resolutionTimeMs: 0));
}
}
}
Loading