Skip to content
Closed
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
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

// ─────────────────────────────────────────────────────────────────────────────
// CREDENTIAL MATRIX – canonical mapping of (credential, mode) → output
//
// This file is purely documentary. It contains no executable code.
// It exists so that reviewers and future maintainers can see, in one place,
// every supported input/mode combination and what each produces.
//
// Legend
// ------
// Mode: Regular = standard JWT-bearer / client-secret flow
// MtlsMode = certificate-bound mTLS Proof-of-Possession flow
//
// Output columns
// ──────────────
// TokenRequestParameters – key/value pairs added to the token-request body
// ResolvedCertificate – X509Certificate2 stored on the request for
// transport binding / logging; null = not applicable
//
// Row │ Credential type │ Mode │ TokenRequestParameters │ ResolvedCertificate
// ────┼─────────────────────────┼───────────┼─────────────────────────────────────────────────┼─────────────────────
// 1 │ X509Certificate │ Regular │ client_assertion_type=jwt-bearer │ certificate
// │ │ │ client_assertion=<signed JWT> │
// ────┼─────────────────────────┼───────────┼─────────────────────────────────────────────────┼─────────────────────
// 2 │ X509Certificate │ MtlsMode │ (empty – TLS layer authenticates the client) │ certificate
// ────┼─────────────────────────┼───────────┼─────────────────────────────────────────────────┼─────────────────────
// 3 │ ClientSecret │ Regular │ client_secret=<secret> │ null
// ────┼─────────────────────────┼───────────┼─────────────────────────────────────────────────┼─────────────────────
// 4 │ ClientSecret │ MtlsMode │ ── UNSUPPORTED → MsalClientException ── │ n/a
// ────┼─────────────────────────┼───────────┼─────────────────────────────────────────────────┼─────────────────────
// 5 │ SignedAssertion (static) │ Regular │ client_assertion_type=jwt-bearer │ null
// │ │ │ client_assertion=<static JWT> │
// ────┼─────────────────────────┼───────────┼─────────────────────────────────────────────────┼─────────────────────
// 6 │ SignedAssertion (static) │ MtlsMode │ ── UNSUPPORTED → MsalClientException ── │ n/a
// ────┼─────────────────────────┼───────────┼─────────────────────────────────────────────────┼─────────────────────
// 7 │ JWT callback (string) │ Regular │ client_assertion_type=jwt-bearer │ null
// │ │ │ client_assertion=<callback JWT> │
// ────┼─────────────────────────┼───────────┼─────────────────────────────────────────────────┼─────────────────────
// 8 │ JWT callback (string) │ MtlsMode │ ── UNSUPPORTED → MsalClientException ── │ n/a
// ────┼─────────────────────────┼───────────┼─────────────────────────────────────────────────┼─────────────────────
// 9 │ JWT+cert callback │ Regular │ client_assertion_type=jwt-pop (cert bound) │ certificate
// │ │ │ client_assertion=<callback JWT> │
// ────┼─────────────────────────┼───────────┼─────────────────────────────────────────────────┼─────────────────────
// 10 │ JWT+cert callback │ MtlsMode │ client_assertion_type=urn:ietf:params: │ certificate
// │ │ │ oauth:client-assertion-type:jwt-pop │
// │ │ │ client_assertion=<callback JWT> │
// ────┴─────────────────────────┴───────────┴─────────────────────────────────────────────────┴─────────────────────
//
// Notes
// ─────
// • Rows 4, 6, 8 throw MsalClientException(MsalError.InvalidCredentialMaterial) because
// the credential type cannot supply a certificate for mTLS transport.
// • Row 9 (bearer-over-mTLS): when the callback returns a certificate in Regular mode, the
// credential uses JWT-PoP (not jwt-bearer) so the token is bound to the presented certificate.
// The TLS certificate has already been set on the OAuth2Client by MtlsPopParametersInitializer.
// • Row 10 (JWT-PoP): the assertion uses the JWT-PoP client_assertion_type so the
// authorization server can verify the token is bound to the presented certificate.
// • "Static" sources (rows 1–3, 5) set CredentialSource.Static.
// Callback sources (rows 7–10) set CredentialSource.Callback.
// ─────────────────────────────────────────────────────────────────────────────
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Internal.Requests;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.OAuth2;
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
using Microsoft.Identity.Client.TelemetryCore;

