diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs index ae5b756c5c..8ab7db9a22 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs @@ -55,6 +55,7 @@ public ConfigurationValidationResult Validate(OpenIdConnectConfiguration openIdC { ErrorMessage = LogHelper.FormatInvariant( LogMessages.IDX21818, + this, LogHelper.MarkAsNonPII(MinimumNumberOfKeys), LogHelper.MarkAsNonPII(numberOfValidKeys), string.IsNullOrEmpty(convertKeyInfos) ? "None" : convertKeyInfos), diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/LogMessages.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/LogMessages.cs index 0ebe0d86f5..642addcf03 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/LogMessages.cs @@ -78,7 +78,7 @@ internal static class LogMessages internal const string IDX21815 = "IDX21815: Error deserializing json: '{0}' into '{1}'."; internal const string IDX21816 = "IDX21816: The number of signing keys must be greater or equal to '{0}'. Value: '{1}'."; internal const string IDX21817 = "IDX21817: The OpenIdConnectConfiguration did not contain any JsonWebKeys. This is required to validate the configuration."; - internal const string IDX21818 = "IDX21818: The OpenIdConnectConfiguration's valid signing keys cannot be less than {0}. Values: {1}. Invalid keys: {2}"; + internal const string IDX21818 = "IDX21818: IConfigurationValidator '{0}', requires '{1}' valid signing keys, found: {2}. Invalid keys: {3}"; #pragma warning restore 1591 } } diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs index cd06188f0f..c85a1416c7 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -18,8 +18,6 @@ namespace Microsoft.IdentityModel.Protocols [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] public class ConfigurationManager : BaseConfigurationManager, IConfigurationManager where T : class { - private DateTimeOffset _syncAfter = DateTimeOffset.MinValue; - private DateTimeOffset _lastRequestRefresh = DateTimeOffset.MinValue; private bool _isFirstRefreshRequest = true; private readonly SemaphoreSlim _configurationNullLock = new SemaphoreSlim(1); @@ -133,7 +131,8 @@ public ConfigurationManager(string metadataAddress, IConfigurationRetriever c /// Obtains an updated version of Configuration. /// /// Configuration of type T. - /// If the time since the last call is less than then is not called and the current Configuration is returned. + /// If the time since the last call is less than then + /// is not called and the current Configuration is returned. public async Task GetConfigurationAsync() { return await GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); @@ -144,11 +143,17 @@ public async Task GetConfigurationAsync() /// /// CancellationToken /// Configuration of type T. - /// If the time since the last call is less than then is not called and the current Configuration is returned. + /// If the time since the last call is less than then + /// is not called and the current Configuration is returned. public virtual async Task GetConfigurationAsync(CancellationToken cancel) { - if (_currentConfiguration != null && _syncAfter > DateTimeOffset.UtcNow) - return _currentConfiguration; + if (_currentConfiguration != null) + { + // StartupTime is the time when ConfigurationManager was instantiated. + double nextRefresh = _automaticRefreshIntervalInSeconds + _timeInSecondsWhenLastRefreshOccurred; + if (nextRefresh > GetSecondsSinceInstanceWasCreated) + return _currentConfiguration; + } Exception fetchMetadataFailure = null; @@ -157,7 +162,7 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) // reach out to the metadata endpoint. Since multiple threads could be calling this method // we need to ensure that only one thread is actually fetching the metadata. // else - // if task is running, return the current configuration + // if update task is running, return the current configuration // else kick off task to update current configuration if (_currentConfiguration == null) { @@ -168,9 +173,11 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) return _currentConfiguration; } -#pragma warning disable CA1031 // Do not catch general exception types try { + _configurationRetrieverState = ConfigurationRetrieverRunning; + Interlocked.Increment(ref _numberOfTimesAutomaticRefreshRequested); + // 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. T configuration = await _configRetriever.GetConfigurationAsync( @@ -192,7 +199,9 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) UpdateConfiguration(configuration); } +#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types { fetchMetadataFailure = ex; @@ -206,15 +215,16 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) } finally { + _configurationRetrieverState = ConfigurationRetrieverIdle; _configurationNullLock.Release(); } -#pragma warning restore CA1031 // Do not catch general exception types } else { if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverRunning, ConfigurationRetrieverIdle) == ConfigurationRetrieverIdle) { - _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); + Interlocked.Increment(ref _numberOfTimesAutomaticRefreshRequested); + _ = Task.Run(RetrieveAndUpdateConfiguration, CancellationToken.None); } } @@ -227,7 +237,7 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) LogHelper.FormatInvariant( LogMessages.IDX20803, LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), - LogHelper.MarkAsNonPII(_syncAfter), + LogHelper.MarkAsNonPII(_timeInSecondsWhenLastRefreshOccurred), LogHelper.MarkAsNonPII(fetchMetadataFailure)), fetchMetadataFailure)); } @@ -235,11 +245,10 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) /// /// This should be called when the configuration needs to be updated either from RequestRefresh or AutomaticRefresh /// The Caller should first check the state checking state using: - /// if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverIdle, ConfigurationRetrieverRunning) != ConfigurationRetrieverRunning). + /// if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverRunning, ConfigurationRetrieverIdle) == ConfigurationRetrieverIdle). /// - private void UpdateCurrentConfiguration() + private void RetrieveAndUpdateConfiguration() { -#pragma warning disable CA1031 // Do not catch general exception types try { T configuration = _configRetriever.GetConfigurationAsync( @@ -265,7 +274,9 @@ private void UpdateCurrentConfiguration() UpdateConfiguration(configuration); } } +#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types { LogHelper.LogExceptionMessage( new InvalidOperationException( @@ -279,14 +290,24 @@ private void UpdateCurrentConfiguration() { Interlocked.Exchange(ref _configurationRetrieverState, ConfigurationRetrieverIdle); } -#pragma warning restore CA1031 // Do not catch general exception types } + /// + /// Called only when configuration is successfully obtained. + /// + /// Set to this value. private void UpdateConfiguration(T configuration) { _currentConfiguration = configuration; - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval + - TimeSpan.FromSeconds(new Random().Next((int)AutomaticRefreshInterval.TotalSeconds / 20))); + // StartupTime is the time when ConfigurationManager was instantiated. + // SecondsSinceInstanceWasCreated is the number of seconds since ConfigurationManager was instantiated. + // For automatic refresh, we add a 5% jitter. + // Record in seconds when the last time configuration was obtained. + double timeInSecondsWhenLastAutomaticRefreshOccurred = GetSecondsSinceInstanceWasCreated + + ((_automaticRefreshIntervalInSeconds >= int.MaxValue) ? 0 : (_random.Next((int)_maxJitter))); + + // transfer to int in single operation. + _timeInSecondsWhenLastRefreshOccurred = (int)((timeInSecondsWhenLastAutomaticRefreshOccurred <= int.MaxValue) ? (int)timeInSecondsWhenLastAutomaticRefreshOccurred : int.MaxValue); } /// @@ -294,7 +315,7 @@ private void UpdateConfiguration(T configuration) /// /// CancellationToken /// Configuration of type BaseConfiguration . - /// If the time since the last call is less than then is not called and the current Configuration is returned. + /// If the time since the last call is less than then is not called and the current Configuration is returned. public override async Task GetBaseConfigurationAsync(CancellationToken cancel) { T obj = await GetConfigurationAsync(cancel).ConfigureAwait(false); @@ -309,19 +330,30 @@ public override async Task GetBaseConfigurationAsync(Cancella /// public override void RequestRefresh() { - DateTimeOffset now = DateTimeOffset.UtcNow; + if (_configurationRetrieverState == ConfigurationRetrieverRunning) + return; - if (now >= DateTimeUtil.Add(_lastRequestRefresh.UtcDateTime, RefreshInterval) || _isFirstRefreshRequest) + double nextRefresh = _requestRefreshIntervalInSeconds + _timeInSecondsWhenLastRequestRefreshWasRequested; + if (nextRefresh < GetSecondsSinceInstanceWasCreated || _isFirstRefreshRequest) { _isFirstRefreshRequest = false; if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverRunning, ConfigurationRetrieverIdle) == ConfigurationRetrieverIdle) { - _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); - _lastRequestRefresh = now; + Interlocked.Increment(ref _numberOfTimesRequestRefreshRequested); + double recordWhenRefreshOccurred = GetSecondsSinceInstanceWasCreated; + // transfer to int in single operation. + _timeInSecondsWhenLastRequestRefreshWasRequested = (int)((recordWhenRefreshOccurred <= int.MaxValue) ? (int)recordWhenRefreshOccurred : int.MaxValue); + _ = Task.Run(RetrieveAndUpdateConfiguration, CancellationToken.None); } } } + /// + /// SecondsSinceInstanceWasCreated is the number of seconds since ConfigurationManager was instantiated. + /// + /// double + private double GetSecondsSinceInstanceWasCreated => (DateTimeOffset.UtcNow - StartupTime).TotalSeconds; + /// /// 12 hours is the default time interval that afterwards, will obtain new configuration. /// diff --git a/src/Microsoft.IdentityModel.Protocols/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Protocols/InternalAPI.Unshipped.txt index e69de29bb2..c723c25adf 100644 --- a/src/Microsoft.IdentityModel.Protocols/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Protocols/InternalAPI.Unshipped.txt @@ -0,0 +1 @@ +const Microsoft.IdentityModel.Protocols.LogMessages.IDX20803 = "IDX20803: Unable to obtain configuration from: '{0}'. Will retry in '{1}' seconds. Exception: '{2}'." -> string \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Protocols/LogMessages.cs b/src/Microsoft.IdentityModel.Protocols/LogMessages.cs index aef3aaa132..9746f5abbb 100644 --- a/src/Microsoft.IdentityModel.Protocols/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Protocols/LogMessages.cs @@ -21,7 +21,7 @@ internal static class LogMessages internal const string IDX20108 = "IDX20108: The address specified '{0}' is not valid as per HTTPS scheme. Please specify an https address for security reasons. If you want to test with http address, set the RequireHttps property on IDocumentRetriever to false."; // configuration retrieval errors - internal const string IDX20803 = "IDX20803: Unable to obtain configuration from: '{0}'. Will retry at '{1}'. Exception: '{2}'."; + internal const string IDX20803 = "IDX20803: Unable to obtain configuration from: '{0}'. Will retry in '{1}' seconds. Exception: '{2}'."; internal const string IDX20804 = "IDX20804: Unable to retrieve document from: '{0}'."; internal const string IDX20805 = "IDX20805: Obtaining information from metadata endpoint: '{0}'."; internal const string IDX20806 = "IDX20806: Unable to obtain an updated configuration from: '{0}'. Returning the current configuration. Exception: '{1}."; diff --git a/src/Microsoft.IdentityModel.Tokens/BaseConfigurationManager.cs b/src/Microsoft.IdentityModel.Tokens/BaseConfigurationManager.cs index 2124c416c4..a64f22c99f 100644 --- a/src/Microsoft.IdentityModel.Tokens/BaseConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Tokens/BaseConfigurationManager.cs @@ -16,13 +16,22 @@ namespace Microsoft.IdentityModel.Tokens public abstract class BaseConfigurationManager { private TimeSpan _automaticRefreshInterval = DefaultAutomaticRefreshInterval; + internal double _automaticRefreshIntervalInSeconds = DefaultAutomaticRefreshInterval.TotalSeconds; + internal double _maxJitter = DefaultAutomaticRefreshInterval.TotalSeconds / 20; private TimeSpan _refreshInterval = DefaultRefreshInterval; + internal double _requestRefreshIntervalInSeconds = DefaultRefreshInterval.TotalSeconds; private TimeSpan _lastKnownGoodLifetime = DefaultLastKnownGoodConfigurationLifetime; private BaseConfiguration _lastKnownGoodConfiguration; private DateTime? _lastKnownGoodConfigFirstUse; + internal Random _random = new(); + + // Seconds since the BaseConfigurationManager was created when the last refresh occurred with a %5 random jitter. + internal int _timeInSecondsWhenLastRefreshOccurred; + internal int _timeInSecondsWhenLastRequestRefreshWasRequested; internal EventBasedLRUCache _lastKnownGoodConfigurationCache; + /// /// Gets or sets the that controls how often an automatic metadata refresh should occur. /// @@ -32,9 +41,59 @@ public TimeSpan AutomaticRefreshInterval set { if (value < MinimumAutomaticRefreshInterval) - throw LogHelper.LogExceptionMessage(new ArgumentOutOfRangeException(nameof(value), LogHelper.FormatInvariant(LogMessages.IDX10108, LogHelper.MarkAsNonPII(MinimumAutomaticRefreshInterval), LogHelper.MarkAsNonPII(value)))); + throw LogHelper.LogExceptionMessage( + new ArgumentOutOfRangeException( + nameof(value), + LogHelper.FormatInvariant( + LogMessages.IDX10108, + LogHelper.MarkAsNonPII(MinimumAutomaticRefreshInterval), + LogHelper.MarkAsNonPII(value)))); _automaticRefreshInterval = value; + Interlocked.Exchange(ref _automaticRefreshIntervalInSeconds, value.TotalSeconds); + Interlocked.Exchange(ref _maxJitter, value.TotalSeconds / 20); + } + } + + /// + /// Records the time this instance was created. + /// Used to determine if the automatic refresh or request refresh interval has passed. + /// The logic is to remember the number of seconds since startup that the last refresh occurred. + /// Store that value in _timeInSecondsWhenLastAutomaticRefreshOccurred or _timeInSecondsWhenLastRequestRefreshOccurred. + /// Then compare to (UtcNow - Startup).TotalSeconds. + /// The set is used for testing purposes. + /// + internal DateTimeOffset StartupTime { get; set; } = DateTimeOffset.UtcNow; + + /// + /// Incremented each time results in a http request. + /// + internal long _numberOfTimesAutomaticRefreshRequested; + + /// + /// Thread safe getter for . + /// + internal long NumberOfTimesAutomaticRefreshRequested + { + get + { + return Interlocked.Read(ref _numberOfTimesAutomaticRefreshRequested); + } + } + + /// + /// Incremented each time results in a http request. + /// + internal long _numberOfTimesRequestRefreshRequested; + + /// + /// Thread safe getter for . + /// + internal long NumberOfTimesRequestRefreshRequested + { + get + { + return Interlocked.Read(ref _numberOfTimesRequestRefreshRequested); } } @@ -165,6 +224,7 @@ public TimeSpan RefreshInterval throw LogHelper.LogExceptionMessage(new ArgumentOutOfRangeException(nameof(value), LogHelper.FormatInvariant(LogMessages.IDX10107, LogHelper.MarkAsNonPII(MinimumRefreshInterval), LogHelper.MarkAsNonPII(value)))); _refreshInterval = value; + Interlocked.Exchange(ref _requestRefreshIntervalInSeconds, value.TotalSeconds); } } diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index 23c18b30a7..91a5d27296 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -12,12 +12,24 @@ Microsoft.IdentityModel.Tokens.AudienceValidationError.TokenAudiences.get -> Sys Microsoft.IdentityModel.Tokens.AudienceValidationError.TokenAudiences.set -> void Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidAudiences.get -> System.Collections.Generic.IList Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidAudiences.set -> void +Microsoft.IdentityModel.Tokens.BaseConfigurationManager.NumberOfTimesAutomaticRefreshRequested.get -> long +Microsoft.IdentityModel.Tokens.BaseConfigurationManager.NumberOfTimesRequestRefreshRequested.get -> long +Microsoft.IdentityModel.Tokens.BaseConfigurationManager._numberOfTimesAutomaticRefreshRequested -> long +Microsoft.IdentityModel.Tokens.BaseConfigurationManager._numberOfTimesRequestRefreshRequested -> long +Microsoft.IdentityModel.Tokens.BaseConfigurationManager._timeInSecondsWhenLastRefreshOccurred -> int +Microsoft.IdentityModel.Tokens.BaseConfigurationManager._timeInSecondsWhenLastRequestRefreshWasRequested -> int Microsoft.IdentityModel.Tokens.IssuerSigningKeyValidationError Microsoft.IdentityModel.Tokens.IssuerSigningKeyValidationError.InvalidSigningKey.get -> Microsoft.IdentityModel.Tokens.SecurityKey Microsoft.IdentityModel.Tokens.IssuerSigningKeyValidationError.InvalidSigningKey.set -> void Microsoft.IdentityModel.Tokens.IssuerSigningKeyValidationError.IssuerSigningKeyValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, Microsoft.IdentityModel.Tokens.SecurityKey invalidSigningKey, Microsoft.IdentityModel.Tokens.ValidationFailureType failureType = null, System.Exception innerException = null) -> void Microsoft.IdentityModel.Tokens.IssuerValidationError.InvalidIssuer.get -> string Microsoft.IdentityModel.Tokens.IssuerValidationError.IssuerValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidIssuer, System.Exception innerException = null) -> void +Microsoft.IdentityModel.Tokens.BaseConfigurationManager.StartupTime.get -> System.DateTimeOffset +Microsoft.IdentityModel.Tokens.BaseConfigurationManager.StartupTime.set -> void +Microsoft.IdentityModel.Tokens.BaseConfigurationManager._automaticRefreshIntervalInSeconds -> double +Microsoft.IdentityModel.Tokens.BaseConfigurationManager._maxJitter -> double +Microsoft.IdentityModel.Tokens.BaseConfigurationManager._random -> System.Random +Microsoft.IdentityModel.Tokens.BaseConfigurationManager._requestRefreshIntervalInSeconds -> double Microsoft.IdentityModel.Tokens.IssuerValidationSource.IssuerMatchedConfiguration = 1 -> Microsoft.IdentityModel.Tokens.IssuerValidationSource Microsoft.IdentityModel.Tokens.IssuerValidationSource.IssuerMatchedValidationParameters = 2 -> Microsoft.IdentityModel.Tokens.IssuerValidationSource Microsoft.IdentityModel.Tokens.LifetimeValidationError.Expires.get -> System.DateTime? diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs index 83d7f5d69c..1f47324172 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// Ignore Spelling: Metadata Validator Retreiver - using System; using System.Collections.Generic; using System.Diagnostics.Tracing; @@ -39,7 +37,7 @@ public async Task GetPublicMetadata(ConfigurationManagerTheoryData( theoryData.MetadataAddress, - theoryData.ConfigurationRetreiver, + theoryData.ConfigurationRetriever, theoryData.DocumentRetriever, theoryData.ConfigurationValidator); @@ -62,7 +60,7 @@ public static TheoryData("AccountsGoogleCom") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AccountsGoogle @@ -70,7 +68,7 @@ public static TheoryData("AADCommonUrl") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AADCommonUrl @@ -78,7 +76,7 @@ public static TheoryData("AADCommonUrlV1") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AADCommonUrlV1 @@ -86,7 +84,7 @@ public static TheoryData("AADCommonUrlV2") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AADCommonUrlV2 @@ -101,7 +99,7 @@ public void OpenIdConnectConstructor(ConfigurationManagerTheoryData(theoryData.MetadataAddress, theoryData.ConfigurationRetreiver, theoryData.DocumentRetriever, theoryData.ConfigurationValidator); + var configurationManager = new ConfigurationManager(theoryData.MetadataAddress, theoryData.ConfigurationRetriever, theoryData.DocumentRetriever, theoryData.ConfigurationValidator); theoryData.ExpectedException.ProcessNoException(); } catch (Exception ex) @@ -120,7 +118,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -131,7 +129,7 @@ public static TheoryData { - ConfigurationRetreiver = null, + ConfigurationRetriever = null, ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -141,7 +139,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = null, ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -151,7 +149,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = null, DocumentRetriever = new HttpDocumentRetriever(), ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -210,97 +208,10 @@ public async Task FetchMetadataFailureTest() } [Fact] - public async Task VerifyInterlockGuardForRequestRefresh() - { - ManualResetEvent waitEvent = new ManualResetEvent(false); - ManualResetEvent signalEvent = new ManualResetEvent(false); - InMemoryDocumentRetriever inMemoryDocumentRetriever = InMemoryDocumentRetrieverWithEvents(waitEvent, signalEvent); - - var configurationManager = new ConfigurationManager( - "AADCommonV1Json", - new OpenIdConnectConfigurationRetriever(), - inMemoryDocumentRetriever); - - // populate the configurationManager with AADCommonV1Config - TestUtilities.SetField(configurationManager, "_currentConfiguration", OpenIdConfigData.AADCommonV1Config); - - // InMemoryDocumentRetrieverWithEvents will block until waitEvent.Set() is called. - // The first RequestRefresh will not have finished before the next RequestRefresh() is called. - // The guard '_lastRequestRefresh' will not block as we set it to DateTimeOffset.MinValue. - // Interlocked guard will block. - // Configuration should be AADCommonV1Config - signalEvent.Reset(); - configurationManager.RequestRefresh(); - - // InMemoryDocumentRetrieverWithEvents will signal when it is OK to change the MetadataAddress - // otherwise, it may be the case that the MetadataAddress is changed before the previous Task has finished. - signalEvent.WaitOne(); - - // AADCommonV1Json would have been passed to the the previous retriever, which is blocked on an event. - configurationManager.MetadataAddress = "AADCommonV2Json"; - TestUtilities.SetField(configurationManager, "_lastRequestRefresh", DateTimeOffset.MinValue); - configurationManager.RequestRefresh(); - - // Set the event to release the lock and let the previous retriever finish. - waitEvent.Set(); - - // Configuration should be AADCommonV1Config - var configuration = await configurationManager.GetConfigurationAsync(); - Assert.True(configuration.Issuer.Equals(OpenIdConfigData.AADCommonV1Config.Issuer), - $"configuration.Issuer from configurationManager was not as expected," + - $"configuration.Issuer: '{configuration.Issuer}' != expected '{OpenIdConfigData.AADCommonV1Config.Issuer}'."); - } - - [Fact] - public async Task VerifyInterlockGuardForGetConfigurationAsync() - { - ManualResetEvent waitEvent = new ManualResetEvent(false); - ManualResetEvent signalEvent = new ManualResetEvent(false); - - InMemoryDocumentRetriever inMemoryDocumentRetriever = InMemoryDocumentRetrieverWithEvents(waitEvent, signalEvent); - waitEvent.Set(); - - var configurationManager = new ConfigurationManager( - "AADCommonV1Json", - new OpenIdConnectConfigurationRetriever(), - inMemoryDocumentRetriever); - - OpenIdConnectConfiguration configuration = await configurationManager.GetConfigurationAsync(); - - // InMemoryDocumentRetrieverWithEvents will block until waitEvent.Set() is called. - // The GetConfigurationAsync to update config will not have finished before the next GetConfigurationAsync() is called. - // The guard '_syncAfter' will not block as we set it to DateTimeOffset.MinValue. - // Interlocked guard should block. - // Configuration should be AADCommonV1Config - - waitEvent.Reset(); - signalEvent.Reset(); - - TestUtilities.SetField(configurationManager, "_syncAfter", DateTimeOffset.MinValue); - await configurationManager.GetConfigurationAsync(CancellationToken.None); - - // InMemoryDocumentRetrieverWithEvents will signal when it is OK to change the MetadataAddress - // otherwise, it may be the case that the MetadataAddress is changed before the previous Task has finished. - signalEvent.WaitOne(); - - // AADCommonV1Json would have been passed to the the previous retriever, which is blocked on an event. - configurationManager.MetadataAddress = "AADCommonV2Json"; - await configurationManager.GetConfigurationAsync(CancellationToken.None); - - // Set the event to release the lock and let the previous retriever finish. - waitEvent.Set(); - - // Configuration should be AADCommonV1Config - configuration = await configurationManager.GetConfigurationAsync(); - Assert.True(configuration.Issuer.Equals(OpenIdConfigData.AADCommonV1Config.Issuer), - $"configuration.Issuer from configurationManager was not as expected," + - $" configuration.Issuer: '{configuration.Issuer}' != expected: '{OpenIdConfigData.AADCommonV1Config.Issuer}'."); - } - - [Fact] - public async Task BootstrapRefreshIntervalTest() + public async Task FaultOnFirstRequest() { - var context = new CompareContext($"{this}.BootstrapRefreshIntervalTest"); + // Tests that on first fault, the ConfigurationManager does not change the _nextAutomaticRefreshAfterSeconds or _nextRequestRefreshAfterSeconds. + var context = new CompareContext($"{this}.FaultOnFirstRequest"); var documentRetriever = new HttpDocumentRetriever( HttpResponseMessageUtils.SetupHttpClientThatReturns("OpenIdConnectMetadata.json", HttpStatusCode.NotFound)); @@ -311,37 +222,39 @@ public async Task BootstrapRefreshIntervalTest() documentRetriever) { RefreshInterval = TimeSpan.FromSeconds(2) }; - // ConfigurationManager._syncAfter is set to DateTimeOffset.MinValue on startup - // If obtaining the metadata fails due to error, the value should not change + double firstNextAutomaticRefreshAfterSeconds = configManager._timeInSecondsWhenLastRefreshOccurred; + // ConfigurationManager._nextAutomaticRefreshAfterSeconds should not have changed if obtaining the metadata faults. try { - var configuration = await configManager.GetConfigurationAsync(); + var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); } catch (Exception firstFetchMetadataFailure) { - // _syncAfter should not have been changed, because the fetch failed. - var syncAfter = TestUtilities.GetField(configManager, "_syncAfter"); - if ((DateTimeOffset)syncAfter != DateTimeOffset.MinValue) - context.AddDiff($"ConfigurationManager._syncAfter: '{syncAfter}' should equal '{DateTimeOffset.MinValue}'."); + // _nextAutomaticRefreshAfterSeconds should not have been changed due to fault. + double secondNextAutomaticRefreshAfterSeconds = configManager._timeInSecondsWhenLastRefreshOccurred; + + if (firstNextAutomaticRefreshAfterSeconds != secondNextAutomaticRefreshAfterSeconds) + context.AddDiff($"firstNextAutomaticRefreshAfterSeconds '{firstNextAutomaticRefreshAfterSeconds}' != secondNextAutomaticRefreshAfterSeconds: '{secondNextAutomaticRefreshAfterSeconds}'."); if (firstFetchMetadataFailure.InnerException == null) context.AddDiff($"Expected exception to contain inner exception for fetch metadata failure."); // Fetch metadata again during refresh interval, the exception should be same from above. + double firstNextRequestRefreshAfterSeconds = configManager._timeInSecondsWhenLastRefreshOccurred; + try { configManager.RequestRefresh(); - var configuration = await configManager.GetConfigurationAsync(); + var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); } catch (Exception secondFetchMetadataFailure) { if (secondFetchMetadataFailure.InnerException == null) context.AddDiff($"Expected exception to contain inner exception for fetch metadata failure."); - // _syncAfter should not have been changed, because the fetch failed. - syncAfter = TestUtilities.GetField(configManager, "_syncAfter"); - if ((DateTimeOffset)syncAfter != DateTimeOffset.MinValue) - context.AddDiff($"ConfigurationManager._syncAfter: '{syncAfter}' should equal '{DateTimeOffset.MinValue}'."); + double secondNextRequestRefreshAfterSeconds = configManager._timeInSecondsWhenLastRefreshOccurred; + if (firstNextRequestRefreshAfterSeconds != secondNextAutomaticRefreshAfterSeconds) + context.AddDiff($"firstNextRequestRefreshAfterSeconds '{firstNextRequestRefreshAfterSeconds}' != secondNextRequestRefreshAfterSeconds: '{secondNextRequestRefreshAfterSeconds}'."); IdentityComparer.AreEqual(firstFetchMetadataFailure, secondFetchMetadataFailure, context); } @@ -384,84 +297,6 @@ public void GetSets() TestUtilities.AssertFailIfErrors("ConfigurationManager_GetSets", context.Errors); } - [Theory, MemberData(nameof(AutomaticIntervalTestCases), DisableDiscoveryEnumeration = true)] - public async Task AutomaticRefreshInterval(ConfigurationManagerTheoryData theoryData) - { - var context = new CompareContext($"{this}.AutomaticRefreshInterval"); - - try - { - - var configuration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None); - IdentityComparer.AreEqual(configuration, theoryData.ExpectedConfiguration, context); - - theoryData.ConfigurationManager.MetadataAddress = theoryData.UpdatedMetadataAddress; - TestUtilities.SetField(theoryData.ConfigurationManager, "_syncAfter", theoryData.SyncAfter); - var updatedConfiguration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None); - // we wait 50 ms here to make the task is finished. - Thread.Sleep(50); - updatedConfiguration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None); - IdentityComparer.AreEqual(updatedConfiguration, theoryData.ExpectedUpdatedConfiguration, context); - - theoryData.ExpectedException.ProcessNoException(context); - } - catch (Exception ex) - { - theoryData.ExpectedException.ProcessException(ex, context); - } - - TestUtilities.AssertFailIfErrors(context); - } - - public static TheoryData> AutomaticIntervalTestCases - { - get - { - var theoryData = new TheoryData>(); - - // Failing to get metadata returns existing. - theoryData.Add(new ConfigurationManagerTheoryData("HttpFault_ReturnExisting") - { - ConfigurationManager = new ConfigurationManager( - "AADCommonV1Json", - new OpenIdConnectConfigurationRetriever(), - InMemoryDocumentRetriever), - ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, - ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV1Config, - SyncAfter = DateTime.UtcNow - TimeSpan.FromDays(2), - UpdatedMetadataAddress = "https://httpstat.us/429" - }); - - // AutomaticRefreshInterval interval should return same config. - theoryData.Add(new ConfigurationManagerTheoryData("AutomaticRefreshIntervalNotHit") - { - ConfigurationManager = new ConfigurationManager( - "AADCommonV1Json", - new OpenIdConnectConfigurationRetriever(), - InMemoryDocumentRetriever), - ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, - ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV1Config, - SyncAfter = DateTime.UtcNow + TimeSpan.FromDays(2), - UpdatedMetadataAddress = "AADCommonV2Json" - }); - - // AutomaticRefreshInterval should pick up new bits. - theoryData.Add(new ConfigurationManagerTheoryData("AutomaticRefreshIntervalHit") - { - ConfigurationManager = new ConfigurationManager( - "AADCommonV1Json", - new OpenIdConnectConfigurationRetriever(), - InMemoryDocumentRetriever), - ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, - ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV2Config, - SyncAfter = DateTime.UtcNow, - UpdatedMetadataAddress = "AADCommonV2Json" - }); - - return theoryData; - } - } - [Theory, MemberData(nameof(RequestRefreshTestCases), DisableDiscoveryEnumeration = true)] public async Task RequestRefresh(ConfigurationManagerTheoryData theoryData) { @@ -472,7 +307,7 @@ public async Task RequestRefresh(ConfigurationManagerTheoryData("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); + CompareContext context = TestUtilities.WriteHeader($"{this}", $"{nameof(AutomaticRefreshBlocked)}", false); - // This is the minimum time that should pass before an automatic refresh occurs - // stored in advance to avoid any time drift issues. - DateTimeOffset minimumRefreshInterval = DateTimeOffset.UtcNow + configManager.AutomaticRefreshInterval; + ManualResetEvent docWaitEvent = new ManualResetEvent(false); + ManualResetEvent docSignalEvent = new ManualResetEvent(false); + ManualResetEvent mgrWaitEvent = new ManualResetEvent(true); + ManualResetEvent mgrSignalEvent = new ManualResetEvent(false); + ManualResetEvent mgrRefreshWaitEvent = new ManualResetEvent(true); + ManualResetEvent mgrRefreshSignalEvent = new ManualResetEvent(false); - // get the first configuration, internal _syncAfter should be set to a time greater than UtcNow + AutomaticRefreshInterval. - var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); + var configurationManager = new EventControlledConfigurationManger( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + EventControlledInMemoryDocumentRetriever(docWaitEvent, docSignalEvent), + mgrSignalEvent, + mgrWaitEvent, + mgrRefreshSignalEvent, + mgrRefreshWaitEvent); + + // _configuration will not be null, which directs configurationManager.GetConfigurationAsync down the path of updating metadata. + TestUtilities.SetField(configurationManager, "_currentConfiguration", OpenIdConfigData.AADCommonV1Config); + + // RequestRefresh will set _configurationRetrieverState to ConfigurationRetrieverRunning until finished. + // EventControlledInMemoryDocumentRetriever will wait until waitEvent.Set() is called + // which in turn resets _configurationRetrieverState to ConfigurationRetrieverIdle. + Task.Run(() => configurationManager.RequestRefresh()); + + // Waiting on for signal that ensures that DocumentRetriever is getting metadata. + docSignalEvent.WaitOne(); + + // Setting the StartupTime in the past so that metadata will be obtained as AutomaticRefreshInterval will have passed. + // Since _configurationRetrieverState still == ConfigurationRetrieverRunning an automatic refresh should not occur. + configurationManager.StartupTime = DateTimeOffset.UtcNow - TimeSpan.FromDays(1); + Task.Run(() => configurationManager.GetConfigurationAsync(CancellationToken.None)); + + // wait until call graph through ConfigurationManager.GetConfigurationAsync() is finished. + mgrSignalEvent.WaitOne(); + + // DocumentRetriever will now complete and return metadata. + docWaitEvent.Set(); + + // ConfigurationManager.RequestRefresh() will now complete + mgrRefreshSignalEvent.WaitOne(); + + Thread.Sleep(1000); + + // ensure correct number of metadata requests have occurred. + Assert.True(configurationManager._numberOfTimesRequestRefreshRequested == 1, $"NumberOfTimesRequestRefreshWasRequested: '{configurationManager._numberOfTimesRequestRefreshRequested}' != '1'."); + Assert.True(configurationManager._numberOfTimesAutomaticRefreshRequested == 0, $"NumberOfTimesAutomaticRefreshWasRequested: '{configurationManager._numberOfTimesAutomaticRefreshRequested}' != '0'."); + } + + [Fact] + public async Task CheckThatAutomaticRefreshIntervalIsSetCorrectly() + { + // Purpose: Verifies that next AutomaticRefreshTime (_timeInSecondsWhenLastAutomaticRefreshOccurred) is set properly. + CompareContext context = TestUtilities.WriteHeader($"{this}", $"{nameof(CheckThatAutomaticRefreshIntervalIsSetCorrectly)}", false); + + var configurationManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), new FileDocumentRetriever()); + var configuration = await configurationManager.GetConfigurationAsync(CancellationToken.None); - // force a refresh by setting internal field - TestUtilities.SetField(configManager, "_syncAfter", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); - configuration = await configManager.GetConfigurationAsync(CancellationToken.None); // wait 1000ms here because update of config is run as a new task. Thread.Sleep(1000); - // check that _syncAfter is greater than DateTimeOffset.UtcNow + AutomaticRefreshInterval - DateTimeOffset syncAfter = (DateTimeOffset)TestUtilities.GetField(configManager, "_syncAfter"); - if (syncAfter < minimumRefreshInterval) - context.Diffs.Add($"(AutomaticRefreshInterval) syncAfter '{syncAfter}' < DateTimeOffset.UtcNow + configManager.AutomaticRefreshInterval: '{minimumRefreshInterval}'."); + // How long has ConfigurationManager has been running? + double totalSeconds = (DateTimeOffset.UtcNow - configurationManager.StartupTime).TotalSeconds; + + // _timeInSecondsWhenLastAutomaticRefreshOccurred should not be greater than (totalSeconds + _automaticRefreshIntervalInSeconds / 20) + // where we added 5% of a random amount between 0 and _automaticRefreshIntervalInSeconds. + if (configurationManager._timeInSecondsWhenLastRefreshOccurred > (int)(totalSeconds + configurationManager._maxJitter)) + context.Diffs.Add($"_timeInSecondsWhenLastAutomaticRefreshOccurred '{configurationManager._timeInSecondsWhenLastRefreshOccurred}' > " + + $"(int)(totalSeconds + configurationManager._automaticRefreshIntervalInSeconds / 20) '{(int)(totalSeconds + configurationManager._maxJitter)}'," + + $" _automaticRefreshIntervalInSeconds: '{configurationManager._automaticRefreshIntervalInSeconds}'."); + + TestUtilities.AssertFailIfErrors(context); + } + + [Fact] + public void CheckThatRefreshIntervalIsSetCorrectly() + { + // Purpose: Verifies that next RequestRefreshTime (_timeInSecondsWhenLastRequestRefreshOccurred) is set properly. + CompareContext context = TestUtilities.WriteHeader($"{this}", $"{nameof(CheckThatAutomaticRefreshIntervalIsSetCorrectly)}", false); + + var configurationManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), new FileDocumentRetriever()); + configurationManager.RequestRefresh(); - // make same check for RequestRefresh - // force a refresh by setting internal field - TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); - configManager.RequestRefresh(); // wait 1000ms here because update of config is run as a new task. Thread.Sleep(1000); - // check that _syncAfter is greater than DateTimeOffset.UtcNow + AutomaticRefreshInterval - syncAfter = (DateTimeOffset)TestUtilities.GetField(configManager, "_syncAfter"); - if (syncAfter < minimumRefreshInterval) - context.Diffs.Add($"(RequestRefresh) syncAfter '{syncAfter}' < DateTimeOffset.UtcNow + configManager.AutomaticRefreshInterval: '{minimumRefreshInterval}'."); + // How long has ConfigurationManager has been running? + double totalSeconds = (DateTimeOffset.UtcNow - configurationManager.StartupTime).TotalSeconds; + + // _timeInSecondsWhenLastRequestRefreshOccurred should not be greater than totalSeconds + if (configurationManager._timeInSecondsWhenLastRequestRefreshWasRequested > (int)(totalSeconds)) + context.Diffs.Add($"_timeInSecondsWhenLastRequestRefreshOccurred '{configurationManager._timeInSecondsWhenLastRefreshOccurred}' > totalSeconds '{totalSeconds}'."); TestUtilities.AssertFailIfErrors(context); } + [Fact] + public void RequestRefreshBlocked() + { + // Purpose: Verifies that RequestRefresh does not issue a metadata request when an AutomaticRefresh is in progress. + CompareContext context = TestUtilities.WriteHeader($"{this}", $"{nameof(RequestRefreshBlocked)}", false); + + ManualResetEvent docWaitEvent = new ManualResetEvent(false); + ManualResetEvent docSignalEvent = new ManualResetEvent(false); + ManualResetEvent mgrWaitEvent = new ManualResetEvent(true); + ManualResetEvent mgrSignalEvent = new ManualResetEvent(false); + ManualResetEvent mgrRefreshWaitEvent = new ManualResetEvent(true); + ManualResetEvent mgrRefreshSignalEvent = new ManualResetEvent(false); + + var configurationManager = new EventControlledConfigurationManger( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + EventControlledInMemoryDocumentRetriever(docWaitEvent, docSignalEvent), + mgrSignalEvent, + mgrWaitEvent, + mgrRefreshSignalEvent, + mgrRefreshWaitEvent); + + // _configuration will not be null, which directs configurationManager.GetConfigurationAsync down the path of updating metadata. + configurationManager.StartupTime = DateTimeOffset.UtcNow - TimeSpan.FromDays(1); + TestUtilities.SetField(configurationManager, "_currentConfiguration", OpenIdConfigData.AADCommonV1Config); + + // GetConfigurationAsync will set _configurationRetrieverState to ConfigurationRetrieverRunning until finished. + // EventControlledInMemoryDocumentRetriever will WaitOne until waitEvent.Set() is called, which returns control to ConfigurationManger which + // in turn resets _configurationRetrieverState to ConfigurationRetrieverIdle. + Task.Run(() => configurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false)); + // wait for signal that we are blocking + docSignalEvent.WaitOne(); + + // RequestRefresh, should NOT request a metadata refresh even though this is the first RequestRefresh(). + Task.Run(() => configurationManager.RequestRefresh()); + + // configurationManager.GetConfigurationAsync() will be able to finish and reset + docWaitEvent.Set(); + mgrRefreshSignalEvent.WaitOne(); + mgrRefreshWaitEvent.Set(); + + // ensure correct number of metadata requests have occurred. + Assert.True(configurationManager._numberOfTimesRequestRefreshRequested == 0, $"NumberOfTimesRequestRefreshWasRequested: '{configurationManager._numberOfTimesRequestRefreshRequested}' != '0'."); + Assert.True(configurationManager._numberOfTimesAutomaticRefreshRequested == 1, $"NumberOfTimesAutomaticRefreshWasRequested: '{configurationManager._numberOfTimesAutomaticRefreshRequested}' != '1'."); + } + + [Fact] + public async Task VerifyInterlockGuardForRequestRefresh() + { + // Purpose: Verifies that ConfigurationManager.RequestRefresh is properly guarded. + // Two RequestRefresh calls should not be allowed to run concurrently. + ManualResetEvent waitEvent = new ManualResetEvent(false); + ManualResetEvent signalEvent = new ManualResetEvent(false); + + var configurationManager = new ConfigurationManager( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + EventControlledInMemoryDocumentRetriever(waitEvent, signalEvent)); + + // populate the configurationManager with AADCommonV1Config + TestUtilities.SetField(configurationManager, "_currentConfiguration", OpenIdConfigData.AADCommonV1Config); + + // EventControlledInMemoryDocumentRetriever will block until waitEvent.Set() is called. + // The first RequestRefresh will not have finished before the next RequestRefresh() is called. + configurationManager.RequestRefresh(); + + // EventControlledInMemoryDocumentRetriever will signal when control is inside GetDocumentAsync + signalEvent.WaitOne(); + + // Change metadata address, AADCommonV1Json would have been passed to the the previous retriever, which is blocked on an event. + configurationManager.MetadataAddress = "AADCommonV2Json"; + + // Setting _isFirstRefreshRequest to true allows us to test our Interlock logic will block the RequestRefresh. + TestUtilities.SetField(configurationManager, "_isFirstRefreshRequest", true); + configurationManager.RequestRefresh(); + + // Set the event to release the lock and let the first RequestRefresh to finish. + waitEvent.Set(); + + // Configuration should be AADCommonV1Config + var configuration = await configurationManager.GetConfigurationAsync(); + Assert.True(configuration.Issuer.Equals(OpenIdConfigData.AADCommonV1Config.Issuer), + $"configuration.Issuer from configurationManager was not as expected," + + $"configuration.Issuer: '{configuration.Issuer}' != expected '{OpenIdConfigData.AADCommonV1Config.Issuer}'."); + + // ensure correct number of metadata requests have occurred. + Assert.True(configurationManager._numberOfTimesRequestRefreshRequested == 1, $"NumberOfTimesRequestRefreshWasRequested: '{configurationManager._numberOfTimesRequestRefreshRequested}' != '1'."); + Assert.True(configurationManager._numberOfTimesAutomaticRefreshRequested == 0, $"NumberOfTimesAutomaticRefreshWasRequested: '{configurationManager._numberOfTimesAutomaticRefreshRequested}' != '0'."); + } + + [Fact] + public async Task VerifyInterlockGuardForGetConfigurationAsync() + { + // Purpose: Verifies that ConfigurationManager.GetConfigurationAsync is properly guarded. + // If a refresh is in progress, a new refresh should not be started. + CompareContext context = TestUtilities.WriteHeader($"{this}", $"{nameof(VerifyInterlockGuardForGetConfigurationAsync)}", false); + + ManualResetEvent docSignalEvent = new ManualResetEvent(false); + ManualResetEvent docWaitEvent = new ManualResetEvent(false); + ManualResetEvent mgrSignalEvent = new ManualResetEvent(false); + ManualResetEvent mgrWaitEvent = new ManualResetEvent(true); + + var configurationManager = new EventControlledConfigurationManger( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + EventControlledInMemoryDocumentRetriever(docWaitEvent, docSignalEvent), + mgrSignalEvent, + mgrWaitEvent); + + // populate the configurationManager with AADCommonV1Config + TestUtilities.SetField(configurationManager, "_currentConfiguration", OpenIdConfigData.AADCommonV1Config); + configurationManager.StartupTime = DateTimeOffset.UtcNow - TimeSpan.FromDays(1); + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task.Run(() => configurationManager.GetConfigurationAsync(CancellationToken.None)); + // wait until the document retriever is in GetDocumentAsync + docSignalEvent.WaitOne(); + + // Set the StartupTime in the past so that metadata will be obtained as AutomaticRefreshInterval will have passed. + // Interlocked guard should not allow a refresh as one is in progress. + // Configuration should be AADCommonV1Config + configurationManager.StartupTime = DateTimeOffset.UtcNow - TimeSpan.FromDays(1); + configurationManager.MetadataAddress = "AADCommonV2Json"; + var configuration = await configurationManager.GetConfigurationAsync(CancellationToken.None); + + // EventControlledInMemoryDocumentRetriever waits until docWaitEvent.Set(). + // The GetConfigurationAsync to update config will not have finished before the next GetConfigurationAsync() is called. + // unblock the document retriever. + docWaitEvent.Set(); + + Assert.True(configuration.Issuer.Equals(OpenIdConfigData.AADCommonV1Config.Issuer), + $"configuration.Issuer from configurationManager was not as expected," + + $" configuration.Issuer: '{configuration.Issuer}' != expected: '{OpenIdConfigData.AADCommonV1Config.Issuer}'."); + + // ensure correct number of metadata requests have occurred. + Assert.True(configurationManager._numberOfTimesRequestRefreshRequested == 0, $"NumberOfTimesRequestRefreshWasRequested: '{configurationManager._numberOfTimesRequestRefreshRequested}' != '0'."); + Assert.True(configurationManager._numberOfTimesAutomaticRefreshRequested == 1, $"NumberOfTimesAutomaticRefreshWasRequested: '{configurationManager._numberOfTimesAutomaticRefreshRequested}' != '1'."); + +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + } + + [Fact] + public void VerifySlimLockForGetConfigurationAsync() + { + CompareContext context = TestUtilities.WriteHeader($"{this}", $"{nameof(VerifySlimLockForGetConfigurationAsync)}", false); + + ManualResetEvent docSignalEvent = new ManualResetEvent(false); + ManualResetEvent docWaitEvent = new ManualResetEvent(false); + ManualResetEvent mgrSignalEvent = new ManualResetEvent(false); + ManualResetEvent mgrWaitEvent = new ManualResetEvent(true); + + InMemoryDocumentRetriever inMemoryDocumentRetriever = EventControlledInMemoryDocumentRetriever(docWaitEvent, docSignalEvent); + var configurationManager = new EventControlledConfigurationManger( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + inMemoryDocumentRetriever, + mgrSignalEvent, + mgrWaitEvent); + + Task.Run(() => configurationManager.GetConfigurationAsync(CancellationToken.None)); + // wait until the document retriever is in GetDocumentAsync + docSignalEvent.WaitOne(); + + // this call will block on SlimLock because the previous call is still running. + Task.Run(() => configurationManager.GetConfigurationAsync(CancellationToken.None)); + + // EventControlledInMemoryDocumentRetriever will block until docWaitEvent.Set() is called. + docWaitEvent.Set(); + + // EventControlledConfigurationManger will block until mgrWaitEvent.Set() is called. + mgrWaitEvent.Set(); + + // wait until manager is done. + mgrSignalEvent.WaitOne(); + + // ensure correct number of metadata requests have occurred. + Assert.True(configurationManager._numberOfTimesRequestRefreshRequested == 0, $"NumberOfTimesRequestRefreshWasRequested: '{configurationManager._numberOfTimesRequestRefreshRequested}' != '0'."); + Assert.True(configurationManager._numberOfTimesAutomaticRefreshRequested == 1, $"NumberOfTimesAutomaticRefreshWasRequested: '{configurationManager._numberOfTimesAutomaticRefreshRequested}' != '1'."); + } + + [Fact] + public async Task CancelWaitingOnSlimLockAsync() + { + // Purpose: Ensure that cancelling either an 'await' or Task() getting metadata does not break any locking. + // Even though we are using Signals, we do not have signals inside configurationManager when locks are released + // therefor Thread.Sleep(1000) is still needed. + CompareContext context = TestUtilities.WriteHeader($"{this}", $"{nameof(VerifySlimLockForGetConfigurationAsync)}", false); + + ManualResetEvent docSignalEvent = new ManualResetEvent(false); + ManualResetEvent docWaitEvent = new ManualResetEvent(false); + ManualResetEvent mgrSignalEvent = new ManualResetEvent(false); + ManualResetEvent mgrWaitEvent = new ManualResetEvent(true); + ManualResetEvent mgrRefreshWaitEvent = new ManualResetEvent(true); + ManualResetEvent mgrRefreshSignalEvent = new ManualResetEvent(true); + + InMemoryDocumentRetriever inMemoryDocumentRetriever = EventControlledInMemoryDocumentRetriever(docWaitEvent, docSignalEvent); + var configurationManager = new EventControlledConfigurationManger( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + inMemoryDocumentRetriever, + mgrSignalEvent, + mgrWaitEvent, + mgrRefreshSignalEvent, + mgrRefreshWaitEvent); + + +#pragma warning disable CS4014 + // This task will hold the SlimLock until docWaitEvent.Set() is called. + Task.Run(() => configurationManager.GetConfigurationAsync(CancellationToken.None)); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + // wait until the document retriever is in GetDocumentAsync + docSignalEvent.WaitOne(); + + // Call GetConfigurationAsync() with a cancelled CancellationToken. + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + CancellationToken cancellationToken = cancellationTokenSource.Token; + cancellationTokenSource.Cancel(); + bool caughtException = false; + try + { + // this call will block on SlimLock because the previous call is still running. + await configurationManager.GetConfigurationAsync(cancellationToken); + } + catch (Exception ex) + { + caughtException = true; + if (ex.GetType() != typeof(OperationCanceledException)) + context.Diffs.Add($"ex.GetType(): '{ex.GetType()}' != typeof(OperationCanceledException)."); + } + + if (!caughtException) + context.Diffs.Add("Expected OperationCanceledException to be thrown."); + + // Run a Task with GetConfigurationAsync(), then cancel. + cancellationTokenSource = new CancellationTokenSource(); + cancellationToken = cancellationTokenSource.Token; + caughtException = false; + try + { +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task task = Task.Run(() => configurationManager.GetConfigurationAsync(cancellationToken)); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + cancellationTokenSource.Cancel(); + await task.WaitAsync(TimeSpan.MaxValue, TimeProvider.System); + } + catch (Exception ex) + { + caughtException = true; + if (ex.GetType() != typeof(TaskCanceledException)) + context.Diffs.Add($"ex.GetType(): '{ex.GetType()}' != typeof(TaskCanceledException)."); + } + + if (!caughtException) + context.Diffs.Add("Expected TaskCanceledException to be thrown."); + + // set _configuration so that the next call to GetConfigurationAsync will not block on the SemaphoreSlim. + // set StartupTime in the past so that metadata will be obtained as AutomaticRefreshInterval will have passed. + // the Interlock will block an update. + TestUtilities.SetField(configurationManager, "_currentConfiguration", OpenIdConfigData.AADCommonV1Config); + configurationManager.StartupTime = DateTimeOffset.UtcNow - TimeSpan.FromDays(1); + await configurationManager.GetConfigurationAsync(CancellationToken.None); + + // release the document retriever, state moves to Idle, interlock should allow the next call to finish. + docWaitEvent.Set(); + Thread.Sleep(1000); + + // set _configuration so that the next call to GetConfigurationAsync will not block on the SemaphoreSlim. + // set StartupTime in the past so that metadata will be obtained as AutomaticRefreshInterval will have passed. + // Another call to GetConfigurationAsync should report another request was made. + mgrSignalEvent.Reset(); + configurationManager.StartupTime = DateTimeOffset.UtcNow - TimeSpan.FromDays(2); + await configurationManager.GetConfigurationAsync(CancellationToken.None); + mgrSignalEvent.WaitOne(); + Thread.Sleep(1000); + + // RequestRefresh() should report a request was made. + mgrRefreshSignalEvent.Reset(); + configurationManager.RequestRefresh(); + mgrRefreshSignalEvent.WaitOne(); + Thread.Sleep(1000); + + // ensure correct number of metadata requests have occurred. + Assert.True(configurationManager._numberOfTimesRequestRefreshRequested == 1, $"NumberOfTimesRequestRefreshWasRequested: '{configurationManager._numberOfTimesRequestRefreshRequested}' != '1'."); + Assert.True(configurationManager._numberOfTimesAutomaticRefreshRequested == 2, $"NumberOfTimesAutomaticRefreshWasRequested: '{configurationManager._numberOfTimesAutomaticRefreshRequested}' != '2'."); + } + #endregion + [Fact] public async Task GetConfigurationAsync() { @@ -657,10 +843,9 @@ public async Task GetConfigurationAsync() configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); - TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); - configManager.RequestRefresh(); configManager.MetadataAddress = "http://127.0.0.1"; var configuration2 = await configManager.GetConfigurationAsync(CancellationToken.None); + Thread.Sleep(1000); IdentityComparer.AreEqual(configuration, configuration2, context); if (!object.ReferenceEquals(configuration, configuration2)) context.Diffs.Add("!object.ReferenceEquals(configuration, configuration2)"); @@ -751,22 +936,39 @@ public void TestConfigurationComparer() TestUtilities.AssertFailIfErrors(context); } + [Fact] + public void TestMaxJitter() + { + var configurationManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), new FileDocumentRetriever()); + Assert.Equal(configurationManager._automaticRefreshIntervalInSeconds / 20, configurationManager._maxJitter); + configurationManager.AutomaticRefreshInterval = TimeSpan.FromMinutes(5); + Assert.Equal(configurationManager._automaticRefreshIntervalInSeconds / 20, configurationManager._maxJitter); + configurationManager.AutomaticRefreshInterval = TimeSpan.MaxValue; + Assert.Equal(configurationManager._automaticRefreshIntervalInSeconds / 20, configurationManager._maxJitter); + } + [Theory, MemberData(nameof(ValidateOpenIdConnectConfigurationTestCases), DisableDiscoveryEnumeration = true)] public async Task ValidateOpenIdConnectConfigurationTests(ConfigurationManagerTheoryData theoryData) { - TestUtilities.WriteHeader($"{this}.ValidateOpenIdConnectConfigurationTests"); - var context = new CompareContext(); + var context = TestUtilities.WriteHeader($"{this}.ValidateOpenIdConnectConfigurationTests", theoryData); OpenIdConnectConfiguration configuration; - var configurationManager = new ConfigurationManager(theoryData.MetadataAddress, theoryData.ConfigurationRetreiver, theoryData.DocumentRetriever, theoryData.ConfigurationValidator); + var configurationManager = new ConfigurationManager( + theoryData.MetadataAddress, + theoryData.ConfigurationRetriever, + theoryData.DocumentRetriever, + theoryData.ConfigurationValidator); if (theoryData.PresetCurrentConfiguration) + { + configurationManager.StartupTime = DateTimeOffset.UtcNow - TimeSpan.FromDays(1); TestUtilities.SetField(configurationManager, "_currentConfiguration", new OpenIdConnectConfiguration() { Issuer = Default.Issuer }); + } try { //create a listener and enable it for logs var listener = TestUtils.SampleListener.CreateLoggerListener(EventLevel.Warning); - configuration = await configurationManager.GetConfigurationAsync(); + configuration = await configurationManager.GetConfigurationAsync(CancellationToken.None); // we need to sleep here to make sure the task that updates configuration has finished. Thread.Sleep(250); @@ -791,14 +993,14 @@ public static TheoryData>(); theoryData.Add(new ConfigurationManagerTheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), - ConfigurationValidator = openIdConnectConfigurationValidator, + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), + ConfigurationValidator = defaultConfigValidator, DocumentRetriever = new FileDocumentRetriever(), First = true, MetadataAddress = "OpenIdConnectMetadata.json", @@ -807,8 +1009,8 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), - ConfigurationValidator = openIdConnectConfigurationValidator2, + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), + ConfigurationValidator = ThreeKeysConfigValidator, DocumentRetriever = new FileDocumentRetriever(), ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX21818:", typeof(InvalidConfigurationException)), MetadataAddress = "OpenIdConnectMetadata.json", @@ -817,8 +1019,8 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), - ConfigurationValidator = openIdConnectConfigurationValidator2, + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), + ConfigurationValidator = ThreeKeysConfigValidator, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, ExpectedErrorMessage = "IDX21818: ", @@ -828,8 +1030,8 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), - ConfigurationValidator = openIdConnectConfigurationValidator2, + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), + ConfigurationValidator = ThreeKeysConfigValidator, DocumentRetriever = new FileDocumentRetriever(), ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX10810:", typeof(InvalidConfigurationException)), MetadataAddress = "OpenIdConnectMetadataUnrecognizedKty.json", @@ -838,8 +1040,8 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), - ConfigurationValidator = openIdConnectConfigurationValidator2, + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), + ConfigurationValidator = ThreeKeysConfigValidator, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, ExpectedErrorMessage = "IDX10810: ", @@ -849,8 +1051,8 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), - ConfigurationValidator = openIdConnectConfigurationValidator2, + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), + ConfigurationValidator = ThreeKeysConfigValidator, DocumentRetriever = new FileDocumentRetriever(), ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX21817:", typeof(InvalidConfigurationException)), MetadataAddress = "JsonWebKeySetUnrecognizedKty.json", @@ -859,8 +1061,8 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), - ConfigurationValidator = openIdConnectConfigurationValidator2, + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), + ConfigurationValidator = ThreeKeysConfigValidator, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, ExpectedErrorMessage = "IDX21817: ", @@ -870,8 +1072,8 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), - ConfigurationValidator = openIdConnectConfigurationValidator2, + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), + ConfigurationValidator = ThreeKeysConfigValidator, DocumentRetriever = new FileDocumentRetriever(), ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX10814:", typeof(InvalidConfigurationException)), MetadataAddress = "OpenIdConnectMetadataBadRsaDataMissingComponent.json", @@ -880,8 +1082,8 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), - ConfigurationValidator = openIdConnectConfigurationValidator2, + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), + ConfigurationValidator = ThreeKeysConfigValidator, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, ExpectedErrorMessage = "IDX10814: ", @@ -902,7 +1104,7 @@ public static TheoryData @@ -926,7 +1128,7 @@ public ConfigurationManagerTheoryData(string testId) : base(testId) { } public TimeSpan AutomaticRefreshInterval { get; set; } - public IConfigurationRetriever ConfigurationRetreiver { get; set; } + public IConfigurationRetriever ConfigurationRetriever { get; set; } public IConfigurationValidator ConfigurationValidator { get; set; } @@ -948,7 +1150,7 @@ public ConfigurationManagerTheoryData(string testId) : base(testId) { } public TimeSpan RefreshInterval { get; set; } = BaseConfigurationManager.DefaultRefreshInterval; - public bool RequestRefresh { get; set; } + public bool RequestFirstRefresh { get; set; } public int SleepTimeInMs { get; set; } = 0; diff --git a/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs b/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs index c37f5d04ce..ccd63a3daa 100644 --- a/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs @@ -81,11 +81,11 @@ public async Task ConfigurationManagerUsingCustomClass() configManager = new ConfigurationManager("IssuerMetadata.json", new IssuerConfigurationRetriever(), docRetriever); configManager.RequestRefresh(); configuration = await configManager.GetConfigurationAsync(); - TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); - configManager.MetadataAddress = "IssuerMetadata2.json"; - // Wait for the refresh to complete. - await Task.Delay(500); + await Task.Delay(1000); + + configManager.StartupTime = DateTimeOffset.UtcNow - TimeSpan.FromDays(2); + configManager.MetadataAddress = "IssuerMetadata2.json"; for (int i = 0; i < 5; i++) { diff --git a/test/Microsoft.IdentityModel.TestUtils/EventDrivenConfigurationManager.cs b/test/Microsoft.IdentityModel.TestUtils/EventDrivenConfigurationManager.cs new file mode 100644 index 0000000000..aed68f5b54 --- /dev/null +++ b/test/Microsoft.IdentityModel.TestUtils/EventDrivenConfigurationManager.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.IdentityModel.TestUtils +{ + public class EventDrivenConfigurationRetriever : IConfigurationRetriever + { + private ManualResetEvent _signalEvent; + private ManualResetEvent _waitEvent; + private T _configuration; + + /// + /// Initializes an new instance of with a configuration instance. + /// + /// The Configuration that will be returned + /// A that is be signaled when inside GetConfigurationAsync. + /// A that waits inside GetConfigurationAsync. + public EventDrivenConfigurationRetriever( + T configuration, + ManualResetEvent signalEvent, + ManualResetEvent waitEvent) + { + _configuration = configuration; + _signalEvent = signalEvent; + _waitEvent = waitEvent; + } + + public Task GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel) + { + _waitEvent.WaitOne(); + _signalEvent.Set(); + return Task.FromResult(_configuration); + } + } + + /// + /// This type is used for testing the functionality of using a last known good configuration, as well + /// as a refreshed configuration. + /// + /// must be a class inherit from . + public class EventControlledConfigurationManger : ConfigurationManager, IConfigurationManager where T : class + { + private ManualResetEvent _configSignalEvent; + private ManualResetEvent _configWaitEvent; + private ManualResetEvent _refreshSignalEvent; + private ManualResetEvent _refreshWaitEvent; + + /// + /// Initializes an new instance of with a Configuration instance. + /// + /// The metadata address to obtain configuration. + /// The that reads the metadata. + /// The that obtains the metadata. + /// A that is signaled when GetConfigurationAsync is exiting. + /// A that waits in GetConfigurationAsync after calling base.GetConfigurationAsync. + /// A that is signaled when RequestRefresh is exiting. + /// A that waits after base.RequestRefresh is called. + public EventControlledConfigurationManger( + string metadataAddress, + IConfigurationRetriever configurationRetriever, + IDocumentRetriever documentRetriever, + ManualResetEvent configSignalEvent, + ManualResetEvent configWaitEvent, + ManualResetEvent refreshSignalEvent = null, + ManualResetEvent refreshWaitEvent = null) : base(metadataAddress, configurationRetriever, documentRetriever) + { + _configSignalEvent = configSignalEvent; + _configWaitEvent = configWaitEvent; + _refreshWaitEvent = refreshWaitEvent; + _refreshSignalEvent = refreshSignalEvent; + } + + /// + /// Obtains an updated version of Configuration. + /// + /// . + /// Configuration of type T. + public override Task GetConfigurationAsync(CancellationToken cancel) + { + try + { + Task t = base.GetConfigurationAsync(cancel); + _configWaitEvent.WaitOne(); + return t; + } + finally + { + _configSignalEvent.Set(); + } + } + + /// + /// Unless _refreshedConfiguration is set, this is a no-op. + /// + public override void RequestRefresh() + { + try + { + base.RequestRefresh(); + if (_refreshWaitEvent != null) + _refreshWaitEvent.WaitOne(); + } + finally + { + if (_refreshSignalEvent != null) + _refreshSignalEvent.Set(); + } + } + } +} + diff --git a/test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs b/test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs index 1c88ba93cd..22dbc09210 100644 --- a/test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs +++ b/test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs @@ -41,13 +41,12 @@ public InMemoryDocumentRetriever(Dictionary configuration, Manua /// UTF8 decoding of bytes in the file. public async Task GetDocumentAsync(string address, CancellationToken cancel) { - // Some tests change the Metadata address on ConfigurationManger to test different scenarios. - // This event is used to let the test know that the GetDocumentAsync method has been called, and the test can now change the Metadata address. + // Signal the we are inside GetDocumentAsync => ConfigurationManager.GetConfigurationAsync OR RequestRefresh is waiting for + // this method to return if (_signalEvent != null) _signalEvent.Set(); - // This event lets the caller control when metadata can be returned. - // Useful when testing delays. + // Wait here until caller wants us to return if (_waitEvent != null) _waitEvent.WaitOne(); diff --git a/test/Microsoft.IdentityModel.TestUtils/TestUtilities.cs b/test/Microsoft.IdentityModel.TestUtils/TestUtilities.cs index 55d5b56cfa..f3f0742c9b 100644 --- a/test/Microsoft.IdentityModel.TestUtils/TestUtilities.cs +++ b/test/Microsoft.IdentityModel.TestUtils/TestUtilities.cs @@ -75,9 +75,24 @@ public static void CallAllPublicInstanceAndStaticPropertyGets(object obj, string /// public static object GetField(object obj, string field) { - Type type = obj.GetType(); - FieldInfo fieldInfo = type.GetField(field, BindingFlags.NonPublic | BindingFlags.Instance); - return fieldInfo.GetValue(obj); + Type t = obj.GetType(); + FieldInfo fi = null; + while (t != null) + { + fi = t.GetField(field, BindingFlags.Instance | BindingFlags.NonPublic); + + if (fi != null) + break; + + t = t.BaseType; + } + + if (fi == null) + throw new Exception(string.Format("Field '{0}' not found in type hierarchy.", field)); + + object retval = fi.GetValue(obj); + + return retval; } /// @@ -85,9 +100,22 @@ public static object GetField(object obj, string field) /// public static void SetField(object obj, string field, object fieldValue) { - Type type = obj.GetType(); - FieldInfo fieldInfo = type.GetField(field, BindingFlags.NonPublic | BindingFlags.Instance); - fieldInfo.SetValue(obj, fieldValue); + Type t = obj.GetType(); + FieldInfo fi = null; + while (t != null) + { + fi = t.GetField(field, BindingFlags.Instance | BindingFlags.NonPublic); + + if (fi != null) + break; + + t = t.BaseType; + } + + if (fi == null) + throw new Exception(string.Format("Field '{0}' not found in type hierarchy.", field)); + + fi.SetValue(obj, fieldValue); } ///