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

using System;
Expand All @@ -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,116 +48,84 @@ 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] Mode={context.Mode}");

// 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 == CredentialTransportProtocol.Mtls)
{
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(
CollectionHelpers.GetEmptyDictionary<string, string>(),
certificate);
Comment thread
gladjohn marked this conversation as resolved.
}

// 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, certificate);
}

/// <summary>
/// Resolves the certificate to use for signing the client assertion.
/// Invokes the certificate provider delegate to get the certificate.
Comment thread
gladjohn marked this conversation as resolved.
/// </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>
/// <param name="context">Immutable context describing the current request.</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 a certificate without a private key.
/// </exception>
private async Task<X509Certificate2> ResolveCertificateAsync(
AuthenticationRequestParameters requestParameters,
string tokenEndpoint,
CredentialContext context,
CancellationToken cancellationToken)
{
requestParameters.RequestContext.Logger.Verbose(
Comment thread
gladjohn marked this conversation as resolved.
context.Logger.Verbose(
() => "[CertificateAndClaimsClientCredential] Resolving certificate from provider.");

// Create AssertionRequestOptions for the callback
var options = new AssertionRequestOptions(
requestParameters.AppConfig,
tokenEndpoint,
requestParameters.AuthorityManager.Authority.TenantId,
requestParameters.RequestContext.CorrelationId)
{
Claims = requestParameters.Claims,
ClientCapabilities = requestParameters.AppConfig.ClientCapabilities,
CancellationToken = cancellationToken
};
// Create AssertionRequestOptions from the credential context for the callback
var options = context.ToAssertionRequestOptions(cancellationToken);

// Invoke the provider to get the certificate
Comment thread
gladjohn marked this conversation as resolved.
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 @@ -168,8 +136,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 @@ -178,17 +145,16 @@ 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. " +
context.Logger.Verbose(
() => $"[CertificateAndClaimsClientCredential] Certificate resolved. " +
$"Thumbprint: {certificate.Thumbprint}");

return certificate;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// 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
Comment thread
gladjohn marked this conversation as resolved.
{
Expand All @@ -26,42 +25,23 @@ 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);
Comment thread
gladjohn marked this conversation as resolved.

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)
{
var opts = new AssertionRequestOptions
{
CancellationToken = ct,
ClientID = p.AppConfig.ClientId,
TokenEndpoint = tokenEndpoint,
ClientCapabilities = p.RequestContext.ServiceBundle.Config.ClientCapabilities,
Claims = p.Claims,
ClientAssertionFmiPath = p.ClientAssertionFmiPath,
CorrelationId = p.RequestContext.CorrelationId
};
context.Logger.Verbose(() => $"[ClientAssertionDelegateCredential] Mode={context.Mode}");

var opts = context.ToAssertionRequestOptions(cancellationToken);

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

if (string.IsNullOrWhiteSpace(resp?.Assertion))
{
Expand All @@ -72,28 +52,28 @@ 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)
if (context.Mode == CredentialTransportProtocol.Mtls && !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;

oAuth2Client.AddBodyParameter(
OAuth2Parameter.ClientAssertionType,
useJwtPop ? OAuth2AssertionType.JwtPop : OAuth2AssertionType.JwtBearer);
// Select the appropriate assertion type based on the presence of a certificate and the OAuth mode.
string assertionType =
(context.Mode == CredentialTransportProtocol.Mtls || hasCert)
? OAuth2AssertionType.JwtPop
: OAuth2AssertionType.JwtBearer;

oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, resp.Assertion);
var parameters = new Dictionary<string, string>
{
{ 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(
parameters,
hasCert ? resp.TokenBindingCertificate : null);
}
}
}
Loading
Loading