Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 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 @@ -521,7 +521,8 @@ await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration)
{
_telemetryClient.IncrementConfigurationRefreshRequestCounter(
validationParameters.ConfigurationManager.MetadataAddress,
TelemetryConstants.Protocols.Lkg);
TelemetryConstants.Protocols.Lkg,
TelemetryConstants.Protocols.ConfigurationSourceUnknown);

validationParameters.ConfigurationManager.RequestRefresh();
validationParameters.RefreshBeforeValidation = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
using System.Threading.Tasks;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Protocols.Configuration;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Telemetry;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.IdentityModel.Protocols
{
Expand Down Expand Up @@ -41,6 +41,11 @@ public partial class ConfigurationManager<T> : BaseConfigurationManager, IConfig
internal TimeProvider TimeProvider = TimeProvider.System;
internal ITelemetryClient TelemetryClient = new TelemetryClient();

/// <summary>
/// Gets or sets the optional configuration event handler.
/// </summary>
public IConfigurationEventHandler<T> ConfigurationEventHandler { get; set; }

/// <summary>
/// Instantiates a new <see cref="ConfigurationManager{T}"/> that manages automatic and controls refreshing on configuration data.
/// </summary>
Expand Down Expand Up @@ -135,6 +140,25 @@ public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> c
_configValidator = configValidator;
}

/// <summary>
/// Instantiates a new <see cref="ConfigurationManager{T}"/> with configuration validator that manages automatic and controls refreshing on configuration data.
/// </summary>
/// <param name="metadataAddress">The address to obtain configuration.</param>
/// <param name="configRetriever">The <see cref="IConfigurationRetriever{T}"/></param>
/// <param name="docRetriever">The <see cref="IDocumentRetriever"/> that reaches out to obtain the configuration.</param>
/// <param name="configValidator">The <see cref="IConfigurationValidator{T}"/></param>
/// <param name="lkgCacheOptions">The <see cref="LastKnownGoodConfigurationCacheOptions"/></param>
/// <param name="configurationEventHandler"> The <see cref="IConfigurationEventHandler{T}"/> that handles configuration events.</param>
/// <exception cref="ArgumentNullException">If 'configValidator' is null.</exception>
public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> configRetriever, IDocumentRetriever docRetriever, IConfigurationValidator<T> configValidator, LastKnownGoodConfigurationCacheOptions lkgCacheOptions, IConfigurationEventHandler<T> configurationEventHandler)
: this(metadataAddress, configRetriever, docRetriever, configValidator, lkgCacheOptions)
{
if (configurationEventHandler == null)
throw LogHelper.LogArgumentNullException(nameof(configurationEventHandler));

ConfigurationEventHandler = configurationEventHandler;
}

/// <summary>
/// Obtains an updated version of Configuration.
/// </summary>
Expand Down Expand Up @@ -203,6 +227,25 @@ private async Task<T> GetConfigurationNonBlockingAsync(CancellationToken cancel)

try
{
// Check if event handler can provide configuration
if (ConfigurationEventHandler != null)
{
var configurationRetrieved = await HandleBeforeRetrieveAsync(cancel).ConfigureAwait(false);

// replicate the behavior of successful retrieval from endpoint
if (configurationRetrieved != null && configurationRetrieved.Configuration != null)
{
TelemetryClient.IncrementConfigurationRefreshRequestCounter(
MetadataAddress,
TelemetryConstants.Protocols.FirstRefresh,
TelemetryConstants.Protocols.ConfigurationSourceHandler
);

UpdateConfiguration(configurationRetrieved.Configuration, configurationRetrieved.RetrievalTime);
return _currentConfiguration;
}
}

// Don't use the individual CT here, this is a shared operation that shouldn't be affected by an individual's cancellation.
// The transport should have its own timeouts, etc.
T configuration = await _configRetriever.GetConfigurationAsync(
Expand All @@ -227,9 +270,11 @@ private async Task<T> GetConfigurationNonBlockingAsync(CancellationToken cancel)

TelemetryClient.IncrementConfigurationRefreshRequestCounter(
MetadataAddress,
TelemetryConstants.Protocols.FirstRefresh);
TelemetryConstants.Protocols.FirstRefresh,
TelemetryConstants.Protocols.ConfigurationSourceRetriever
);

UpdateConfiguration(configuration);
UpdateConfiguration(configuration, TimeProvider.GetUtcNow().UtcDateTime);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
Expand All @@ -238,6 +283,7 @@ private async Task<T> GetConfigurationNonBlockingAsync(CancellationToken cancel)
TelemetryClient.IncrementConfigurationRefreshRequestCounter(
MetadataAddress,
TelemetryConstants.Protocols.FirstRefresh,
TelemetryConstants.Protocols.ConfigurationSourceRetriever,
ex);

LogHelper.LogExceptionMessage(
Expand All @@ -260,7 +306,8 @@ private async Task<T> GetConfigurationNonBlockingAsync(CancellationToken cancel)
{
TelemetryClient.IncrementConfigurationRefreshRequestCounter(
MetadataAddress,
TelemetryConstants.Protocols.Automatic);
TelemetryConstants.Protocols.Automatic,
TelemetryConstants.Protocols.ConfigurationSourceUnknown);

_ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None);
}
Expand Down Expand Up @@ -291,6 +338,19 @@ private void UpdateCurrentConfiguration()

