Skip to content
Merged
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
Expand Up @@ -145,6 +145,16 @@ public string ClientVersion
/// </summary>
public Func<AssertionRequestOptions, ExecutionResult, Task> OnCompletion { get; set; }

/// <summary>
/// Callback invoked when a fire-and-forget background (proactive) token refresh completes, giving
/// callers visibility into an otherwise-unobservable path. Receives an <see cref="ExecutionResult"/>
/// describing the outcome: on success <see cref="ExecutionResult.Result"/> is the refreshed token; on
/// failure <see cref="ExecutionResult.Exception"/> is the thrown exception (whose
/// <see cref="MsalException.AuthenticationResultMetadata"/> carries the failed attempt's HTTP duration).
/// Only set for confidential-client and managed-identity applications.
/// </summary>
public Func<ExecutionResult, Task> OnBackgroundTokenRefreshCompleted { get; set; }

#endregion

#region ClientCredentials
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,38 @@ public static ConfidentialClientApplicationBuilder OnCompletion(
builder.Config.OnCompletion = onCompletion;
return builder;
}

/// <summary>
/// Configures an async callback invoked when a fire-and-forget background (proactive) token refresh completes.
/// Proactive refresh runs on a background thread after the caller has already received a valid cached token,
/// so its outcome (latency, failures) is otherwise unobservable. This callback surfaces it, e.g. for telemetry.
/// </summary>
/// <param name="builder">The confidential client application builder.</param>
/// <param name="onBackgroundTokenRefreshCompleted">
/// An async callback that receives the <see cref="ExecutionResult"/> of the background refresh. On success,
/// <see cref="ExecutionResult.Result"/> holds the refreshed token; on failure, <see cref="ExecutionResult.Exception"/>
/// holds the exception, whose <see cref="MsalException.AuthenticationResultMetadata"/> carries the failed
/// attempt's HTTP duration.
/// </param>
/// <returns>The builder to chain additional configuration calls.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="onBackgroundTokenRefreshCompleted"/> is null.</exception>
/// <remarks>
/// <para>Invoked on a background thread. Check <see cref="ExecutionResult.Successful"/> to determine the outcome.</para>
/// <para>Exceptions thrown by this callback are caught and logged internally so they cannot disrupt the refresh.</para>
/// </remarks>
public static ConfidentialClientApplicationBuilder OnBackgroundTokenRefreshCompleted(
this ConfidentialClientApplicationBuilder builder,
Func<ExecutionResult, Task> onBackgroundTokenRefreshCompleted)
{
builder.ValidateUseOfExperimentalFeature();

if (onBackgroundTokenRefreshCompleted == null)
Comment thread
neha-bhargava marked this conversation as resolved.
{
throw new ArgumentNullException(nameof(onBackgroundTokenRefreshCompleted));
}

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

using System;
using System.Threading.Tasks;

namespace Microsoft.Identity.Client.Extensibility
{
/// <summary>
/// Extensibility methods for <see cref="ManagedIdentityApplicationBuilder"/>.
/// </summary>
#if !SUPPORTS_CONFIDENTIAL_CLIENT
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide managed identity flow on mobile
#endif
public static class ManagedIdentityApplicationBuilderExtensions
{
/// <summary>
/// Configures an async callback invoked when a fire-and-forget background (proactive) token refresh completes.
/// Proactive refresh runs on a background thread after the caller has already received a valid cached token,
/// so its outcome (latency, failures) is otherwise unobservable. This callback surfaces it, e.g. for telemetry.
/// </summary>
/// <param name="builder">The managed identity application builder.</param>
/// <param name="onBackgroundTokenRefreshCompleted">
/// An async callback that receives the <see cref="ExecutionResult"/> of the background refresh. On success,
/// <see cref="ExecutionResult.Result"/> holds the refreshed token; on failure, <see cref="ExecutionResult.Exception"/>
/// holds the exception, whose <see cref="MsalException.AuthenticationResultMetadata"/> carries the failed
/// attempt's HTTP duration.
/// </param>
/// <returns>The builder to chain additional configuration calls.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="onBackgroundTokenRefreshCompleted"/> is null.</exception>
/// <remarks>
/// <para>Invoked on a background thread. Check <see cref="ExecutionResult.Successful"/> to determine the outcome.</para>
/// <para>Exceptions thrown by this callback are caught and logged internally so they cannot disrupt the refresh.</para>
/// </remarks>
public static ManagedIdentityApplicationBuilder OnBackgroundTokenRefreshCompleted(
this ManagedIdentityApplicationBuilder builder,
Func<ExecutionResult, Task> onBackgroundTokenRefreshCompleted)
{
builder.ValidateUseOfExperimentalFeature();

if (onBackgroundTokenRefreshCompleted == null)
{
throw new ArgumentNullException(nameof(onBackgroundTokenRefreshCompleted));
}

builder.Config.OnBackgroundTokenRefreshCompleted = onBackgroundTokenRefreshCompleted;
return builder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ private void UpdateTelemetry(long elapsedMilliseconds, ApiEvent apiEvent, Authen
/// is left at its default (0 / null). <see cref="AuthenticationResultMetadata.TokenSource"/> has
/// no meaningful value on failure, so it stays at the constructor default and should not be relied on.
/// </summary>
private static AuthenticationResultMetadata CreateFailureMetadata(ApiEvent apiEvent, long totalDurationInMs)
internal static AuthenticationResultMetadata CreateFailureMetadata(ApiEvent apiEvent, long totalDurationInMs)
{
return new AuthenticationResultMetadata(TokenSource.IdentityProvider)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,13 @@ internal static void ProcessFetchInBackground(
try
{
var authResult = await fetchAction().ConfigureAwait(false);
var executionResult = new ExecutionResult { Successful = true, Result = authResult };

// Invoke the enricher once for this background refresh and reuse the materialized
// tags across every instrument below.
IReadOnlyList<KeyValuePair<string, object>> extraTags = OtelEnrichmentHelper.MaterializeExtraTags(
tagsEnricher,
() => new ExecutionResult { Successful = true, Result = authResult },
() => executionResult,
logger);

serviceBundle.PlatformProxy.OtelInstrumentation.IncrementSuccessCounter(
Expand Down Expand Up @@ -135,6 +136,8 @@ internal static void ProcessFetchInBackground(
authResult.AuthenticationResultMetadata,
logger,
extraTags);

await InvokeBackgroundRefreshCallbackAsync(serviceBundle, executionResult, logger).ConfigureAwait(false);
}
catch (MsalServiceException ex)
{
Expand All @@ -150,22 +153,66 @@ internal static void ProcessFetchInBackground(

LogBackgroundFailureTelemetry(serviceBundle, apiEvent, callerSdkId, callerSdkVersion,
ex.ErrorCode, ex.StatusCode, ex.ErrorCodes?.FirstOrDefault(), ex, tagsEnricher, logger);

// Background refresh doesn't go through RunAsync, so the exception isn't carrying metadata yet.
// Fill it in from apiEvent so the callback can see the HTTP duration and cache-refresh reason.
// Total duration stays 0 on purpose - the caller already got a token from the cache.
if (ex.AuthenticationResultMetadata == null)
{
ex.AuthenticationResultMetadata = RequestBase.CreateFailureMetadata(apiEvent, totalDurationInMs: 0);
}

await InvokeBackgroundRefreshCallbackAsync(serviceBundle, new ExecutionResult { Successful = false, Exception = ex }, logger).ConfigureAwait(false);
}
catch (OperationCanceledException ex)
{
logger.WarningPiiWithPrefix(ex, ProactiveRefreshCancellationError);
LogBackgroundFailureTelemetry(serviceBundle, apiEvent, callerSdkId, callerSdkVersion,
ex.GetType().Name, httpStatusCode: 0, tagsEnricher: tagsEnricher, logger: logger);

await InvokeBackgroundRefreshCallbackAsync(serviceBundle, new ExecutionResult { Successful = false, Exception = null }, logger).ConfigureAwait(false);
}
catch (Exception ex)
{
logger.ErrorPiiWithPrefix(ex, ProactiveRefreshGeneralError);
LogBackgroundFailureTelemetry(serviceBundle, apiEvent, callerSdkId, callerSdkVersion,
ex.GetType().Name, httpStatusCode: 0, tagsEnricher: tagsEnricher, logger: logger);

MsalException msalException = ex as MsalException;
if (msalException != null && msalException.AuthenticationResultMetadata == null)
{
msalException.AuthenticationResultMetadata = RequestBase.CreateFailureMetadata(apiEvent, totalDurationInMs: 0);
}

await InvokeBackgroundRefreshCallbackAsync(serviceBundle, new ExecutionResult { Successful = false, Exception = msalException }, logger).ConfigureAwait(false);
}
});
}

// Invokes the app-configured background-refresh completion callback (confidential client and managed
// identity only). Runs on the fire-and-forget background thread; a throwing callback is caught and
// logged so it cannot disrupt the refresh.
private static async Task InvokeBackgroundRefreshCallbackAsync(
IServiceBundle serviceBundle,
ExecutionResult executionResult,
ILoggerAdapter logger)
{
Func<ExecutionResult, Task> callback = serviceBundle.Config.OnBackgroundTokenRefreshCompleted;
if (callback == null)
{
return;
}

try
{
await callback(executionResult).ConfigureAwait(false);
}
catch (Exception ex)
{
logger.WarningPiiWithPrefix(ex, "OnBackgroundTokenRefreshCompleted callback threw an exception; it was suppressed.");
}
}

// Records telemetry for a fire-and-forget background refresh failure: increments the
// failure counter and records V2 HTTP duration when an HTTP exchange happened.
// Total duration is deliberately not recorded — the foreground user already received
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ Microsoft.Identity.Client.AppConfig.PoPOptions.PoPOptions() -> void
static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey<T>(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder<T> builder, string key, string value, bool partitionRefreshToken) -> T
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder, Microsoft.Identity.Client.AppConfig.PoPOptions options) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Microsoft.Identity.Client.MsalException.AuthenticationResultMetadata.get -> Microsoft.Identity.Client.AuthenticationResultMetadata
Microsoft.Identity.Client.Extensibility.ManagedIdentityApplicationBuilderExtensions
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnBackgroundTokenRefreshCompleted(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func<Microsoft.Identity.Client.Extensibility.ExecutionResult, System.Threading.Tasks.Task> onBackgroundTokenRefreshCompleted) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ManagedIdentityApplicationBuilderExtensions.OnBackgroundTokenRefreshCompleted(this Microsoft.Identity.Client.ManagedIdentityApplicationBuilder builder, System.Func<Microsoft.Identity.Client.Extensibility.ExecutionResult, System.Threading.Tasks.Task> onBackgroundTokenRefreshCompleted) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ Microsoft.Identity.Client.AppConfig.PoPOptions.PoPOptions() -> void
static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey<T>(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder<T> builder, string key, string value, bool partitionRefreshToken) -> T
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder, Microsoft.Identity.Client.AppConfig.PoPOptions options) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Microsoft.Identity.Client.MsalException.AuthenticationResultMetadata.get -> Microsoft.Identity.Client.AuthenticationResultMetadata
Microsoft.Identity.Client.Extensibility.ManagedIdentityApplicationBuilderExtensions
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnBackgroundTokenRefreshCompleted(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func<Microsoft.Identity.Client.Extensibility.ExecutionResult, System.Threading.Tasks.Task> onBackgroundTokenRefreshCompleted) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ManagedIdentityApplicationBuilderExtensions.OnBackgroundTokenRefreshCompleted(this Microsoft.Identity.Client.ManagedIdentityApplicationBuilder builder, System.Func<Microsoft.Identity.Client.Extensibility.ExecutionResult, System.Threading.Tasks.Task> onBackgroundTokenRefreshCompleted) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ Microsoft.Identity.Client.AppConfig.PoPOptions.PoPOptions() -> void
static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey<T>(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder<T> builder, string key, string value, bool partitionRefreshToken) -> T
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder, Microsoft.Identity.Client.AppConfig.PoPOptions options) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Microsoft.Identity.Client.MsalException.AuthenticationResultMetadata.get -> Microsoft.Identity.Client.AuthenticationResultMetadata
Microsoft.Identity.Client.Extensibility.ManagedIdentityApplicationBuilderExtensions
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnBackgroundTokenRefreshCompleted(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func<Microsoft.Identity.Client.Extensibility.ExecutionResult, System.Threading.Tasks.Task> onBackgroundTokenRefreshCompleted) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ManagedIdentityApplicationBuilderExtensions.OnBackgroundTokenRefreshCompleted(this Microsoft.Identity.Client.ManagedIdentityApplicationBuilder builder, System.Func<Microsoft.Identity.Client.Extensibility.ExecutionResult, System.Threading.Tasks.Task> onBackgroundTokenRefreshCompleted) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ Microsoft.Identity.Client.AppConfig.PoPOptions.PoPOptions() -> void
static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey<T>(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder<T> builder, string key, string value, bool partitionRefreshToken) -> T
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder, Microsoft.Identity.Client.AppConfig.PoPOptions options) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Microsoft.Identity.Client.MsalException.AuthenticationResultMetadata.get -> Microsoft.Identity.Client.AuthenticationResultMetadata
Microsoft.Identity.Client.Extensibility.ManagedIdentityApplicationBuilderExtensions
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnBackgroundTokenRefreshCompleted(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func<Microsoft.Identity.Client.Extensibility.ExecutionResult, System.Threading.Tasks.Task> onBackgroundTokenRefreshCompleted) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ManagedIdentityApplicationBuilderExtensions.OnBackgroundTokenRefreshCompleted(this Microsoft.Identity.Client.ManagedIdentityApplicationBuilder builder, System.Func<Microsoft.Identity.Client.Extensibility.ExecutionResult, System.Threading.Tasks.Task> onBackgroundTokenRefreshCompleted) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ Microsoft.Identity.Client.AppConfig.PoPOptions.PoPOptions() -> void
static Microsoft.Identity.Client.Extensibility.AcquireTokenParameterBuilderExtensions.WithCachePartitionKey<T>(this Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder<T> builder, string key, string value, bool partitionRefreshToken) -> T
static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder, Microsoft.Identity.Client.AppConfig.PoPOptions options) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder
Microsoft.Identity.Client.MsalException.AuthenticationResultMetadata.get -> Microsoft.Identity.Client.AuthenticationResultMetadata
Microsoft.Identity.Client.Extensibility.ManagedIdentityApplicationBuilderExtensions
static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.OnBackgroundTokenRefreshCompleted(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func<Microsoft.Identity.Client.Extensibility.ExecutionResult, System.Threading.Tasks.Task> onBackgroundTokenRefreshCompleted) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder
static Microsoft.Identity.Client.Extensibility.ManagedIdentityApplicationBuilderExtensions.OnBackgroundTokenRefreshCompleted(this Microsoft.Identity.Client.ManagedIdentityApplicationBuilder builder, System.Func<Microsoft.Identity.Client.Extensibility.ExecutionResult, System.Threading.Tasks.Task> onBackgroundTokenRefreshCompleted) -> Microsoft.Identity.Client.ManagedIdentityApplicationBuilder
Loading
Loading