Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
initial
  • Loading branch information
GladwinJohnson committed Aug 12, 2025
commit 898aa839f59f305de2965a61f0b94c015eec90d4
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 @@ -15,6 +15,7 @@
using Microsoft.Identity.Client.Utils;
using Microsoft.Identity.Client.Extensibility;
using Microsoft.Identity.Client.OAuth2;
using System.Security.Cryptography.X509Certificates;

namespace Microsoft.Identity.Client
{
Expand Down Expand Up @@ -96,16 +97,33 @@ public AcquireTokenForClientParameterBuilder WithSendX5C(bool withSendX5C)
/// <returns>The current instance of <see cref="AcquireTokenForClientParameterBuilder"/> to enable method chaining.</returns>
public AcquireTokenForClientParameterBuilder WithMtlsProofOfPossession()
{
if (ServiceBundle.Config.ClientCredential is not CertificateClientCredential certificateCredential)
if (ServiceBundle.Config.ClientCredential is CertificateClientCredential certCred)
{
throw new MsalClientException(
MsalError.MtlsCertificateNotProvided,
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
CommonParameters.AuthenticationOperation =
new MtlsPopAuthenticationOperation(certCred.Certificate);
CommonParameters.MtlsCertificate = certCred.Certificate;
}
else if (ServiceBundle.Config.ClientCredential is ClientAssertionDelegateCredential assertCred)
{
X509Certificate2 cert = assertCred.PeekCertificate(ServiceBundle.Config.ClientId);

if (cert == null)
{
// Delegate did not supply a certificate ➜ cannot proceed with mTLS‑PoP
throw new MsalClientException(
MsalError.MtlsCertificateNotProvided,
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
}

CommonParameters.AuthenticationOperation =
new MtlsPopAuthenticationOperation(cert);
CommonParameters.MtlsCertificate = cert;
}
else
{
CommonParameters.AuthenticationOperation = new MtlsPopAuthenticationOperation(certificateCredential.Certificate);
CommonParameters.MtlsCertificate = certificateCredential.Certificate;
throw new MsalClientException(
MsalError.MtlsCertificateNotProvided,
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
}

return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Security.Cryptography.X509Certificates;

namespace Microsoft.Identity.Client
{
/// <summary>
/// Container returned from <c>WithClientAssertion</c>.
/// </summary>
public class AssertionResponse
{
/// <summary>Base-64 JWT that MSAL sends as <c>client_assertion</c>.</summary>
public string Assertion { get; init; }

/// <summary>
/// Certificate for mutual-TLS PoP.
/// Leave <c>null</c> for a bearer assertion.
/// </summary>
public X509Certificate2 TokenBindingCertificate { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,12 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(Func<string> cli
throw new ArgumentNullException(nameof(clientAssertionDelegate));
}

Func<CancellationToken, Task<string>> clientAssertionAsyncDelegate = (_) =>
{
return Task.FromResult(clientAssertionDelegate());
};

Config.ClientCredential = new SignedAssertionDelegateClientCredential(clientAssertionAsyncDelegate);
return this;
return WithClientAssertion(
(opts, ct) =>
Task.FromResult(new AssertionResponse
{
Assertion = clientAssertionDelegate() // bearer
}));
}

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

Config.ClientCredential = new SignedAssertionDelegateClientCredential(clientAssertionAsyncDelegate);
return this;
return WithClientAssertion(
async (opts, ct) =>
{
string jwt = await clientAssertionAsyncDelegate(ct).ConfigureAwait(false);
return new AssertionResponse { Assertion = jwt }; // bearer
});
}

/// <summary>
Expand All @@ -270,7 +273,35 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(Func<AssertionRe
throw new ArgumentNullException(nameof(clientAssertionAsyncDelegate));
}

Config.ClientCredential = new SignedAssertionDelegateClientCredential(clientAssertionAsyncDelegate);
return WithClientAssertion(
async (opts, _) =>
{
string jwt = await clientAssertionAsyncDelegate(opts).ConfigureAwait(false);
return new AssertionResponse { Assertion = jwt }; // bearer
});
}

/// <summary>
/// Configures the client application to use a client assertion for authentication.
/// </summary>
/// <remarks>This method allows the client application to authenticate using a custom client
/// assertion, which can be useful in scenarios where the assertion needs to be dynamically generated or
/// retrieved.</remarks>
/// <param name="boundAssertionAsync">A delegate that asynchronously provides an <see cref="AssertionResponse"/> based on the given <see
/// cref="AssertionRequestOptions"/> and <see cref="CancellationToken"/>. This delegate must not be <see
/// langword="null"/>.</param>
/// <returns>The <see cref="ConfidentialClientApplicationBuilder"/> instance configured with the specified client
/// assertion.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="boundAssertionAsync"/> is <see langword="null"/>.</exception>
public ConfidentialClientApplicationBuilder WithClientAssertion(Func<AssertionRequestOptions,
CancellationToken, Task<AssertionResponse>> boundAssertionAsync)
{
if (boundAssertionAsync == null)
{
throw new ArgumentNullException(nameof(boundAssertionAsync));
}

Config.ClientCredential = new ClientAssertionDelegateCredential(boundAssertionAsync);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
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
/// <see cref="AssertionResponse"/> (JWT + optional certificate).
/// </summary>
internal sealed class ClientAssertionDelegateCredential : IClientCredential
{
private readonly Func<AssertionRequestOptions, CancellationToken, Task<AssertionResponse>> _assertionDelegate;

public ClientAssertionDelegateCredential(
Func<AssertionRequestOptions, CancellationToken, Task<AssertionResponse>> assertionDelegate)
{
_assertionDelegate = assertionDelegate ?? throw new ArgumentNullException(nameof(assertionDelegate));
}

public AssertionType AssertionType => AssertionType.ClientAssertion;

private X509Certificate2 _cachedCertificate;

internal X509Certificate2 PeekCertificate(string clientId)
{
// Return the cached value if we already probed once
if (_cachedCertificate != null)
{
return _cachedCertificate;
}

try
{
var probeOpts = new AssertionRequestOptions
{
ClientID = clientId,
};

var resp = _assertionDelegate(probeOpts, CancellationToken.None)
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();

_cachedCertificate = resp?.TokenBindingCertificate;
}
catch
{
}

return _cachedCertificate;
}

public async Task AddConfidentialClientParametersAsync(
OAuth2Client oAuth2Client,
AuthenticationRequestParameters requestParameters,
ICryptographyManager cryptographyManager,
string tokenEndpoint,
CancellationToken cancellationToken)
{
// Build the same AssertionRequestOptions old code produced
var opts = new AssertionRequestOptions
{
CancellationToken = cancellationToken,
ClientID = requestParameters.AppConfig.ClientId,
TokenEndpoint = tokenEndpoint,
ClientCapabilities = requestParameters.RequestContext.ServiceBundle.Config.ClientCapabilities,
Claims = requestParameters.Claims,
ClientAssertionFmiPath = requestParameters.ClientAssertionFmiPath
};

// Execute delegate
AssertionResponse resp = await _assertionDelegate(opts, cancellationToken)
.ConfigureAwait(false);

// Empty JWT is not allowed
if (string.IsNullOrWhiteSpace(resp.Assertion))
{
throw new ArgumentException(
"The assertion delegate returned an empty JWT.",
nameof(_assertionDelegate));
}

// Set assertion type
if (resp.TokenBindingCertificate != null)
{
oAuth2Client.AddBodyParameter(
OAuth2Parameter.ClientAssertionType,
OAuth2AssertionType.JwtPop);
}
else
{
oAuth2Client.AddBodyParameter(
OAuth2Parameter.ClientAssertionType,
OAuth2AssertionType.JwtBearer);
}

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

This file was deleted.

11 changes: 11 additions & 0 deletions src/client/Microsoft.Identity.Client/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#if !NET8_0_OR_GREATER
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit
{
}
}
#endif
2 changes: 1 addition & 1 deletion src/client/Microsoft.Identity.Client/MsalErrorMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName)
public const string ClaimsChallenge = "The returned error contains a claims challenge. For additional info on how to handle claims related to multifactor authentication, Conditional Access, and incremental consent, see https://aka.ms/msal-conditional-access-claims. If you are using the On-Behalf-Of flow, see https://aka.ms/msal-conditional-access-claims-obo for details.";
public const string CryptographicError = "A cryptographic exception occurred. Possible cause: the certificate has been disposed. See inner exception for full details.";
public const string MtlsPopWithoutRegion = "mTLS Proof of Possession requires a region to be specified. Please set AzureRegion in the configuration at the application level.";
public const string MtlsCertificateNotProvidedMessage = "mTLS Proof of Possession requires a certificate to be configured. Please provide a certificate at the application level using the .WithCertificate() instead of passing an assertion. See https://aka.ms/msal-net-pop for details.";
public const string MtlsCertificateNotProvidedMessage = "mTLS Proof‑of‑Possession requires a certificate for this request. Either configure the application with .WithCertificate(...) or pass a certificate‑bound client‑assertion and chain .WithMtlsProofOfPossession() on the request builder. See https://aka.ms/msal-net-pop for details.";
public const string MtlsInvalidAuthorityTypeMessage = "mTLS PoP is only supported for AAD authority type. See https://aka.ms/msal-net-pop for details.";
public const string MtlsNonTenantedAuthorityNotAllowedMessage = "mTLS authentication requires a tenanted authority. Using 'common', 'organizations', or similar non-tenanted authorities is not allowed. Please provide an authority with a specific tenant ID (e.g., 'https://login.microsoftonline.com/{tenantId}'). See https://aka.ms/msal-net-pop for details.";
public const string RegionRequiredForMtlsPopMessage = "Regional auto-detect failed. mTLS Proof-of-Possession requires a region to be specified, as there is no global endpoint for mTLS. See https://aka.ms/msal-net-pop for details.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ internal static class OAuth2ResponseType
internal static class OAuth2AssertionType
{
public const string JwtBearer = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
public const string JwtPop = "urn:ietf:params:oauth:client-assertion-type:jwt-pop";
}

internal static class OAuth2RequestedTokenUse
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Microsoft.Identity.Client.AssertionResponse
Microsoft.Identity.Client.AssertionResponse.Assertion.get -> string
Microsoft.Identity.Client.AssertionResponse.Assertion.init -> void
Microsoft.Identity.Client.AssertionResponse.AssertionResponse() -> void
Microsoft.Identity.Client.AssertionResponse.TokenBindingCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2
Microsoft.Identity.Client.AssertionResponse.TokenBindingCertificate.init -> void
Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithClientAssertion(System.Func<Microsoft.Identity.Client.AssertionRequestOptions, System.Threading.CancellationToken, System.Threading.Tasks.Task<Microsoft.Identity.Client.AssertionResponse>> boundAssertionAsync) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Microsoft.Identity.Client.AssertionResponse
Microsoft.Identity.Client.AssertionResponse.Assertion.get -> string
Microsoft.Identity.Client.AssertionResponse.Assertion.init -> void
Microsoft.Identity.Client.AssertionResponse.AssertionResponse() -> void
Microsoft.Identity.Client.AssertionResponse.TokenBindingCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2
Microsoft.Identity.Client.AssertionResponse.TokenBindingCertificate.init -> void
Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithClientAssertion(System.Func<Microsoft.Identity.Client.AssertionRequestOptions, System.Threading.CancellationToken, System.Threading.Tasks.Task<Microsoft.Identity.Client.AssertionResponse>> boundAssertionAsync) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
Loading