try
{
// Check if event handler can provide configuration
if (ConfigurationEventHandler != null)
{
var configurationRetrieved = HandleBeforeRetrieveAsync().ConfigureAwait(false).GetAwaiter().GetResult();
if (configurationRetrieved != null && configurationRetrieved.Configuration != null)
{
UpdateConfiguration(configurationRetrieved.Configuration, configurationRetrieved.RetrievalTime);

_onBackgroundTaskFinish?.Invoke();
return;
}
}

T configuration = _configRetriever.GetConfigurationAsync(
MetadataAddress,
_docRetriever,
Expand All @@ -299,11 +359,12 @@ private void UpdateCurrentConfiguration()
var elapsedTime = TimeProvider.GetElapsedTime(startTimestamp);
TelemetryClient.LogConfigurationRetrievalDuration(
MetadataAddress,
TelemetryConstants.Protocols.ConfigurationSourceRetriever,
elapsedTime);

if (_configValidator == null)
{
UpdateConfiguration(configuration);
UpdateConfiguration(configuration, TimeProvider.GetUtcNow().UtcDateTime);
}
else
{
Expand All @@ -316,7 +377,7 @@ private void UpdateCurrentConfiguration()
LogMessages.IDX20810,
result.ErrorMessage)));
else
UpdateConfiguration(configuration);
UpdateConfiguration(configuration, TimeProvider.GetUtcNow().UtcDateTime);
}
}
#pragma warning disable CA1031 // Do not catch general exception types
Expand All @@ -325,6 +386,7 @@ private void UpdateCurrentConfiguration()
var elapsedTime = TimeProvider.GetElapsedTime(startTimestamp);
TelemetryClient.LogConfigurationRetrievalDuration(
MetadataAddress,
TelemetryConstants.Protocols.ConfigurationSourceRetriever,
elapsedTime,
ex);

Expand All @@ -345,11 +407,32 @@ private void UpdateCurrentConfiguration()
_onBackgroundTaskFinish?.Invoke();
}

private void UpdateConfiguration(T configuration)
private void UpdateConfiguration(T configuration, DateTimeOffset retrievalTime)
{
_currentConfiguration = configuration;
_syncAfter = DateTimeUtil.Add(TimeProvider.GetUtcNow().UtcDateTime, AutomaticRefreshInterval +
_syncAfter = DateTimeUtil.Add(retrievalTime.UtcDateTime, AutomaticRefreshInterval +
TimeSpan.FromSeconds(new Random().Next((int)AutomaticRefreshInterval.TotalSeconds / 20)));

if (ConfigurationEventHandler != null)
{
_ = Task.Run(async () =>
{
try
{
await ConfigurationEventHandler.AfterUpdateAsync(MetadataAddress, configuration).ConfigureAwait(false);
}
catch (Exception ex)
{
LogHelper.LogExceptionMessage(
new InvalidOperationException(
LogHelper.FormatInvariant(
LogMessages.IDX20812,
LogHelper.MarkAsNonPII(MetadataAddress ?? "null"),
ex),
ex));
}
});
}
}

/// <summary>
Expand Down Expand Up @@ -391,7 +474,8 @@ private void RequestRefreshBackgroundThread()
{
TelemetryClient.IncrementConfigurationRefreshRequestCounter(
MetadataAddress,
TelemetryConstants.Protocols.Manual);
TelemetryConstants.Protocols.Manual,
TelemetryConstants.Protocols.ConfigurationSourceUnknown);

_isFirstRefreshRequest = false;
if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverRunning, ConfigurationRetrieverIdle) == ConfigurationRetrieverIdle)
Expand All @@ -402,6 +486,63 @@ private void RequestRefreshBackgroundThread()
}
}

