From b02514c72ee78d3078f0349df739aa3e638c15cb Mon Sep 17 00:00:00 2001 From: Kelly Song Date: Wed, 12 Jun 2024 17:06:25 -0700 Subject: [PATCH] initial changes --- .../Configuration/ConfigurationManager.cs | 69 +++++++++++++++---- .../DistributedConfigurationOptions.cs | 13 ++++ .../IDistributedConfigurationManager.cs | 34 +++++++++ .../BaseConfiguration.cs | 9 ++- .../IConfigurationRetrievalTime.cs | 17 +++++ 5 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Protocols/Configuration/DistributedConfigurationOptions.cs create mode 100644 src/Microsoft.IdentityModel.Protocols/Configuration/IDistributedConfigurationManager.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/IConfigurationRetrievalTime.cs diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs index 1365dd1fab..721034ef4c 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Diagnostics.Contracts; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -22,6 +21,7 @@ public class ConfigurationManager : BaseConfigurationManager, IConfigurationM private DateTimeOffset _syncAfter = DateTimeOffset.MinValue; private DateTimeOffset _lastRefresh = DateTimeOffset.MinValue; private bool _isFirstRefreshRequest = true; + private bool _skipDistributedConfigurationManager = false; private readonly SemaphoreSlim _refreshLock; private readonly IDocumentRetriever _docRetriever; @@ -30,6 +30,13 @@ public class ConfigurationManager : BaseConfigurationManager, IConfigurationM private T _currentConfiguration; private Exception _fetchMetadataFailure; private TimeSpan _bootstrapRefreshInterval = TimeSpan.FromSeconds(1); + private static readonly DistributedConfigurationOptions s_distributedConfigurationOptions = new(); + + // L2 TODO: internal until L2 cache is implemented S2S. + /// + /// TODO inject via ctors + /// + internal IDistributedConfigurationManager DistributedConfigurationManager { get; set; } /// /// Instantiates a new that manages automatic and controls refreshing on configuration data. @@ -156,21 +163,54 @@ public async Task GetConfigurationAsync(CancellationToken cancel) { try { - // 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); - if (_configValidator != null) + T configuration = null; + if (DistributedConfigurationManager != null && !_skipDistributedConfigurationManager) { - ConfigurationValidationResult result = _configValidator.Validate(configuration); - if (!result.Succeeded) - throw LogHelper.LogExceptionMessage(new InvalidConfigurationException(LogHelper.FormatInvariant(LogMessages.IDX20810, result.ErrorMessage))); + // TODO handle try/catch + configuration = await DistributedConfigurationManager.GetConfigurationAsync(MetadataAddress, s_distributedConfigurationOptions, CancellationToken.None).ConfigureAwait(false); + if (_configValidator != null) + { + // TODO don't throw but log another exception + ConfigurationValidationResult result = _configValidator.Validate(configuration); + if (!result.Succeeded) + { + configuration = null; + LogHelper.LogExceptionMessage(new InvalidConfigurationException(LogHelper.FormatInvariant(LogMessages.IDX20810, result.ErrorMessage))); + } + } } - _lastRefresh = DateTimeOffset.UtcNow; - // Add a random amount between 0 and 5% of AutomaticRefreshInterval jitter to avoid spike traffic to IdentityProvider. - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval + - TimeSpan.FromSeconds(new Random().Next((int)AutomaticRefreshInterval.TotalSeconds / 20))); - _currentConfiguration = configuration; + if (configuration == null) + { + // 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.. + configuration = await _configRetriever.GetConfigurationAsync(MetadataAddress, _docRetriever, CancellationToken.None).ConfigureAwait(false); + if (configuration is IConfigurationRetrievalTime configRetrievalTime) + configRetrievalTime.RetrievalTime = DateTimeOffset.UtcNow; + + if (_configValidator != null) + { + ConfigurationValidationResult result = _configValidator.Validate(configuration); + if (!result.Succeeded) + throw LogHelper.LogExceptionMessage(new InvalidConfigurationException(LogHelper.FormatInvariant(LogMessages.IDX20810, result.ErrorMessage))); + } + + if (_skipDistributedConfigurationManager) + _skipDistributedConfigurationManager = false; + + if (DistributedConfigurationManager != null) + // TODO fire and forget (or not if refresh happens on a background thread) + await DistributedConfigurationManager.SetConfigurationAsync(MetadataAddress, configuration, s_distributedConfigurationOptions, CancellationToken.None).ConfigureAwait(false); + + if (configuration is IConfigurationRetrievalTime configurationRetrievalTime) + _lastRefresh = configurationRetrievalTime.RetrievalTime; + else + _lastRefresh = DateTimeOffset.UtcNow; + + // Add a random amount between 0-5% of AutomaticRefreshInterval jitter to avoid spiking traffic to IdP. + _syncAfter = DateTimeUtil.Add(_lastRefresh.DateTime, AutomaticRefreshInterval + TimeSpan.FromSeconds(new Random().Next((int)(AutomaticRefreshInterval.TotalSeconds / 20)))); + _currentConfiguration = configuration; + } } catch (Exception ex) { @@ -250,6 +290,9 @@ public override void RequestRefresh() { _syncAfter = now; _isFirstRefreshRequest = false; + + if (DistributedConfigurationManager != null) + _skipDistributedConfigurationManager = true; } else if (now >= DateTimeUtil.Add(_lastRefresh.UtcDateTime, RefreshInterval)) { diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/DistributedConfigurationOptions.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/DistributedConfigurationOptions.cs new file mode 100644 index 0000000000..08bddf45a7 --- /dev/null +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/DistributedConfigurationOptions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.IdentityModel.Protocols.Configuration +{ + /// + /// + /// + // L2 TODO: internal until L2 cache is implemented S2S. + internal class DistributedConfigurationOptions + { + } +} diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/IDistributedConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/IDistributedConfigurationManager.cs new file mode 100644 index 0000000000..f3a8617358 --- /dev/null +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/IDistributedConfigurationManager.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.IdentityModel.Protocols.Configuration +{ + /// + /// + /// + // L2 TODO: internal until L2 cache is implemented S2S. + internal interface IDistributedConfigurationManager where T : class + { + /// + /// + /// + /// + /// + /// + /// + Task GetConfigurationAsync(string metadataAddress, DistributedConfigurationOptions distributedConfigurationOptions, CancellationToken cancellationToken = default); + + /// + /// + /// + /// + /// + /// + /// + /// + Task SetConfigurationAsync(string metadataAddress, T configuration, DistributedConfigurationOptions distributedConfigurationOptions, CancellationToken cancellationToken = default); + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/BaseConfiguration.cs b/src/Microsoft.IdentityModel.Tokens/BaseConfiguration.cs index 6f25f80c54..91dce52b34 100644 --- a/src/Microsoft.IdentityModel.Tokens/BaseConfiguration.cs +++ b/src/Microsoft.IdentityModel.Tokens/BaseConfiguration.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text.Json.Serialization; @@ -10,7 +11,7 @@ namespace Microsoft.IdentityModel.Tokens /// /// Represents a generic metadata configuration which is applicable for both XML and JSON based configurations. /// - public abstract class BaseConfiguration + public abstract class BaseConfiguration /*: IConfigurationRetrievalTime*/ // L2 TODO: internal until L2 cache is implemented S2S. { /// /// Gets the issuer specified via the metadata endpoint. @@ -46,5 +47,11 @@ public virtual ICollection TokenDecryptionKeys { get; } = new Collection(); + + /// + /// + /// + // L2 TODO: internal until L2 cache is implemented S2S. + internal DateTimeOffset RetrievalTime { get; set; } } } diff --git a/src/Microsoft.IdentityModel.Tokens/IConfigurationRetrievalTime.cs b/src/Microsoft.IdentityModel.Tokens/IConfigurationRetrievalTime.cs new file mode 100644 index 0000000000..ec160015b8 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/IConfigurationRetrievalTime.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// + /// + // L2 TODO: internal until L2 cache is implemented S2S. + internal interface IConfigurationRetrievalTime + { + // L2 TODO: internal until L2 cache is implemented S2S. + internal DateTimeOffset RetrievalTime { get; set; } + } +}