Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,4 @@ package-lock.json

# ignore JetBrains Rider files
.idea/
/git
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public string ClientSecret
{
get
{
if (ClientCredential is SecretStringClientCredential secretCred)
if (ClientCredential is ClientSecretCredential secretCred)
{
return secretCred.Secret;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ public ConfidentialClientApplicationBuilder WithClientSecret(string clientSecret
throw new ArgumentNullException(nameof(clientSecret));
}

Config.ClientCredential = new SecretStringClientCredential(clientSecret);
Config.ClientCredential = new ClientSecretCredential(clientSecret);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
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;
using Microsoft.Identity.Client.Utils;

namespace Microsoft.Identity.Client.Internal.ClientCredential
{
Expand Down Expand Up @@ -48,115 +48,91 @@ 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;
context.Logger.Verbose(() => $"[CertificateAndClaimsClientCredential] Resolving credential material. " +
$"Mode={context.Mode}, " + $"TokenEndpoint={context.TokenEndpoint}");

// Log the incoming request parameters for diagnostic purposes
requestParameters.RequestContext.Logger.Verbose(
() => $"Building assertion from certificate with clientId: {clientId} at endpoint: {tokenEndpoint}");
// Resolve the certificate via the provider (used both for Regular and MtlsMode paths).
X509Certificate2 certificate = await ResolveCertificateAsync(context, cancellationToken)
.ConfigureAwait(false);

// If mTLS cert is not already set for the request, proceed with JWT bearer client assertion.
if (requestParameters.MtlsCertificate == null)
if (context.Mode == OAuthMode.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);
context.Logger.Verbose(() => "[CertificateAndClaimsClientCredential] mTLS mode detected. " +
"Using certificate for TLS client authentication; no client_assertion will be added.");

// mTLS path: the certificate authenticates the client at the TLS layer.
// No client_assertion is needed; return an empty parameter set.
return new CredentialMaterial(
CollectionHelpers.GetEmptyDictionary<string, string>(),
certificate);
}

oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer);
oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, assertion);
context.Logger.Verbose(() => "[CertificateAndClaimsClientCredential] Regular mode detected. " +
"Building certificate-based client assertion.");

// No extra outputs for the common case.
return ClientCredentialApplicationResult.None;
// 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);
}

// 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;
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 }
};

context.Logger.Verbose(() => "[CertificateAndClaimsClientCredential] Certificate-based client " +
"assertion created successfully.");

return new CredentialMaterial(parameters, 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.");
context.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.");
context.Logger.Error("[CertificateAndClaimsClientCredential] Certificate provider returned null.");

throw new MsalClientException(
MsalError.InvalidClientAssertion,
Expand All @@ -167,8 +143,7 @@ private async Task<X509Certificate2> ResolveCertificateAsync(
{
if (!certificate.HasPrivateKey)
{
requestParameters.RequestContext.Logger.Error(
"[CertificateAndClaimsClientCredential] Certificate from provider does not have a private key.");
context.Logger.Error("[CertificateAndClaimsClientCredential] The certificate does not have a private key.");

throw new MsalClientException(
MsalError.CertWithoutPrivateKey,
Expand All @@ -177,18 +152,15 @@ private async Task<X509Certificate2> ResolveCertificateAsync(
}
catch (System.Security.Cryptography.CryptographicException ex)
{
requestParameters.RequestContext.Logger.Error(
"[CertificateAndClaimsClientCredential] A cryptographic error occurred while accessing the certificate.");
context.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}");
context.Logger.Verbose(() => "[CertificateAndClaimsClientCredential] Successfully resolved certificate from provider.");

return certificate;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
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;

namespace Microsoft.Identity.Client.Internal.ClientCredential
{
/// <summary>
/// Handles client assertions supplied via a delegate that returns an
/// <see cref="ClientSignedAssertion"/> (JWT + optional certificate bound for mTLSPoP).
/// Handles client assertions supplied via a delegate that returns a
/// <see cref="ClientSignedAssertion"/> (JWT + optional certificate bound for mTLS-PoP).
/// </summary>
internal sealed class ClientAssertionDelegateCredential : IClientCredential, IClientSignedAssertionProvider
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.

Can you add tests or a debug assertion to ensure that each credential, and especially this one, is called once and only once per token request?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added test coverage to verify the callback-backed credential path is invoked exactly once per token request.

{
Expand All @@ -26,41 +25,34 @@ internal ClientAssertionDelegateCredential(
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}

// Private helper for internal readability
private Task<ClientSignedAssertion> GetAssertionAsync(
AssertionRequestOptions options,
CancellationToken cancellationToken) =>
_provider(options, cancellationToken);

// Capability interface (only used where we intentionally cast to check the capability)
Task<ClientSignedAssertion> IClientSignedAssertionProvider.GetAssertionAsync(
AssertionRequestOptions options,
CancellationToken cancellationToken) =>
GetAssertionAsync(options, cancellationToken);
_provider(options, cancellationToken);

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(
CredentialContext context,
CancellationToken cancellationToken)
{
context.Logger.Verbose(() => $"[ClientAssertionDelegateCredential] Resolving client assertion material. " +
$"Mode={context.Mode}, TokenEndpoint={context.TokenEndpoint}");

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 = context.ClientId,
TokenEndpoint = context.TokenEndpoint,
ClientCapabilities = context.ClientCapabilities,
Claims = context.Claims,
ClientAssertionFmiPath = context.ClientAssertionFmiPath
};

ClientSignedAssertion resp = await GetAssertionAsync(opts, ct).ConfigureAwait(false);
context.Logger.Verbose(() => "[ClientAssertionDelegateCredential] Invoking client assertion provider delegate.");

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

if (string.IsNullOrWhiteSpace(resp?.Assertion))
{
Expand All @@ -71,28 +63,35 @@ public async Task<ClientCredentialApplicationResult> AddConfidentialClientParame

bool hasCert = resp.TokenBindingCertificate != null;

// If PoP was explicitly requested, we must have a certificate.
// (Preflight should enforce this too, but keep this defensive.)
if (p.IsMtlsPopRequested && !hasCert)
context.Logger.Verbose(() => $"[ClientAssertionDelegateCredential] Provider returned assertion. " +
$"TokenBindingCertificatePresent={hasCert}");

if (context.Mode == OAuthMode.MtlsMode && !hasCert)
{
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;
// Select the appropriate assertion type based on the presence of a certificate and the OAuth mode.
string assertionType =
(context.Mode == OAuthMode.MtlsMode || hasCert)
? OAuth2AssertionType.JwtPop
: OAuth2AssertionType.JwtBearer;

context.Logger.Verbose(() => $"[ClientAssertionDelegateCredential] Selected client assertion type: {assertionType}");

oAuth2Client.AddBodyParameter(
OAuth2Parameter.ClientAssertionType,
useJwtPop ? OAuth2AssertionType.JwtPop : OAuth2AssertionType.JwtBearer);
var parameters = new Dictionary<string, string>
{
{ OAuth2Parameter.ClientAssertionType, assertionType },
{ OAuth2Parameter.ClientAssertion, resp.Assertion }
};

oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, resp.Assertion);
context.Logger.Verbose(() => "[ClientAssertionDelegateCredential] Client assertion material created successfully.");

// Only return a cert if we actually have one.
return hasCert
? new ClientCredentialApplicationResult(useJwtPopClientAssertion: useJwtPop, mtlsCertificate: resp.TokenBindingCertificate)
: ClientCredentialApplicationResult.None;
return new CredentialMaterial(
parameters,
hasCert ? resp.TokenBindingCertificate : null);
}
}
}
Loading
Loading