private async Task<ConfigurationEventHandlerResult<T>> HandleBeforeRetrieveAsync(CancellationToken cancel = default)
{
long beforeHandlerTimestamp = TimeProvider.GetTimestamp();

try
{
var handlerResult = await ConfigurationEventHandler.BeforeRetrieveAsync(MetadataAddress, cancel).ConfigureAwait(false);
if (handlerResult != null && handlerResult.Configuration != null)
{
var handlerElapsedTime = TimeProvider.GetElapsedTime(beforeHandlerTimestamp);
TelemetryClient.LogConfigurationRetrievalDuration(
MetadataAddress,
TelemetryConstants.Protocols.ConfigurationSourceHandler,
handlerElapsedTime);

// Validate configuration from handler
if (_configValidator != null)
{
ConfigurationValidationResult result = _configValidator.Validate(handlerResult.Configuration);
if (!result.Succeeded)
{
// Just log the error and proceed to fetch from endpoint
LogHelper.LogExceptionMessage(
new InvalidConfigurationException(
LogHelper.FormatInvariant(
LogMessages.IDX20812,
result.ErrorMessage)));

return ConfigurationEventHandlerResult<T>.NoResult;
}
}

// No validator configured, return configuration
return handlerResult;
}
}
catch (Exception ex)
{
var handlerErrorElapsedTime = TimeProvider.GetElapsedTime(beforeHandlerTimestamp);
TelemetryClient.LogConfigurationRetrievalDuration(
MetadataAddress,
TelemetryConstants.Protocols.ConfigurationSourceHandler,
handlerErrorElapsedTime,
ex);

LogHelper.LogExceptionMessage(
new InvalidOperationException(
LogHelper.FormatInvariant(
LogMessages.IDX20811,
LogHelper.MarkAsNonPII(MetadataAddress ?? "null"),
ex),
ex));
}

return ConfigurationEventHandlerResult<T>.NoResult;
}

/// <summary>
/// 12 hours is the default time interval that afterwards, <see cref="GetBaseConfigurationAsync(CancellationToken)"/> will obtain new configuration.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Microsoft.IdentityModel.Protocols
{
partial class ConfigurationManager<T> where T : class
public partial class ConfigurationManager<T> where T : class
{
private readonly SemaphoreSlim _refreshLock = new(1, 1);
private TimeSpan _bootstrapRefreshInterval = TimeSpan.FromSeconds(1);
Expand All @@ -34,13 +34,33 @@ private async Task<T> GetConfigurationWithBlockingAsync(CancellationToken cancel
{
try
{
// Check if event handler can provide configuration
if (ConfigurationEventHandler != null)
{
var configurationRetrieved = await HandleBeforeRetrieveAsync(cancel).ConfigureAwait(false);

// replicate the behavior of successful retrieval from endpoint
if (configurationRetrieved != null && configurationRetrieved.Configuration != null)
{
TelemetryForUpdateBlocking(TelemetryConstants.Protocols.ConfigurationSourceHandler);

if (_refreshRequested)
_refreshRequested = false;

UpdateConfiguration(configurationRetrieved.Configuration, configurationRetrieved.RetrievalTime);

return _currentConfiguration;
}
}

// Don't use the individual CT here, this is a shared operation that shouldn't be affected by an individual's cancellation.
// The transport should have it's own timeouts, etc..
var configuration = await _configRetriever.GetConfigurationAsync(MetadataAddress, _docRetriever, CancellationToken.None).ConfigureAwait(false);

var elapsedTime = TimeProvider.GetElapsedTime(startTimestamp);
TelemetryClient.LogConfigurationRetrievalDuration(
MetadataAddress,
TelemetryConstants.Protocols.ConfigurationSourceRetriever,
elapsedTime);

if (_configValidator != null)
Expand All @@ -52,12 +72,12 @@ private async Task<T> GetConfigurationWithBlockingAsync(CancellationToken cancel

_lastRequestRefresh = TimeProvider.GetUtcNow().UtcDateTime;

TelemetryForUpdateBlocking();
TelemetryForUpdateBlocking(TelemetryConstants.Protocols.ConfigurationSourceRetriever);

if (_refreshRequested)
_refreshRequested = false;

UpdateConfiguration(configuration);
UpdateConfiguration(configuration, TimeProvider.GetUtcNow().UtcDateTime);
}
catch (Exception ex)
{
Expand All @@ -82,6 +102,7 @@ private async Task<T> GetConfigurationWithBlockingAsync(CancellationToken cancel
TelemetryClient.IncrementConfigurationRefreshRequestCounter(
MetadataAddress,
TelemetryConstants.Protocols.FirstRefresh,
TelemetryConstants.Protocols.ConfigurationSourceRetriever,
ex);

throw LogHelper.LogExceptionMessage(
Expand All @@ -98,6 +119,7 @@ private async Task<T> GetConfigurationWithBlockingAsync(CancellationToken cancel

TelemetryClient.LogConfigurationRetrievalDuration(
MetadataAddress,
TelemetryConstants.Protocols.ConfigurationSourceRetriever,
elapsedTime,
ex);

Expand Down Expand Up @@ -139,7 +161,7 @@ private void RequestRefreshBlocking()
}
}

private void TelemetryForUpdateBlocking()
private void TelemetryForUpdateBlocking(string configurationSource)
{
string updateMode;

Expand All @@ -152,7 +174,8 @@ private void TelemetryForUpdateBlocking()
{
TelemetryClient.IncrementConfigurationRefreshRequestCounter(
MetadataAddress,
updateMode);
updateMode,
configurationSource);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
Expand Down
Loading
Loading