namespace Microsoft.Identity.Client.Internal.ClientCredential
Expand Down Expand Up @@ -48,116 +46,77 @@ public CertificateAndClaimsClientCredential(
Certificate = certificate;
}

public async Task<ClientCredentialApplicationResult> AddConfidentialClientParametersAsync(
OAuth2Client oAuth2Client,
AuthenticationRequestParameters requestParameters,
ICryptographyManager cryptographyManager,
string tokenEndpoint,
public async Task<CredentialMaterial> GetCredentialMaterialAsync(
CredentialContext context,
CancellationToken cancellationToken)
{
string clientId = requestParameters.AppConfig.ClientId;
// Resolve the certificate via the provider (used both for Regular and MtlsMode paths).
X509Certificate2 certificate = await ResolveCertificateAsync(context, cancellationToken)
.ConfigureAwait(false);

// 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)
if (context.Mode == ClientAuthMode.MtlsMode)
{
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: the certificate authenticates the client at the TLS layer.
// No client_assertion is needed; return an empty parameter set.
return new CredentialMaterial(
new Dictionary<string, string>(),
CredentialSource.Static,
certificate);
}

// 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.");
// Regular path: build a JWT-bearer client assertion.
JsonWebToken jwtToken;
if (string.IsNullOrEmpty(context.ExtraClientAssertionClaims))
{
jwtToken = new JsonWebToken(
context.CryptographyManager,
context.ClientId,
context.TokenEndpoint,
_claimsToSign,
_appendDefaultClaims);
}
else
{
jwtToken = new JsonWebToken(
context.CryptographyManager,
context.ClientId,
context.TokenEndpoint,
context.ExtraClientAssertionClaims,
_appendDefaultClaims);
}

requestParameters.ResolvedCertificate = requestParameters.MtlsCertificate;
string assertion = jwtToken.Sign(certificate, context.SendX5C, context.UseSha2);

// Return the mTLS certificate via the result object so the pipeline can use it
// (HTTP handler + policy/region checks).
return new ClientCredentialApplicationResult
var parameters = new Dictionary<string, string>
{
MtlsCertificate = requestParameters.MtlsCertificate,
UseJwtPopClientAssertion = false // no client assertion set here
{ OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer },
{ OAuth2Parameter.ClientAssertion, assertion }
};

return new CredentialMaterial(parameters, CredentialSource.Static, certificate);
}

/// <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,
CredentialContext context,
CancellationToken cancellationToken)
{
requestParameters.RequestContext.Logger.Verbose(
() => "[CertificateAndClaimsClientCredential] Resolving certificate from provider.");

// Create AssertionRequestOptions for the callback
var options = new AssertionRequestOptions(
requestParameters.AppConfig,
tokenEndpoint,
requestParameters.AuthorityManager.Authority.TenantId)
var options = new AssertionRequestOptions
{
Claims = requestParameters.Claims,
ClientCapabilities = requestParameters.AppConfig.ClientCapabilities,
ClientID = context.ClientId,
TokenEndpoint = context.TokenEndpoint,
Claims = context.Claims,
ClientCapabilities = context.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.");
Expand All @@ -167,30 +126,21 @@ private async Task<X509Certificate2> ResolveCertificateAsync(
{
if (!certificate.HasPrivateKey)
{
requestParameters.RequestContext.Logger.Error(
"[CertificateAndClaimsClientCredential] Certificate from provider does not have a private key.");

throw new MsalClientException(
MsalError.CertWithoutPrivateKey,
MsalErrorMessage.CertMustHavePrivateKey(certificate.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);
}

requestParameters.RequestContext.Logger.Info(
() => $"[CertificateAndClaimsClientCredential] Successfully resolved certificate from provider. " +
$"Thumbprint: {certificate.Thumbprint}");

return certificate;
}
}
}

Loading