Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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,7 +48,8 @@ protected override void Validate()
// Confidential client must have a credential
if (ServiceBundle?.Config.ClientCredential == null &&
CommonParameters.OnBeforeTokenRequestHandler == null &&
ServiceBundle?.Config.AppTokenProvider == null
ServiceBundle?.Config.AppTokenProvider == null &&
ServiceBundle?.Config.ClientCredentialCertificateProvider == null
)
{
throw new MsalClientException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,26 @@ public string ClientVersion
internal IRetryPolicyFactory RetryPolicyFactory { get; set; }
internal ICsrFactory CsrFactory { get; set; }

#region Extensibility Callbacks

/// <summary>
/// Dynamic certificate provider callback for client credential flows.
/// </summary>
public Func<ClientCredentialExtensionParameters, Task<X509Certificate2>> ClientCredentialCertificateProvider { get; set; }

/// <summary>
/// MSAL service failure callback that determines whether to retry after a token acquisition failure from the identity provider.
/// Only invoked for MsalServiceException (errors from the Security Token Service).
/// </summary>
public Func<ClientCredentialExtensionParameters, MsalException, Task<bool>> OnMsalServiceFailureCallback { get; set; }

/// <summary>
/// Success callback that receives the result of token acquisition attempts (typically successful, but can include failures after retries are exhausted).
/// </summary>
public Func<ClientCredentialExtensionParameters, ExecutionResult, Task> OnSuccessCallback { get; set; }

#endregion

#region ClientCredentials

// Indicates if claims or assertions are used within the configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Identity.Client.Extensibility
{
/// <summary>
/// Provides application configuration context to client credential extensibility callbacks.
/// Contains read-only information about the confidential client application.
/// </summary>
#if !SUPPORTS_CONFIDENTIAL_CLIENT
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile
#endif
public class ClientCredentialExtensionParameters
{
/// <summary>
/// Internal constructor - only MSAL can create instances of this class.
/// </summary>
/// <param name="config">The application configuration.</param>
internal ClientCredentialExtensionParameters(ApplicationConfiguration config)
{
ClientId = config.ClientId;
TenantId = config.TenantId;
Authority = config.Authority?.AuthorityInfo?.CanonicalAuthority?.ToString();
}

/// <summary>
/// The application (client) ID as registered in the Azure portal or application registration portal.
/// </summary>
public string ClientId { get; }

/// <summary>
/// The tenant ID if the application is configured for a specific tenant.
/// Will be null for multi-tenant applications.
/// </summary>
public string TenantId { get; }

/// <summary>
/// The authority URL used for authentication (e.g., https://login.microsoftonline.com/common).
/// </summary>
public string Authority { get; }
}
}
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.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace Microsoft.Identity.Client.Extensibility
Expand Down Expand Up @@ -29,5 +30,153 @@ public static ConfidentialClientApplicationBuilder WithAppTokenProvider(
builder.Config.AppTokenProvider = appTokenProvider ?? throw new ArgumentNullException(nameof(appTokenProvider));
return builder;
}

/// <summary>
/// Configures an async callback to provide the client credential certificate dynamically.
/// The callback is invoked before each token acquisition request to the identity provider (including retries).
/// This enables scenarios such as certificate rotation and dynamic certificate selection based on application context.
/// </summary>
/// <param name="builder">The confidential client application builder.</param>
/// <param name="certificateProvider">
/// An async callback that provides the certificate based on the application configuration.
/// Called before each network request to acquire a token.
/// Must return a valid <see cref="X509Certificate2"/> with a private key.
/// </param>
/// <returns>The builder to chain additional configuration calls.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="certificateProvider"/> is null.</exception>
/// <exception cref="MsalClientException">
/// Thrown if a static certificate is already configured via <see cref="ConfidentialClientApplicationBuilder.WithCertificate(X509Certificate2)"/>.
/// </exception>
/// <remarks>
/// <para>This method cannot be used together with <see cref="ConfidentialClientApplicationBuilder.WithCertificate(X509Certificate2)"/>.</para>
/// <para>The callback is not invoked when tokens are retrieved from cache, only for network calls.</para>
/// <para>The certificate returned by the callback will be used to sign the client assertion (JWT) for that token request.</para>
/// <para>The callback can perform async operations such as fetching certificates from Azure Key Vault or other secret management systems.</para>
/// <para>See https://aka.ms/msal-net-client-credentials for more details on client credentials.</para>
/// </remarks>
public static ConfidentialClientApplicationBuilder WithCertificate(
this ConfidentialClientApplicationBuilder builder,
Func<ClientCredentialExtensionParameters, Task<X509Certificate2>> certificateProvider)
{
if (certificateProvider == null)
{
throw new ArgumentNullException(nameof(certificateProvider));
}

builder.Config.ClientCredentialCertificateProvider = certificateProvider;

// Create a CertificateAndClaimsClientCredential with null certificate
// The certificate will be resolved dynamically via the provider in ResolveCertificateAsync
builder.Config.ClientCredential = new Microsoft.Identity.Client.Internal.ClientCredential.CertificateAndClaimsClientCredential(
certificate: null,
claimsToSign: null,
appendDefaultClaims: true);

return builder;
}

/// <summary>
/// Configures an async callback that is invoked when MSAL receives an error response from the identity provider (Security Token Service).
/// The callback determines whether MSAL should retry the token request or propagate the exception.
/// This callback is invoked after each service failure and can be called multiple times until it returns <c>false</c> or the request succeeds.
/// </summary>
/// <param name="builder">The confidential client application builder.</param>
/// <param name="onMsalServiceFailureCallback">
/// An async callback that determines whether to retry after a service failure.
/// Receives the application configuration parameters and the <see cref="MsalServiceException"/> that occurred.
/// Returns <c>true</c> to retry the request, or <c>false</c> to stop retrying and propagate the exception.
/// The callback will be invoked repeatedly after each service failure until it returns <c>false</c> or the request succeeds.
/// </param>
/// <returns>The builder to chain additional configuration calls.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="onMsalServiceFailureCallback"/> is null.</exception>
/// <remarks>
/// <para>This callback is ONLY triggered for <see cref="MsalServiceException"/> - errors returned by the identity provider (e.g., HTTP 500, 503, throttling).</para>
/// <para>This callback is NOT triggered for client-side errors (<see cref="MsalClientException"/>) or network failures handled internally by MSAL.</para>
/// <para>This callback is only invoked for network token acquisition attempts, not when tokens are retrieved from cache.</para>
/// <para>When the callback returns <c>true</c>, MSAL will invoke the certificate provider (if configured via <see cref="WithCertificate"/>)
/// before making another token request, enabling certificate rotation scenarios.</para>
/// <para>MSAL's internal throttling and retry mechanisms will still apply, including respecting Retry-After headers from the identity provider.</para>
/// <para>To prevent infinite loops, ensure your callback has appropriate termination conditions (e.g., max retry count, timeout).</para>
/// <para>The callback can perform async operations such as logging to remote services, checking external health endpoints, or querying configuration stores.</para>
/// </remarks>
/// <example>
/// <code>
/// int retryCount = 0;
/// var app = ConfidentialClientApplicationBuilder
/// .Create(clientId)
/// .WithCertificate(async parameters => await GetCertificateFromKeyVaultAsync(parameters.TenantId))
/// .OnMsalServiceFailure(async (parameters, serviceException) =>
/// {
/// retryCount++;
/// await LogExceptionAsync(serviceException);
///
/// // Retry up to 3 times for transient service errors (5xx)
/// return serviceException.StatusCode >= 500 &amp;&amp; retryCount &lt; 3;
/// })
/// .Build();
/// </code>
/// </example>
public static ConfidentialClientApplicationBuilder OnMsalServiceFailure(
this ConfidentialClientApplicationBuilder builder,
Func<ClientCredentialExtensionParameters, MsalException, Task<bool>> onMsalServiceFailureCallback)
{
if (onMsalServiceFailureCallback == null)
throw new ArgumentNullException(nameof(onMsalServiceFailureCallback));

builder.Config.OnMsalServiceFailureCallback = onMsalServiceFailureCallback;
return builder;
}

/// <summary>
/// Configures an async callback that is invoked when a token acquisition request completes.
/// This callback is invoked once per <c>AcquireTokenForClient</c> call, after all retry attempts have been exhausted.
/// While named <c>OnSuccess</c> for the common case, this callback fires for both successful and failed acquisitions.
/// This enables scenarios such as telemetry, logging, and custom result handling.
/// </summary>
/// <param name="builder">The confidential client application builder.</param>
/// <param name="onSuccessCallback">
/// An async callback that receives the application configuration parameters and the execution result.
/// The result contains either the successful <see cref="AuthenticationResult"/> or the <see cref="MsalException"/> that occurred.
/// This callback is invoked after all retries have been exhausted (if an <see cref="OnMsalServiceFailure"/> handler is configured).
/// </param>
/// <returns>The builder to chain additional configuration calls.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="onSuccessCallback"/> is null.</exception>
/// <remarks>
/// <para>This callback is invoked for both successful and failed token acquisitions. Check <see cref="ExecutionResult.Successful"/> to determine the outcome.</para>
/// <para>This callback is only invoked for network token acquisition attempts, not when tokens are retrieved from cache.</para>
/// <para>If multiple calls to <c>OnSuccess</c> are made, only the last configured callback will be used.</para>
/// <para>Exceptions thrown by this callback will be caught and logged internally to prevent disruption of the authentication flow.</para>
/// <para>The callback is invoked on the same thread/context as the token acquisition request.</para>
/// <para>The callback can perform async operations such as sending telemetry to Application Insights, persisting logs to databases, or triggering webhooks.</para>
/// </remarks>
/// <example>
/// <code>
/// var app = ConfidentialClientApplicationBuilder
/// .Create(clientId)
/// .WithCertificate(certificate)
/// .OnSuccess(async (parameters, result) =>
/// {
/// if (result.Successful)
/// {
/// await telemetry.TrackEventAsync("TokenAcquired", new { ClientId = parameters.ClientId });
/// }
/// else
/// {
/// await telemetry.TrackExceptionAsync(result.Exception);
/// }
/// })
/// .Build();
/// </code>
/// </example>
public static ConfidentialClientApplicationBuilder OnSuccess(
this ConfidentialClientApplicationBuilder builder,
Func<ClientCredentialExtensionParameters, ExecutionResult, Task> onSuccessCallback)
{
if (onSuccessCallback == null)
throw new ArgumentNullException(nameof(onSuccessCallback));

builder.Config.OnSuccessCallback = onSuccessCallback;
return builder;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Identity.Client.Extensibility
{
/// <summary>
/// Represents the result of a token acquisition attempt.
/// Used by the execution observer configured via <see cref="ConfidentialClientApplicationBuilderExtensions.OnSuccess"/>.
/// </summary>
public class ExecutionResult
Copy link

Choose a reason for hiding this comment

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

Can this include the credential used (primarily care about the exact certificate used). If that information is in AuthenticationResult then that is fine.

{
/// <summary>
/// Internal constructor for ExecutionResult.
/// </summary>
internal ExecutionResult() { }

/// <summary>
/// Indicates whether the token acquisition was successful.
/// </summary>
/// <value>
/// <c>true</c> if the token was successfully acquired; otherwise, <c>false</c>.
/// </value>
public bool Successful { get; internal set; }

/// <summary>
/// The authentication result if the token acquisition was successful.
/// </summary>
/// <value>
/// An <see cref="AuthenticationResult"/> containing the access token and related metadata if <see cref="Successful"/> is <c>true</c>;
/// otherwise, <c>null</c>.
/// </value>
public AuthenticationResult Result { get; internal set; }

/// <summary>
/// The exception that occurred if the token acquisition failed.
/// </summary>
/// <value>
/// An <see cref="MsalException"/> describing the failure if <see cref="Successful"/> is <c>false</c>;
/// otherwise, <c>null</c>.
/// </value>
public MsalException Exception { get; internal set; }
}
}
Loading
Loading