diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs index b5ab88c555..ae5b756c5c 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs @@ -37,11 +37,20 @@ public ConfigurationValidationResult Validate(OpenIdConnectConfiguration openIdC Succeeded = false }; } - var numberOfValidKeys = openIdConnectConfiguration.JsonWebKeySet.Keys.Where(key => key.ConvertedSecurityKey != null).Count(); + + int numberOfValidKeys = 0; + for (int i = 0; i < openIdConnectConfiguration.JsonWebKeySet.Keys.Count; i++) + if (openIdConnectConfiguration.JsonWebKeySet.Keys[i].ConvertedSecurityKey != null) + numberOfValidKeys++; if (numberOfValidKeys < MinimumNumberOfKeys) { - var convertKeyInfos = string.Join("\n", openIdConnectConfiguration.JsonWebKeySet.Keys.Where(key => !string.IsNullOrEmpty(key.ConvertKeyInfo)).Select(key => key.Kid.ToString() + ": " + key.ConvertKeyInfo)); + string convertKeyInfos = string.Join( + "\n", + openIdConnectConfiguration.JsonWebKeySet.Keys.Where( + key => !string.IsNullOrEmpty(key.ConvertKeyInfo)) + .Select(key => key.Kid.ToString() + ": " + key.ConvertKeyInfo)); + return new ConfigurationValidationResult { ErrorMessage = LogHelper.FormatInvariant( diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs index c923422ab1..3ab4c16aef 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -19,17 +19,23 @@ namespace Microsoft.IdentityModel.Protocols public class ConfigurationManager : BaseConfigurationManager, IConfigurationManager where T : class { private DateTimeOffset _syncAfter = DateTimeOffset.MinValue; - private DateTimeOffset _lastRefresh = DateTimeOffset.MinValue; + private DateTimeOffset _lastRequestRefresh = DateTimeOffset.MinValue; private bool _isFirstRefreshRequest = true; + private readonly SemaphoreSlim _configurationNullLock = new SemaphoreSlim(1); - private readonly SemaphoreSlim _refreshLock; private readonly IDocumentRetriever _docRetriever; private readonly IConfigurationRetriever _configRetriever; private readonly IConfigurationValidator _configValidator; private T _currentConfiguration; - private Exception _fetchMetadataFailure; private TimeSpan _bootstrapRefreshInterval = TimeSpan.FromSeconds(1); + // task states are used to ensure the call to 'update config' (UpdateCurrentConfiguration) is a singleton. Uses Interlocked.CompareExchange. + // metadata is not being obtained + private const int ConfigurationRetrieverIdle = 0; + // metadata is being retrieved + private const int ConfigurationRetrieverRunning = 1; + private int _configurationRetrieverState = ConfigurationRetrieverIdle; + /// /// Instantiates a new that manages automatic and controls refreshing on configuration data. /// @@ -91,7 +97,6 @@ public ConfigurationManager(string metadataAddress, IConfigurationRetriever c MetadataAddress = metadataAddress; _docRetriever = docRetriever; _configRetriever = configRetriever; - _refreshLock = new SemaphoreSlim(1); } /// @@ -144,83 +149,174 @@ public async Task GetConfigurationAsync() public virtual async Task GetConfigurationAsync(CancellationToken cancel) { if (_currentConfiguration != null && _syncAfter > DateTimeOffset.UtcNow) - { return _currentConfiguration; - } - await _refreshLock.WaitAsync(cancel).ConfigureAwait(false); - try + Exception fetchMetadataFailure = null; + + // LOGIC + // if configuration == null => configuration has never been retrieved. + // 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 + // else kick off task to update current configuration + if (_currentConfiguration == null) { - if (_syncAfter <= DateTimeOffset.UtcNow) + await _configurationNullLock.WaitAsync(cancel).ConfigureAwait(false); + if (_currentConfiguration != null) { - 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) - { - ConfigurationValidationResult result = _configValidator.Validate(configuration); - if (!result.Succeeded) - throw LogHelper.LogExceptionMessage(new InvalidConfigurationException(LogHelper.FormatInvariant(LogMessages.IDX20810, result.ErrorMessage))); - } + _configurationNullLock.Release(); + return _currentConfiguration; + } - _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; - } - catch (Exception ex) + 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. + T configuration = await _configRetriever.GetConfigurationAsync( + MetadataAddress, + _docRetriever, + CancellationToken.None).ConfigureAwait(false); + + if (_configValidator != null) { - _fetchMetadataFailure = ex; + ConfigurationValidationResult result = _configValidator.Validate(configuration); + // in this case we have never had a valid configuration, so we will throw an exception if the validation fails + if (!result.Succeeded) + throw LogHelper.LogExceptionMessage( + new InvalidConfigurationException( + LogHelper.FormatInvariant( + LogMessages.IDX20810, + result.ErrorMessage))); + } - if (_currentConfiguration == null) // Throw an exception if there's no configuration to return. - { - if (_bootstrapRefreshInterval < RefreshInterval) - { - // Adopt exponential backoff for bootstrap refresh interval with a decorrelated jitter if it is not longer than the refresh interval. - TimeSpan _bootstrapRefreshIntervalWithJitter = TimeSpan.FromSeconds(new Random().Next((int)_bootstrapRefreshInterval.TotalSeconds)); - _bootstrapRefreshInterval += _bootstrapRefreshInterval; - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, _bootstrapRefreshIntervalWithJitter); - } - else - { - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); - } + // 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))); - throw LogHelper.LogExceptionMessage( - new InvalidOperationException( - LogHelper.FormatInvariant(LogMessages.IDX20803, LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), LogHelper.MarkAsNonPII(_syncAfter), LogHelper.MarkAsNonPII(ex)), ex)); + _currentConfiguration = configuration; + } + catch (Exception ex) + { + fetchMetadataFailure = ex; + + // In this case configuration was never obtained. + if (_currentConfiguration == null) + { + if (_bootstrapRefreshInterval < RefreshInterval) + { + // Adopt exponential backoff for bootstrap refresh interval with a decorrelated jitter if it is not longer than the refresh interval. + TimeSpan _bootstrapRefreshIntervalWithJitter = TimeSpan.FromSeconds(new Random().Next((int)_bootstrapRefreshInterval.TotalSeconds)); + _bootstrapRefreshInterval += _bootstrapRefreshInterval; + _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, _bootstrapRefreshIntervalWithJitter); } else { - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); - - LogHelper.LogExceptionMessage( - new InvalidOperationException( - LogHelper.FormatInvariant(LogMessages.IDX20806, LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), LogHelper.MarkAsNonPII(ex)), ex)); + _syncAfter = DateTimeUtil.Add( + DateTime.UtcNow, + AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); } + + throw LogHelper.LogExceptionMessage( + new InvalidOperationException( + LogHelper.FormatInvariant( + LogMessages.IDX20803, + LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), + LogHelper.MarkAsNonPII(_syncAfter), + LogHelper.MarkAsNonPII(ex)), + ex)); } + else + { + _syncAfter = DateTimeUtil.Add( + DateTime.UtcNow, + AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); + + LogHelper.LogExceptionMessage( + new InvalidOperationException( + LogHelper.FormatInvariant( + LogMessages.IDX20806, + LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), + LogHelper.MarkAsNonPII(ex)), + ex)); + } + } + finally + { + _configurationNullLock.Release(); } + } + else + { + if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverIdle, ConfigurationRetrieverRunning) != ConfigurationRetrieverRunning) + { + _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); + } + } - // Stale metadata is better than no metadata - if (_currentConfiguration != null) - return _currentConfiguration; + // If metadata exists return it. + if (_currentConfiguration != null) + return _currentConfiguration; + + throw LogHelper.LogExceptionMessage( + new InvalidOperationException( + LogHelper.FormatInvariant( + LogMessages.IDX20803, + LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), + LogHelper.MarkAsNonPII(_syncAfter), + LogHelper.MarkAsNonPII(fetchMetadataFailure)), + fetchMetadataFailure)); + } + + /// + /// 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). + /// + private void UpdateCurrentConfiguration() + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + T configuration = _configRetriever.GetConfigurationAsync( + MetadataAddress, + _docRetriever, + CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + + if (_configValidator == null) + { + _currentConfiguration = configuration; + } else - throw LogHelper.LogExceptionMessage( - new InvalidOperationException( - LogHelper.FormatInvariant( - LogMessages.IDX20803, - LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), - LogHelper.MarkAsNonPII(_syncAfter), - LogHelper.MarkAsNonPII(_fetchMetadataFailure)), - _fetchMetadataFailure)); + { + ConfigurationValidationResult result = _configValidator.Validate(configuration); + + if (!result.Succeeded) + LogHelper.LogExceptionMessage( + new InvalidConfigurationException( + LogHelper.FormatInvariant( + LogMessages.IDX20810, + result.ErrorMessage))); + else + _currentConfiguration = configuration; + } + } + catch (Exception ex) + { + LogHelper.LogExceptionMessage( + new InvalidOperationException( + LogHelper.FormatInvariant( + LogMessages.IDX20806, + LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), + ex), + ex)); } finally { - _refreshLock.Release(); + _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); + Interlocked.Exchange(ref _configurationRetrieverState, ConfigurationRetrieverIdle); } +#pragma warning restore CA1031 // Do not catch general exception types } /// @@ -231,10 +327,8 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) /// 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) { - var obj = await GetConfigurationAsync(cancel).ConfigureAwait(false); - if (obj is BaseConfiguration) - return obj as BaseConfiguration; - return null; + T obj = await GetConfigurationAsync(cancel).ConfigureAwait(false); + return obj as BaseConfiguration; } /// @@ -245,14 +339,15 @@ public override async Task GetBaseConfigurationAsync(Cancella public override void RequestRefresh() { DateTimeOffset now = DateTimeOffset.UtcNow; - if (_isFirstRefreshRequest) + + if (now >= DateTimeUtil.Add(_lastRequestRefresh.UtcDateTime, RefreshInterval) || _isFirstRefreshRequest) { - _syncAfter = now; _isFirstRefreshRequest = false; - } - else if (now >= DateTimeUtil.Add(_lastRefresh.UtcDateTime, RefreshInterval)) - { - _syncAfter = now; + _lastRequestRefresh = now; + if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverIdle, ConfigurationRetrieverRunning) != ConfigurationRetrieverRunning) + { + _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); + } } } diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/HttpDocumentRetriever.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/HttpDocumentRetriever.cs index 9eb4c2291b..c69174c757 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/HttpDocumentRetriever.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/HttpDocumentRetriever.cs @@ -84,17 +84,22 @@ public HttpDocumentRetriever(HttpClient httpClient) public async Task GetDocumentAsync(string address, CancellationToken cancel) { if (string.IsNullOrWhiteSpace(address)) - throw LogHelper.LogArgumentNullException("address"); + throw LogHelper.LogArgumentNullException(nameof(address)); if (!Utility.IsHttps(address) && RequireHttps) - throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX20108, address), nameof(address))); + throw LogHelper.LogExceptionMessage( + new ArgumentException( + LogHelper.FormatInvariant( + LogMessages.IDX20108, + LogHelper.MarkAsNonPII(address)), + nameof(address))); Exception unsuccessfulHttpResponseException; HttpResponseMessage response; try { if (LogHelper.IsEnabled(EventLogLevel.Verbose)) - LogHelper.LogVerbose(LogMessages.IDX20805, address); + LogHelper.LogVerbose(LogMessages.IDX20805, LogHelper.MarkAsNonPII(address)); var httpClient = _httpClient ?? _defaultHttpClient; var uri = new Uri(address, UriKind.RelativeOrAbsolute); @@ -104,13 +109,24 @@ public async Task GetDocumentAsync(string address, CancellationToken can if (response.IsSuccessStatusCode) return responseContent; - unsuccessfulHttpResponseException = new IOException(LogHelper.FormatInvariant(LogMessages.IDX20807, address, response, responseContent)); + unsuccessfulHttpResponseException = new IOException( + LogHelper.FormatInvariant( + LogMessages.IDX20807, + LogHelper.MarkAsNonPII(address), + response, + responseContent)); + unsuccessfulHttpResponseException.Data.Add(StatusCode, response.StatusCode); unsuccessfulHttpResponseException.Data.Add(ResponseContent, responseContent); } catch (Exception ex) { - throw LogHelper.LogExceptionMessage(new IOException(LogHelper.FormatInvariant(LogMessages.IDX20804, address), ex)); + throw LogHelper.LogExceptionMessage( + new IOException( + LogHelper.FormatInvariant( + LogMessages.IDX20804, + LogHelper.MarkAsNonPII(address)), + ex)); } throw LogHelper.LogExceptionMessage(unsuccessfulHttpResponseException); diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs index d103db30ba..3d4cb1ea7f 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs @@ -11,6 +11,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Protocols.Configuration; using Microsoft.IdentityModel.Protocols.OpenIdConnect.Configuration; using Microsoft.IdentityModel.TestUtils; @@ -290,163 +291,245 @@ public void GetSets() TestUtilities.AssertFailIfErrors("ConfigurationManager_GetSets", context.Errors); } - [Fact] - public void GetConfiguration() + [Theory, MemberData(nameof(AutomaticIntervalTestCases), DisableDiscoveryEnumeration = true)] + public async Task AutomaticRefreshInterval(ConfigurationManagerTheoryData theoryData) { - var docRetriever = new FileDocumentRetriever(); - var configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - var context = new CompareContext($"{this}.GetConfiguration"); + var context = new CompareContext($"{this}.AutomaticRefreshInterval"); - // AutomaticRefreshInterval interval should return same config. - var configuration = configManager.GetConfigurationAsync().Result; - configManager.MetadataAddress = "OpenIdConnectMetadata2.json"; - var configuration2 = configManager.GetConfigurationAsync().Result; - IdentityComparer.AreEqual(configuration, configuration2, context); - if (!object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("!object.ReferenceEquals(configuration, configuration2)"); + try + { - // AutomaticRefreshInterval should pick up new bits. - configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - configManager.RequestRefresh(); - configuration = configManager.GetConfigurationAsync().Result; - TestUtilities.SetField(configManager, "_lastRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); - configManager.MetadataAddress = "OpenIdConnectMetadata2.json"; - configManager.RequestRefresh(); - configuration2 = configManager.GetConfigurationAsync().Result; - if (IdentityComparer.AreEqual(configuration, configuration2)) - context.Diffs.Add("IdentityComparer.AreEqual(configuration, configuration2)"); + var configuration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + IdentityComparer.AreEqual(configuration, theoryData.ExpectedConfiguration, context); - if (object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("object.ReferenceEquals(configuration, configuration2) (2)"); + theoryData.ConfigurationManager.MetadataAddress = theoryData.UpdatedMetadataAddress; + TestUtilities.SetField(theoryData.ConfigurationManager, "_syncAfter", theoryData.SyncAfter); + var updatedConfiguration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + // we wait 50 ms here to make the task is finished. + Thread.Sleep(50); + updatedConfiguration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + IdentityComparer.AreEqual(updatedConfiguration, theoryData.ExpectedUpdatedConfiguration, context); - // RefreshInterval is set to MaxValue - configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - configuration = configManager.GetConfigurationAsync().Result; - configManager.RefreshInterval = TimeSpan.MaxValue; - configManager.MetadataAddress = "OpenIdConnectMetadata2.json"; - configuration2 = configManager.GetConfigurationAsync().Result; - IdentityComparer.AreEqual(configuration, configuration2, context); - if (!object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("!object.ReferenceEquals(configuration, configuration2) (3)"); + theoryData.ExpectedException.ProcessNoException(context); + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } - configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - configuration = configManager.GetConfigurationAsync().Result; - // First force refresh should pickup new config - configManager.RequestRefresh(); - configManager.MetadataAddress = "OpenIdConnectMetadata2.json"; - configuration2 = configManager.GetConfigurationAsync().Result; - if (IdentityComparer.AreEqual(configuration, configuration2)) - context.Diffs.Add("IdentityComparer.AreEqual(configuration, configuration2), should be different"); - if (object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("object.ReferenceEquals(configuration, configuration2) (4)"); - // Next force refresh shouldn't pickup new config, as RefreshInterval hasn't passed - configManager.RequestRefresh(); - configManager.MetadataAddress = "OpenIdConnectMetadata.json"; - var configuration3 = configManager.GetConfigurationAsync().Result; - IdentityComparer.AreEqual(configuration2, configuration3, context); - if (!object.ReferenceEquals(configuration2, configuration3)) - context.Diffs.Add("!object.ReferenceEquals(configuration2, configuration3) (5)"); - // Next force refresh should pickup config since, RefreshInterval is set to 1s - configManager.RefreshInterval = TimeSpan.FromSeconds(1); - Thread.Sleep(1000); - configManager.RequestRefresh(); - var configuration4 = configManager.GetConfigurationAsync().Result; - if (IdentityComparer.AreEqual(configuration2, configuration4)) - context.Diffs.Add("IdentityComparer.AreEqual(configuration2, configuration4), should be different"); - if (object.ReferenceEquals(configuration2, configuration4)) - context.Diffs.Add("object.ReferenceEquals(configuration2, configuration4) (6)"); + TestUtilities.AssertFailIfErrors(context); + } - // Refresh should force pickup of new config - configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - configuration = configManager.GetConfigurationAsync().Result; - TestUtilities.SetField(configManager, "_lastRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); - configManager.RequestRefresh(); - configManager.MetadataAddress = "OpenIdConnectMetadata2.json"; - configuration2 = configManager.GetConfigurationAsync().Result; - if (IdentityComparer.AreEqual(configuration, configuration2)) - context.Diffs.Add("IdentityComparer.AreEqual(configuration, configuration2), should be different"); + public static TheoryData> AutomaticIntervalTestCases + { + get + { + var theoryData = new TheoryData>(); - if (object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("object.ReferenceEquals(configuration, configuration2)"); + // 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" + }); - // Refresh set to MaxValue - configManager.RefreshInterval = TimeSpan.MaxValue; - configuration = configManager.GetConfigurationAsync().Result; - IdentityComparer.AreEqual(configuration, configuration2, context); - if (!object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("!object.ReferenceEquals(configuration, configuration2)"); + // 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" + }); - // get configuration from http address, should throw - configManager = new ConfigurationManager("http://127.0.0.1", new OpenIdConnectConfigurationRetriever()); - var ee = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(ArgumentException)); - try + return theoryData; + } + } + + [Theory, MemberData(nameof(RequestRefreshTestCases), DisableDiscoveryEnumeration = true)] + public async Task RequestRefresh(ConfigurationManagerTheoryData theoryData) + { + var context = new CompareContext($"{this}.RequestRefresh"); + + var configuration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + IdentityComparer.AreEqual(configuration, theoryData.ExpectedConfiguration, context); + + // the first call to RequestRefresh will trigger a refresh with ConfigurationManager.RefreshInterval being ignored. + // Testing RefreshInterval requires a two calls, the second call will trigger a refresh with ConfigurationManager.RefreshInterval being used. + if (theoryData.RequestRefresh) { - configuration = configManager.GetConfigurationAsync().Result; - ee.ProcessNoException(context); + theoryData.ConfigurationManager.RequestRefresh(); + configuration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); } - catch (AggregateException ex) + + theoryData.ConfigurationManager.RefreshInterval = theoryData.RefreshInterval; + theoryData.ConfigurationManager.MetadataAddress = theoryData.UpdatedMetadataAddress; + if (theoryData.SleepTimeInMs > 0) + Thread.Sleep(theoryData.SleepTimeInMs); + + theoryData.ConfigurationManager.RequestRefresh(); + + if (theoryData.SleepTimeInMs > 0) + Thread.Sleep(theoryData.SleepTimeInMs); + + var updatedConfiguration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + + IdentityComparer.AreEqual(updatedConfiguration, theoryData.ExpectedUpdatedConfiguration, context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData> RequestRefreshTestCases + { + get { - // this should throw, because last configuration retrived was null - Assert.Throws(() => configuration = configManager.GetConfigurationAsync().Result); + var theoryData = new TheoryData>(); - ex.Handle((x) => + // RefreshInterval set to 1 sec should return new config. + theoryData.Add(new ConfigurationManagerTheoryData("RequestRefresh_TimeSpan_1000ms") { - ee.ProcessException(x, context); - return true; + ConfigurationManager = new ConfigurationManager( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + InMemoryDocumentRetriever), + ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, + ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV2Config, + RefreshInterval = TimeSpan.FromSeconds(1), + RequestRefresh = true, + SleepTimeInMs = 1000, + UpdatedMetadataAddress = "AADCommonV2Json" }); - } - // get configuration from https address, should throw - configManager = new ConfigurationManager("https://127.0.0.1", new OpenIdConnectConfigurationRetriever()); - ee = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(IOException)); - try - { - configuration = configManager.GetConfigurationAsync().Result; - ee.ProcessNoException(context); - } - catch (AggregateException ex) - { - // this should throw, because last configuration retrived was null - Assert.Throws(() => configuration = configManager.GetConfigurationAsync().Result); + // RefreshInterval set to TimeSpan.MaxValue should return same config. + theoryData.Add(new ConfigurationManagerTheoryData("RequestRefresh_TimeSpan_MaxValue") + { + ConfigurationManager = new ConfigurationManager( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + InMemoryDocumentRetriever), + ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, + ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV1Config, + RefreshInterval = TimeSpan.MaxValue, + RequestRefresh = true, + UpdatedMetadataAddress = "AADCommonV2Json" + }); - ex.Handle((x) => + // First RequestRefresh should pickup new config + theoryData.Add(new ConfigurationManagerTheoryData("RequestRefresh_FirstRefresh") { - ee.ProcessException(x, context); - return true; + ConfigurationManager = new ConfigurationManager( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + InMemoryDocumentRetriever), + ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, + ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV2Config, + SleepTimeInMs = 100, + UpdatedMetadataAddress = "AADCommonV2Json" }); + + return theoryData; } + } + + [Theory, MemberData(nameof(HttpFailuresTestCases), DisableDiscoveryEnumeration = true)] + public async Task HttpFailures(ConfigurationManagerTheoryData theoryData) + { + var context = new CompareContext($"{this}.HttpFailures"); - // get configuration with unsuccessful HTTP response status code - configManager = new ConfigurationManager("https://httpstat.us/429", new OpenIdConnectConfigurationRetriever()); - ee = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(IOException)); try { - configuration = configManager.GetConfigurationAsync().Result; - ee.ProcessNoException(context); + _ = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + theoryData.ExpectedException.ProcessNoException(context); } - catch (AggregateException ex) + catch (Exception ex) { - // this should throw, because last configuration retrived was null - Assert.Throws(() => configuration = configManager.GetConfigurationAsync().Result); + theoryData.ExpectedException.ProcessException(ex, context); + } - ex.Handle((x) => + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData> HttpFailuresTestCases + { + get + { + var theoryData = new TheoryData>(); + + theoryData.Add(new ConfigurationManagerTheoryData("LocalHost_HTTPS_Status_Error") { - ee.ProcessException(x, context); - return true; + ConfigurationManager = new ConfigurationManager( + "https://httpstat.us/429", + new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever()), + ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(IOException)), + }); + + theoryData.Add(new ConfigurationManagerTheoryData("LocalHost_HTTPS_Error") + { + ConfigurationManager = new ConfigurationManager( + "https://127.0.0.1", + new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever()), + ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(IOException)), }); + + theoryData.Add(new ConfigurationManagerTheoryData("LocalHost_HTTP_ArgumentError") + { + ConfigurationManager = new ConfigurationManager( + "http://127.0.0.1", + new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever()), + ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(ArgumentException)), + }); + + return theoryData; } + } + + [Fact] + public async Task GetConfigurationAsync() + { + var docRetriever = new FileDocumentRetriever(); + var configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); + var context = new CompareContext($"{this}.GetConfiguration"); // Unable to obtain a new configuration, but _currentConfiguration is not null so it should be returned. configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - configuration = configManager.GetConfigurationAsync().Result; - TestUtilities.SetField(configManager, "_lastRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); + var configuration = await configManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); configManager.RequestRefresh(); configManager.MetadataAddress = "http://127.0.0.1"; - configuration2 = configManager.GetConfigurationAsync().Result; + var configuration2 = await configManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); IdentityComparer.AreEqual(configuration, configuration2, context); if (!object.ReferenceEquals(configuration, configuration2)) context.Diffs.Add("!object.ReferenceEquals(configuration, configuration2)"); + + // get configuration from http address, should throw + // get configuration with unsuccessful HTTP response status code TestUtilities.AssertFailIfErrors(context); } @@ -545,9 +628,11 @@ public void ValidateOpenIdConnectConfigurationTests(ConfigurationManagerTheoryDa { //create a listener and enable it for logs var listener = TestUtils.SampleListener.CreateLoggerListener(EventLevel.Warning); - configuration = configurationManager.GetConfigurationAsync().Result; + // we need to sleep here to make sure the task that updates configuration has finished. + Thread.Sleep(250); + if (!string.IsNullOrEmpty(theoryData.ExpectedErrorMessage) && !listener.TraceBuffer.Contains(theoryData.ExpectedErrorMessage)) context.AddDiff($"Expected exception to contain: '{theoryData.ExpectedErrorMessage}'.{Environment.NewLine}Log is:{Environment.NewLine}'{listener.TraceBuffer}'"); @@ -674,17 +759,28 @@ public static TheoryData : TheoryDataBase + private static InMemoryDocumentRetriever InMemoryDocumentRetriever => new InMemoryDocumentRetriever( + new Dictionary + { + { "AADCommonV1Json", OpenIdConfigData.AADCommonV1Json }, + { "https://login.microsoftonline.com/common/discovery/keys", OpenIdConfigData.AADCommonV1JwksString }, + { "AADCommonV2Json", OpenIdConfigData.AADCommonV2Json }, + { "https://login.microsoftonline.com/common/discovery/v2.0/keys", OpenIdConfigData.AADCommonV2JwksString } + }); + + public class ConfigurationManagerTheoryData : TheoryDataBase where T : class { - public ConfigurationManagerTheoryData() { } + public ConfigurationManager ConfigurationManager { get; set; } - public ConfigurationManagerTheoryData(string testId) : base(testId) { } + public ConfigurationManagerTheoryData() {} + + public ConfigurationManagerTheoryData(string testId) : base(testId) {} public TimeSpan AutomaticRefreshInterval { get; set; } public IConfigurationRetriever ConfigurationRetreiver { get; set; } - public IConfigurationValidator ConfigurationValidator { get; set; } + public IConfigurationValidator ConfigurationValidator { get; set; } public IDocumentRetriever DocumentRetriever { get; set; } @@ -692,18 +788,30 @@ public ConfigurationManagerTheoryData(string testId) : base(testId) { } public string ExpectedErrorMessage { get; set; } + public T ExpectedConfiguration { get; set; } + + public T ExpectedUpdatedConfiguration { get; set; } + + public DateTimeOffset LastRefreshTime { get; set; } = DateTime.MinValue; + public string MetadataAddress { get; set; } public bool PresetCurrentConfiguration { get; set; } - public TimeSpan RefreshInterval { get; set; } + public TimeSpan RefreshInterval { get; set; } = BaseConfigurationManager.DefaultRefreshInterval; public bool RequestRefresh { get; set; } + public int SleepTimeInMs { get; set; } = 0; + + public DateTimeOffset SyncAfter { get; set; } = DateTime.UtcNow; + public override string ToString() { return $"{TestId}, {MetadataAddress}, {ExpectedException}"; } + + public string UpdatedMetadataAddress { get; set; } } } } diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs index 27e80e390e..1e3606cb77 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs @@ -274,6 +274,92 @@ public static OpenIdConnectConfiguration AccountsGoogleComConfig } """; + // 7/22/2024 + public static string AADCommonV1JwksString => + """ + { + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "23ntaxfH9Gk-8D_USzoAJgwjyt8", + "x5t": "23ntaxfH9Gk-8D_USzoAJgwjyt8", + "n": "hu2SJrLlDOUtU2s9T6-6OVGEPaba2zIT2_Jl50f4NGG-r-GyQdaOzTFASfAfMkMfMQMRnabqd-dp_Ooqha473bw6DMbM23nv2uhBn5Afp-S1W_d4NxEhfNlN1Tgjx3Sh6UblBSFCE4JGkugSkLi2SVouy43seskesQotXGVNv4iboFm4yO_twlMCG9EDwza32y6WZtV8i9gkQP42OfK0X1qy6EUz2DN7cpfZtmkNtsFJhFf9waOvNCR95LVCPGafeCOMAQEvu1VO3mrBSIg7Izu0CzvuaBQTwnGv29Ggxc3GO4gvb_OStkkmfIwchu3A8F6e0aJ4Ys8PFP7z7Z8lqQ", + "e": "AQAB", + "x5c": [ + "MIIC/jCCAeagAwIBAgIJAJB4tJM2GkZjMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwNTIyMTg1ODQwWhcNMjkwNTIyMTg1ODQwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhu2SJrLlDOUtU2s9T6+6OVGEPaba2zIT2/Jl50f4NGG+r+GyQdaOzTFASfAfMkMfMQMRnabqd+dp/Ooqha473bw6DMbM23nv2uhBn5Afp+S1W/d4NxEhfNlN1Tgjx3Sh6UblBSFCE4JGkugSkLi2SVouy43seskesQotXGVNv4iboFm4yO/twlMCG9EDwza32y6WZtV8i9gkQP42OfK0X1qy6EUz2DN7cpfZtmkNtsFJhFf9waOvNCR95LVCPGafeCOMAQEvu1VO3mrBSIg7Izu0CzvuaBQTwnGv29Ggxc3GO4gvb/OStkkmfIwchu3A8F6e0aJ4Ys8PFP7z7Z8lqQIDAQABoyEwHzAdBgNVHQ4EFgQUeGPdsxkVp8lIRku0u41SCzqW7LIwDQYJKoZIhvcNAQELBQADggEBAHMJCPO473QQJtTXJ49OhZ48kVCiVgbut+xElHxvBWQrfJ4Zb6WAi2RudjwrpwchVBciwjIelp/3Ryp5rVL94D479Ta/C5BzWNm9LsZCw3rPrsIvUdx26GmfQomHyL18AJQyBj8jZ+pVvdprvbV7v586TcgY24pW018IiYGQEO/fR8DSO4eN8ekTvT8hODBoKiJ9NFy+BruqW1AbMDptH12uzpU/N9bftysnWeDJEVZd5Rj8u8F9MRbB6V7dzxdoswaKkiJbxt+JrZgdtHSFqz6rDypIkumYwUkyiwH4/GQGPiyBLFbRp1EYVa3SFwAEmhl4a7On05aHVnOfCoyj/qA=" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "MGLqj98VNLoXaFfpJCBpgB4JaKs", + "x5t": "MGLqj98VNLoXaFfpJCBpgB4JaKs", + "n": "yfNcG8Ka_b4R7niLqdzlFvzRjrTdl2wTVEtqRWXqDhJAt_KIizVrqe0x3E1tNohmySHAMz3IS4llC41ML5YUDZVg33XR0RMc6UntOq-qCSOkPXdliC3QkwxspGAaJxVzhO5OZuQlMoQNL6_1FgdaR62gfayjLSJepB8M-o7yC8sOtRhatwe9kbO_5QJC54B8ni0ge5i9nANMln-9ZCHeRQYkgl0RSvR_KtfpWrEqAa4K2cyPaDqejOs8G8V0kM_8CLtDWi5diKpO_fvzRJwparEB5hfMdjAyJgdTOqCVUulZdL7tsoHzb8-_ufq-5QFJkyNUpYB9R1mVQwmRGdY0nQ", + "e": "AQAB", + "x5c": [ + "MIIC/jCCAeagAwIBAgIJAJtuCSyF4i1FMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwNjA5MTkxNzM5WhcNMjkwNjA5MTkxNzM5WjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyfNcG8Ka/b4R7niLqdzlFvzRjrTdl2wTVEtqRWXqDhJAt/KIizVrqe0x3E1tNohmySHAMz3IS4llC41ML5YUDZVg33XR0RMc6UntOq+qCSOkPXdliC3QkwxspGAaJxVzhO5OZuQlMoQNL6/1FgdaR62gfayjLSJepB8M+o7yC8sOtRhatwe9kbO/5QJC54B8ni0ge5i9nANMln+9ZCHeRQYkgl0RSvR/KtfpWrEqAa4K2cyPaDqejOs8G8V0kM/8CLtDWi5diKpO/fvzRJwparEB5hfMdjAyJgdTOqCVUulZdL7tsoHzb8+/ufq+5QFJkyNUpYB9R1mVQwmRGdY0nQIDAQABoyEwHzAdBgNVHQ4EFgQUzF0gtMcVDEn4JoNlDOxvhM8IHBswDQYJKoZIhvcNAQELBQADggEBAJe2muR0H2h3phiZ/v6FD8Yio6niulN9jr7+eC/UJV1M7l5xdHgVL83JbNZjUECDrJ/m+ICY1NbEXfv4fo3sfpU1AwG5GXAhxTrS4zMhH7Hvir3800wCd3ByJ/2vQW1y3orlqR8Q65BN9ayub6BCBTNmtUAOpAWcnP3FnGtIDmAL4APcacK92ZTg8ayVX586U7DDWmI4l7X6xCruK0ic5W2b13k2cay0EalHNWHl+gikqQg6tTGSvM295P6Xy5bQ1I5QtHjVCbm0315T/FylvR8fZhVD+AUCc1DwtOr3Yhm3EXftDb6hP08C4yDhGIDH3Q3+xuWlIA7KQjgljuiT67U=" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "inEVM76gXEQEonQ0PSQXBO_7HfU", + "x5t": "inEVM76gXEQEonQ0PSQXBO_7HfU", + "n": "q0sct8P8TxXmXX2QXzIhMnwZHdCO96SMFCMtfswF1TpxYqaIFObIhT_zxxpBTsvkYHAxG7CUQ6qVgd_TQhMx0TSZq_X3_0NG6cIRik0g-Woe0gT6tUJ-o6zdtO-6EvoOXovT3YMh8vN1Q5UJV6dudDqjnlTNHH1OxFcU4U6no1R6iILDMci_TGq7I2AJS5i_O9Ptp5NmgDT_kbwZHJz1Abbw4VuOPMFJ2Q1rN9odV9YHKjjowqa3BULVyTvP8FoGUzhoopu6O7oA-ehlO9fhEoSS0zNn0lWXQMZXUF7GSyui12121kIXyll2KlvuETQNdVkeXu0m95g_pnX-8iZ_cw", + "e": "AQAB", + "x5c": [ + "MIIC/TCCAeWgAwIBAgIINBmSj+3xdxwwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNDA2MTIxNjA3MjBaFw0yOTA2MTIxNjA3MjBaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrSxy3w/xPFeZdfZBfMiEyfBkd0I73pIwUIy1+zAXVOnFipogU5siFP/PHGkFOy+RgcDEbsJRDqpWB39NCEzHRNJmr9ff/Q0bpwhGKTSD5ah7SBPq1Qn6jrN2077oS+g5ei9PdgyHy83VDlQlXp250OqOeVM0cfU7EVxThTqejVHqIgsMxyL9MarsjYAlLmL870+2nk2aANP+RvBkcnPUBtvDhW448wUnZDWs32h1X1gcqOOjCprcFQtXJO8/wWgZTOGiim7o7ugD56GU71+EShJLTM2fSVZdAxldQXsZLK6LXbXbWQhfKWXYqW+4RNA11WR5e7Sb3mD+mdf7yJn9zAgMBAAGjITAfMB0GA1UdDgQWBBSZbhe/r/sxfv0nYlyrwjx+b6W2RTANBgkqhkiG9w0BAQsFAAOCAQEAoWZ+C/snZySK1KiOsrn1iq7wrVzkuModPMZEshR3SuDIB6+C76fmP42I3UtDVIY5EeE79YjdwDwy86dPZjKVNbP7yUSbJC8uPM1TNMA9s8QpO6RZ63ZZ4i8hcgk6PXgi0PPjX2cmzUSNUa4gS8ibhf7JDu4aF9lUceBsNQghNQfz3tBs1ksJoW3WY5EfW6yMCv1Vim0uBlpnYdlynAd8O+9N2JR9wC+12PwPGrdGQDX3pos8bnmBxM55ueiGoqDH5yGI1h63POlGnpEdqOONT8N4cZNazQ1NswbBQuZMZSfbPXjiiFQ4bktyiXr421KbknQrkRYogi1F2Cjrd4SZJg==" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "KQ2tAcrE7lBaVVGBmc5FobgdJo4", + "x5t": "KQ2tAcrE7lBaVVGBmc5FobgdJo4", + "n": "08xqQ-OBv9jvWmtvWw8g3IkcuDHVOAGCn3K6TXyKie0L6cAyQWNX4vqxbt0cHdaLunrzaFJ2mIGj_qfor8KR_FOFVFOF24FAakB5El96LvsTwlWJNIw4kpf1O_xibycZ_UcDAEqABJfe51JSPh-PxI2sXt0UMapSjvTdnps0Conp11Ay_yupl_h7nawVg0kzw3QDX5-vKTruiHAHr845YwDRW1yJLEgkUPYXdM8d_SrRgqb2RKJEN8D1c4-SUpFHKwGAwLgVYH1cqwADX9el857z_2uKqJoP48l8WqUOfNGdvx79RCgF1NzzRh07EQrk0GJ_EB8eO-EF4YHLPImVtQ", + "e": "AQAB", + "x5c": [ + "MIIC/TCCAeWgAwIBAgIIRf5MUh/1XVIwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNDA3MTAxNjA0NTBaFw0yOTA3MTAxNjA0NTBaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTzGpD44G/2O9aa29bDyDciRy4MdU4AYKfcrpNfIqJ7QvpwDJBY1fi+rFu3Rwd1ou6evNoUnaYgaP+p+ivwpH8U4VUU4XbgUBqQHkSX3ou+xPCVYk0jDiSl/U7/GJvJxn9RwMASoAEl97nUlI+H4/Ejaxe3RQxqlKO9N2emzQKienXUDL/K6mX+HudrBWDSTPDdANfn68pOu6IcAevzjljANFbXIksSCRQ9hd0zx39KtGCpvZEokQ3wPVzj5JSkUcrAYDAuBVgfVyrAANf16XznvP/a4qomg/jyXxapQ580Z2/Hv1EKAXU3PNGHTsRCuTQYn8QHx474QXhgcs8iZW1AgMBAAGjITAfMB0GA1UdDgQWBBQGoURL0sKGdYALEdvfObZ6NEgmJTANBgkqhkiG9w0BAQsFAAOCAQEAl/UkmIa4OvsgULkBmGIZ6HeyJDvVuORphBK9/vpxEFsgnlitwMncBO54uMjJVr63baV490ODSI+ZTiCh7WGM+zrSjllCbVWDxjrdXA1ygHnXX7bXecIQyDmVb5/Hfb7DmQ4MHa3lEwf+pNS5XJeOhPoduRsfYCdD0QbxEADDgqV4FtgYx4I+iAoqbPDPou7wchEu9d3MuFuTMorkTvDLCyTHi2rgBnk9GBf2rArCGyTpvVPGXmxBttqgm9krFRujLj00u9jKUx4YkmAhS9YRddME8+gh6X4qFMxQMhyzkaBxjLs/E+pwMJaUwBqvostwt9+52qrMSUo+jkFgiGCe4Q==" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "EHu9neGZBCDyv2IYq8U5JiRMFng", + "x5t": "EHu9neGZBCDyv2IYq8U5JiRMFng", + "n": "w1kH9dFGdaJS8fvQulDssuuNhkczzy1Mo6IiNoC3ih3K-L_VF5TQmSkqXrovWCUlhBCfc1VPR9Cn2G4UP7Sygn0nTqXBY1NFQQZecqwGESJFIuonRqjdlDhNYXjSF_eg63KyuyLV8A-Sn05Ufuc8ax0tyrxPbkOql0pB2hmRhj94iDAFB2LBoxfEgxCG3VT0ascVYW6voTCChs2P65-4RLC-ib1w1FjuACDwsB7KZDxxaUGLfnIoLWUjmw1zCaDRiRvhxB4jQXpB64IFxaYsqxA_x8bj2JEE7qALZ2dZ3fPy9yYSAnRfaTMetgouR9x4SKy4HxUxsADMm_7p9LiRZQ", + "e": "AQAB", + "x5c": [ + "MIIC6jCCAdKgAwIBAgIJAO8yTjZIibNNMA0GCSqGSIb3DQEBCwUAMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czAeFw0yNDA1MDYyMzA5MDJaFw0yOTA1MDYyMzA5MDJaMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMNZB/XRRnWiUvH70LpQ7LLrjYZHM88tTKOiIjaAt4odyvi/1ReU0JkpKl66L1glJYQQn3NVT0fQp9huFD+0soJ9J06lwWNTRUEGXnKsBhEiRSLqJ0ao3ZQ4TWF40hf3oOtysrsi1fAPkp9OVH7nPGsdLcq8T25DqpdKQdoZkYY/eIgwBQdiwaMXxIMQht1U9GrHFWFur6EwgobNj+ufuESwvom9cNRY7gAg8LAeymQ8cWlBi35yKC1lI5sNcwmg0Ykb4cQeI0F6QeuCBcWmLKsQP8fG49iRBO6gC2dnWd3z8vcmEgJ0X2kzHrYKLkfceEisuB8VMbAAzJv+6fS4kWUCAwEAAaMhMB8wHQYDVR0OBBYEFJ4xtCt3JpPxlUVH7ATgJGM4ofg7MA0GCSqGSIb3DQEBCwUAA4IBAQB9WAEvE3VtO5wIOtN5N/QbIU63H5QPgMW3M9nOs43AhLgwvWupxaiATyMqtK53RPvcxYPe7QwSw/xH9McXii1bOBVmc71AcjlXYfuMJ/0IMEFEUQwZDEwj+vIlg07gWh0hleehyAgMblDUQRRN+b5J+soa9LBBAooY/48F/++y4DiTzKyoWn5cV4H2kdIFVyB43XzJRqDoK1ZhplVLTc1a3K1NL1/qP9rhvtx62YDzfNh4+FTJLu31ALcUbD+Qx2m0U9wuWq3EdUzEen5DeLvhx55YD7V1BASHNYBd8lGhHk97aTw53CMGAuTELvWO+4x7dFM9autw2KvSn76n/4Ql" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "FB8_wii85nv_UW3qrldTvWwg-rE", + "x5t": "FB8_wii85nv_UW3qrldTvWwg-rE", + "n": "vusbA5UBNtCB0U2RmyQOCE-8fWl8bzCQXm3V5Nd7oockcyCpqXOWfhVNJD-Ifb5_zAmxRgHvRdfpA2btaqZiit5XaFYngtRK6mVxCcnOEgwxQGX9DLM5plXWtGTf_DF1FATBidFlM8KgicTS3MTyKZNrnTz0JD7ISxwV0TgSEiRrsm7eVsumuNYNW30Yb38DRDTei9U1YR0YDmdZyuf-OKTllxKH_BO-aj8Gkxcnkdriih2CINF6M6oASOHTJYO7P8CQE1DX2y2Zq7xxVvzm4IClk7WDdzuAoC-ZiKvDaU5plSyrnH3_VgjJrzXtuGN-HEd4Vg89h_2rE74cN5KRtQ", + "e": "AQAB", + "x5c": [ + "MIIC6jCCAdKgAwIBAgIJAJOO92n+BJBLMA0GCSqGSIb3DQEBCwUAMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czAeFw0yNDA2MTAxODA3NDVaFw0yOTA2MTAxODA3NDVaMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL7rGwOVATbQgdFNkZskDghPvH1pfG8wkF5t1eTXe6KHJHMgqalzln4VTSQ/iH2+f8wJsUYB70XX6QNm7WqmYoreV2hWJ4LUSuplcQnJzhIMMUBl/QyzOaZV1rRk3/wxdRQEwYnRZTPCoInE0tzE8imTa5089CQ+yEscFdE4EhIka7Ju3lbLprjWDVt9GG9/A0Q03ovVNWEdGA5nWcrn/jik5ZcSh/wTvmo/BpMXJ5Ha4oodgiDRejOqAEjh0yWDuz/AkBNQ19stmau8cVb85uCApZO1g3c7gKAvmYirw2lOaZUsq5x9/1YIya817bhjfhxHeFYPPYf9qxO+HDeSkbUCAwEAAaMhMB8wHQYDVR0OBBYEFLBW6P0A+qHESOFg8Rgxqp38myYtMA0GCSqGSIb3DQEBCwUAA4IBAQAsZzkzk8w7RR3KCHOY+XLn3R2NanL/j+WILdOHnJn9Ot1VbG868MFQgwMp8Y2y7Kj5RekknY6EGcNuJi4rLgq5u1LSB/IoNPCs7l3MhRQqoedJX4sDNf4NfTVHK+4GNSQqP60eBoxClRexIbKcHJ0x57Ww/S9NNWtldBIfB7egoSj6UVcTHRLWZyPoZsOXHY4bYOf8ANNg21jT1KWwOXSWUx60v7tVxEXs8XAEUnmuMbuh3yAnjv3UoRdl7wcaQ5jq2/+vaAWZm0WlWN3CCY3y2mE0OZZg9HRCQu+o+58wt658sDIpP7PXGjyA5h23W9+i8QtyQ1PtqCXKj8zktivW" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "ZZ8JkzXRCgYMWcMYdOn9sdDBeUA", + "x5t": "ZZ8JkzXRCgYMWcMYdOn9sdDBeUA", + "n": "iJd0N795eVyYQvWe417HOF_GHlRgOsPZRh1KwNHyWP_WKrjlOl8ftPAs-Sspv-s8v68TilHYkY9pjUdEkxBvBolJiPP6ntAKIKk_Fa4K7sutgQdyxKehhRnk4hAIc-mUM9ROrkyr4dIi-Au9T7aWaBSG5dCffXQBQ1DBVbIUMNOwr4ewQYeb49ujxzE6dPiCB-uDX2Z_hjy9M8wMrHS8e2vKDYqx3AJ3xyiDjIDB-wBEe-SF5bKVNSfExcsiL0KzV_iQkKQNALJrakX4Mw-hC3ssv7q3NQcza9kpyew0TKytSOcJcIheX9Cse22F-y1h873iOkYWiebNIu5TeTjVww", + "e": "AQAB", + "x5c": [ + "MIIC6TCCAdGgAwIBAgIIHxgoSKq7mWgwDQYJKoZIhvcNAQELBQAwIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMB4XDTI0MDYxMDE4MjgyOFoXDTI5MDYxMDE4MjgyOFowIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiJd0N795eVyYQvWe417HOF/GHlRgOsPZRh1KwNHyWP/WKrjlOl8ftPAs+Sspv+s8v68TilHYkY9pjUdEkxBvBolJiPP6ntAKIKk/Fa4K7sutgQdyxKehhRnk4hAIc+mUM9ROrkyr4dIi+Au9T7aWaBSG5dCffXQBQ1DBVbIUMNOwr4ewQYeb49ujxzE6dPiCB+uDX2Z/hjy9M8wMrHS8e2vKDYqx3AJ3xyiDjIDB+wBEe+SF5bKVNSfExcsiL0KzV/iQkKQNALJrakX4Mw+hC3ssv7q3NQcza9kpyew0TKytSOcJcIheX9Cse22F+y1h873iOkYWiebNIu5TeTjVwwIDAQABoyEwHzAdBgNVHQ4EFgQUcLvbIYVCbexuF1KXcKysM8kS6EMwDQYJKoZIhvcNAQELBQADggEBAF95Wf/yAfmHksmL42JiCemjsHN0KlZ2NsGTj2+zbDXbttj8zm+ZA74bPlAWI5aFvKfxxpC3Chfi26+GhKVeVRA65KyokTulQzE+BWbqphQZoH6Iz07J3GB3uUthPQbedtj6SDD/zE4jcmhmrY8o0lU5zJhkp9T5f8644ZR6rJRIXpFbDwmbsFM5H4Nz7D5FG+A4uYumICoTaiQjJ+cu/k8sDM8ut6R2cGmwlRMIGzD8HzNeGuaRtXsFqCGAI+qRbW29hJoFNZxhQBeFRDdBvwbNIa/o6ZAzKq81E4SdV1d33oM3vWDMBlR3b46a1d+Unm1Ou8uJ2yDfqMrZ7/NGNV8=" + ] + } + ] + } + """; + public static OpenIdConnectConfiguration AADCommonV1Config { get @@ -306,6 +392,7 @@ public static OpenIdConnectConfiguration AADCommonV1Config config.AdditionalData.Add("cloud_graph_host_name", "graph.windows.net"); config.AdditionalData.Add("msgraph_host", "graph.microsoft.com"); config.AdditionalData.Add("rbac_url", "https://pas.windows.net"); + config.JsonWebKeySet = JsonWebKeySet.Create(AADCommonV1JwksString); return config; } @@ -342,6 +429,133 @@ public static OpenIdConnectConfiguration AADCommonV1Config } """; + public static string AADCommonV2JwksString => + """ + { + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "23ntaxfH9Gk-8D_USzoAJgwjyt8", + "x5t": "23ntaxfH9Gk-8D_USzoAJgwjyt8", + "n": "hu2SJrLlDOUtU2s9T6-6OVGEPaba2zIT2_Jl50f4NGG-r-GyQdaOzTFASfAfMkMfMQMRnabqd-dp_Ooqha473bw6DMbM23nv2uhBn5Afp-S1W_d4NxEhfNlN1Tgjx3Sh6UblBSFCE4JGkugSkLi2SVouy43seskesQotXGVNv4iboFm4yO_twlMCG9EDwza32y6WZtV8i9gkQP42OfK0X1qy6EUz2DN7cpfZtmkNtsFJhFf9waOvNCR95LVCPGafeCOMAQEvu1VO3mrBSIg7Izu0CzvuaBQTwnGv29Ggxc3GO4gvb_OStkkmfIwchu3A8F6e0aJ4Ys8PFP7z7Z8lqQ", + "e": "AQAB", + "x5c": [ + "MIIC/jCCAeagAwIBAgIJAJB4tJM2GkZjMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwNTIyMTg1ODQwWhcNMjkwNTIyMTg1ODQwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhu2SJrLlDOUtU2s9T6+6OVGEPaba2zIT2/Jl50f4NGG+r+GyQdaOzTFASfAfMkMfMQMRnabqd+dp/Ooqha473bw6DMbM23nv2uhBn5Afp+S1W/d4NxEhfNlN1Tgjx3Sh6UblBSFCE4JGkugSkLi2SVouy43seskesQotXGVNv4iboFm4yO/twlMCG9EDwza32y6WZtV8i9gkQP42OfK0X1qy6EUz2DN7cpfZtmkNtsFJhFf9waOvNCR95LVCPGafeCOMAQEvu1VO3mrBSIg7Izu0CzvuaBQTwnGv29Ggxc3GO4gvb/OStkkmfIwchu3A8F6e0aJ4Ys8PFP7z7Z8lqQIDAQABoyEwHzAdBgNVHQ4EFgQUeGPdsxkVp8lIRku0u41SCzqW7LIwDQYJKoZIhvcNAQELBQADggEBAHMJCPO473QQJtTXJ49OhZ48kVCiVgbut+xElHxvBWQrfJ4Zb6WAi2RudjwrpwchVBciwjIelp/3Ryp5rVL94D479Ta/C5BzWNm9LsZCw3rPrsIvUdx26GmfQomHyL18AJQyBj8jZ+pVvdprvbV7v586TcgY24pW018IiYGQEO/fR8DSO4eN8ekTvT8hODBoKiJ9NFy+BruqW1AbMDptH12uzpU/N9bftysnWeDJEVZd5Rj8u8F9MRbB6V7dzxdoswaKkiJbxt+JrZgdtHSFqz6rDypIkumYwUkyiwH4/GQGPiyBLFbRp1EYVa3SFwAEmhl4a7On05aHVnOfCoyj/qA=" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "MGLqj98VNLoXaFfpJCBpgB4JaKs", + "x5t": "MGLqj98VNLoXaFfpJCBpgB4JaKs", + "n": "yfNcG8Ka_b4R7niLqdzlFvzRjrTdl2wTVEtqRWXqDhJAt_KIizVrqe0x3E1tNohmySHAMz3IS4llC41ML5YUDZVg33XR0RMc6UntOq-qCSOkPXdliC3QkwxspGAaJxVzhO5OZuQlMoQNL6_1FgdaR62gfayjLSJepB8M-o7yC8sOtRhatwe9kbO_5QJC54B8ni0ge5i9nANMln-9ZCHeRQYkgl0RSvR_KtfpWrEqAa4K2cyPaDqejOs8G8V0kM_8CLtDWi5diKpO_fvzRJwparEB5hfMdjAyJgdTOqCVUulZdL7tsoHzb8-_ufq-5QFJkyNUpYB9R1mVQwmRGdY0nQ", + "e": "AQAB", + "x5c": [ + "MIIC/jCCAeagAwIBAgIJAJtuCSyF4i1FMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwNjA5MTkxNzM5WhcNMjkwNjA5MTkxNzM5WjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyfNcG8Ka/b4R7niLqdzlFvzRjrTdl2wTVEtqRWXqDhJAt/KIizVrqe0x3E1tNohmySHAMz3IS4llC41ML5YUDZVg33XR0RMc6UntOq+qCSOkPXdliC3QkwxspGAaJxVzhO5OZuQlMoQNL6/1FgdaR62gfayjLSJepB8M+o7yC8sOtRhatwe9kbO/5QJC54B8ni0ge5i9nANMln+9ZCHeRQYkgl0RSvR/KtfpWrEqAa4K2cyPaDqejOs8G8V0kM/8CLtDWi5diKpO/fvzRJwparEB5hfMdjAyJgdTOqCVUulZdL7tsoHzb8+/ufq+5QFJkyNUpYB9R1mVQwmRGdY0nQIDAQABoyEwHzAdBgNVHQ4EFgQUzF0gtMcVDEn4JoNlDOxvhM8IHBswDQYJKoZIhvcNAQELBQADggEBAJe2muR0H2h3phiZ/v6FD8Yio6niulN9jr7+eC/UJV1M7l5xdHgVL83JbNZjUECDrJ/m+ICY1NbEXfv4fo3sfpU1AwG5GXAhxTrS4zMhH7Hvir3800wCd3ByJ/2vQW1y3orlqR8Q65BN9ayub6BCBTNmtUAOpAWcnP3FnGtIDmAL4APcacK92ZTg8ayVX586U7DDWmI4l7X6xCruK0ic5W2b13k2cay0EalHNWHl+gikqQg6tTGSvM295P6Xy5bQ1I5QtHjVCbm0315T/FylvR8fZhVD+AUCc1DwtOr3Yhm3EXftDb6hP08C4yDhGIDH3Q3+xuWlIA7KQjgljuiT67U=" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "inEVM76gXEQEonQ0PSQXBO_7HfU", + "x5t": "inEVM76gXEQEonQ0PSQXBO_7HfU", + "n": "q0sct8P8TxXmXX2QXzIhMnwZHdCO96SMFCMtfswF1TpxYqaIFObIhT_zxxpBTsvkYHAxG7CUQ6qVgd_TQhMx0TSZq_X3_0NG6cIRik0g-Woe0gT6tUJ-o6zdtO-6EvoOXovT3YMh8vN1Q5UJV6dudDqjnlTNHH1OxFcU4U6no1R6iILDMci_TGq7I2AJS5i_O9Ptp5NmgDT_kbwZHJz1Abbw4VuOPMFJ2Q1rN9odV9YHKjjowqa3BULVyTvP8FoGUzhoopu6O7oA-ehlO9fhEoSS0zNn0lWXQMZXUF7GSyui12121kIXyll2KlvuETQNdVkeXu0m95g_pnX-8iZ_cw", + "e": "AQAB", + "x5c": [ + "MIIC/TCCAeWgAwIBAgIINBmSj+3xdxwwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNDA2MTIxNjA3MjBaFw0yOTA2MTIxNjA3MjBaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrSxy3w/xPFeZdfZBfMiEyfBkd0I73pIwUIy1+zAXVOnFipogU5siFP/PHGkFOy+RgcDEbsJRDqpWB39NCEzHRNJmr9ff/Q0bpwhGKTSD5ah7SBPq1Qn6jrN2077oS+g5ei9PdgyHy83VDlQlXp250OqOeVM0cfU7EVxThTqejVHqIgsMxyL9MarsjYAlLmL870+2nk2aANP+RvBkcnPUBtvDhW448wUnZDWs32h1X1gcqOOjCprcFQtXJO8/wWgZTOGiim7o7ugD56GU71+EShJLTM2fSVZdAxldQXsZLK6LXbXbWQhfKWXYqW+4RNA11WR5e7Sb3mD+mdf7yJn9zAgMBAAGjITAfMB0GA1UdDgQWBBSZbhe/r/sxfv0nYlyrwjx+b6W2RTANBgkqhkiG9w0BAQsFAAOCAQEAoWZ+C/snZySK1KiOsrn1iq7wrVzkuModPMZEshR3SuDIB6+C76fmP42I3UtDVIY5EeE79YjdwDwy86dPZjKVNbP7yUSbJC8uPM1TNMA9s8QpO6RZ63ZZ4i8hcgk6PXgi0PPjX2cmzUSNUa4gS8ibhf7JDu4aF9lUceBsNQghNQfz3tBs1ksJoW3WY5EfW6yMCv1Vim0uBlpnYdlynAd8O+9N2JR9wC+12PwPGrdGQDX3pos8bnmBxM55ueiGoqDH5yGI1h63POlGnpEdqOONT8N4cZNazQ1NswbBQuZMZSfbPXjiiFQ4bktyiXr421KbknQrkRYogi1F2Cjrd4SZJg==" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "KQ2tAcrE7lBaVVGBmc5FobgdJo4", + "x5t": "KQ2tAcrE7lBaVVGBmc5FobgdJo4", + "n": "08xqQ-OBv9jvWmtvWw8g3IkcuDHVOAGCn3K6TXyKie0L6cAyQWNX4vqxbt0cHdaLunrzaFJ2mIGj_qfor8KR_FOFVFOF24FAakB5El96LvsTwlWJNIw4kpf1O_xibycZ_UcDAEqABJfe51JSPh-PxI2sXt0UMapSjvTdnps0Conp11Ay_yupl_h7nawVg0kzw3QDX5-vKTruiHAHr845YwDRW1yJLEgkUPYXdM8d_SrRgqb2RKJEN8D1c4-SUpFHKwGAwLgVYH1cqwADX9el857z_2uKqJoP48l8WqUOfNGdvx79RCgF1NzzRh07EQrk0GJ_EB8eO-EF4YHLPImVtQ", + "e": "AQAB", + "x5c": [ + "MIIC/TCCAeWgAwIBAgIIRf5MUh/1XVIwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNDA3MTAxNjA0NTBaFw0yOTA3MTAxNjA0NTBaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTzGpD44G/2O9aa29bDyDciRy4MdU4AYKfcrpNfIqJ7QvpwDJBY1fi+rFu3Rwd1ou6evNoUnaYgaP+p+ivwpH8U4VUU4XbgUBqQHkSX3ou+xPCVYk0jDiSl/U7/GJvJxn9RwMASoAEl97nUlI+H4/Ejaxe3RQxqlKO9N2emzQKienXUDL/K6mX+HudrBWDSTPDdANfn68pOu6IcAevzjljANFbXIksSCRQ9hd0zx39KtGCpvZEokQ3wPVzj5JSkUcrAYDAuBVgfVyrAANf16XznvP/a4qomg/jyXxapQ580Z2/Hv1EKAXU3PNGHTsRCuTQYn8QHx474QXhgcs8iZW1AgMBAAGjITAfMB0GA1UdDgQWBBQGoURL0sKGdYALEdvfObZ6NEgmJTANBgkqhkiG9w0BAQsFAAOCAQEAl/UkmIa4OvsgULkBmGIZ6HeyJDvVuORphBK9/vpxEFsgnlitwMncBO54uMjJVr63baV490ODSI+ZTiCh7WGM+zrSjllCbVWDxjrdXA1ygHnXX7bXecIQyDmVb5/Hfb7DmQ4MHa3lEwf+pNS5XJeOhPoduRsfYCdD0QbxEADDgqV4FtgYx4I+iAoqbPDPou7wchEu9d3MuFuTMorkTvDLCyTHi2rgBnk9GBf2rArCGyTpvVPGXmxBttqgm9krFRujLj00u9jKUx4YkmAhS9YRddME8+gh6X4qFMxQMhyzkaBxjLs/E+pwMJaUwBqvostwt9+52qrMSUo+jkFgiGCe4Q==" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "EHu9neGZBCDyv2IYq8U5JiRMFng", + "x5t": "EHu9neGZBCDyv2IYq8U5JiRMFng", + "n": "w1kH9dFGdaJS8fvQulDssuuNhkczzy1Mo6IiNoC3ih3K-L_VF5TQmSkqXrovWCUlhBCfc1VPR9Cn2G4UP7Sygn0nTqXBY1NFQQZecqwGESJFIuonRqjdlDhNYXjSF_eg63KyuyLV8A-Sn05Ufuc8ax0tyrxPbkOql0pB2hmRhj94iDAFB2LBoxfEgxCG3VT0ascVYW6voTCChs2P65-4RLC-ib1w1FjuACDwsB7KZDxxaUGLfnIoLWUjmw1zCaDRiRvhxB4jQXpB64IFxaYsqxA_x8bj2JEE7qALZ2dZ3fPy9yYSAnRfaTMetgouR9x4SKy4HxUxsADMm_7p9LiRZQ", + "e": "AQAB", + "x5c": [ + "MIIC6jCCAdKgAwIBAgIJAO8yTjZIibNNMA0GCSqGSIb3DQEBCwUAMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czAeFw0yNDA1MDYyMzA5MDJaFw0yOTA1MDYyMzA5MDJaMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMNZB/XRRnWiUvH70LpQ7LLrjYZHM88tTKOiIjaAt4odyvi/1ReU0JkpKl66L1glJYQQn3NVT0fQp9huFD+0soJ9J06lwWNTRUEGXnKsBhEiRSLqJ0ao3ZQ4TWF40hf3oOtysrsi1fAPkp9OVH7nPGsdLcq8T25DqpdKQdoZkYY/eIgwBQdiwaMXxIMQht1U9GrHFWFur6EwgobNj+ufuESwvom9cNRY7gAg8LAeymQ8cWlBi35yKC1lI5sNcwmg0Ykb4cQeI0F6QeuCBcWmLKsQP8fG49iRBO6gC2dnWd3z8vcmEgJ0X2kzHrYKLkfceEisuB8VMbAAzJv+6fS4kWUCAwEAAaMhMB8wHQYDVR0OBBYEFJ4xtCt3JpPxlUVH7ATgJGM4ofg7MA0GCSqGSIb3DQEBCwUAA4IBAQB9WAEvE3VtO5wIOtN5N/QbIU63H5QPgMW3M9nOs43AhLgwvWupxaiATyMqtK53RPvcxYPe7QwSw/xH9McXii1bOBVmc71AcjlXYfuMJ/0IMEFEUQwZDEwj+vIlg07gWh0hleehyAgMblDUQRRN+b5J+soa9LBBAooY/48F/++y4DiTzKyoWn5cV4H2kdIFVyB43XzJRqDoK1ZhplVLTc1a3K1NL1/qP9rhvtx62YDzfNh4+FTJLu31ALcUbD+Qx2m0U9wuWq3EdUzEen5DeLvhx55YD7V1BASHNYBd8lGhHk97aTw53CMGAuTELvWO+4x7dFM9autw2KvSn76n/4Ql" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "FB8_wii85nv_UW3qrldTvWwg-rE", + "x5t": "FB8_wii85nv_UW3qrldTvWwg-rE", + "n": "vusbA5UBNtCB0U2RmyQOCE-8fWl8bzCQXm3V5Nd7oockcyCpqXOWfhVNJD-Ifb5_zAmxRgHvRdfpA2btaqZiit5XaFYngtRK6mVxCcnOEgwxQGX9DLM5plXWtGTf_DF1FATBidFlM8KgicTS3MTyKZNrnTz0JD7ISxwV0TgSEiRrsm7eVsumuNYNW30Yb38DRDTei9U1YR0YDmdZyuf-OKTllxKH_BO-aj8Gkxcnkdriih2CINF6M6oASOHTJYO7P8CQE1DX2y2Zq7xxVvzm4IClk7WDdzuAoC-ZiKvDaU5plSyrnH3_VgjJrzXtuGN-HEd4Vg89h_2rE74cN5KRtQ", + "e": "AQAB", + "x5c": [ + "MIIC6jCCAdKgAwIBAgIJAJOO92n+BJBLMA0GCSqGSIb3DQEBCwUAMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czAeFw0yNDA2MTAxODA3NDVaFw0yOTA2MTAxODA3NDVaMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL7rGwOVATbQgdFNkZskDghPvH1pfG8wkF5t1eTXe6KHJHMgqalzln4VTSQ/iH2+f8wJsUYB70XX6QNm7WqmYoreV2hWJ4LUSuplcQnJzhIMMUBl/QyzOaZV1rRk3/wxdRQEwYnRZTPCoInE0tzE8imTa5089CQ+yEscFdE4EhIka7Ju3lbLprjWDVt9GG9/A0Q03ovVNWEdGA5nWcrn/jik5ZcSh/wTvmo/BpMXJ5Ha4oodgiDRejOqAEjh0yWDuz/AkBNQ19stmau8cVb85uCApZO1g3c7gKAvmYirw2lOaZUsq5x9/1YIya817bhjfhxHeFYPPYf9qxO+HDeSkbUCAwEAAaMhMB8wHQYDVR0OBBYEFLBW6P0A+qHESOFg8Rgxqp38myYtMA0GCSqGSIb3DQEBCwUAA4IBAQAsZzkzk8w7RR3KCHOY+XLn3R2NanL/j+WILdOHnJn9Ot1VbG868MFQgwMp8Y2y7Kj5RekknY6EGcNuJi4rLgq5u1LSB/IoNPCs7l3MhRQqoedJX4sDNf4NfTVHK+4GNSQqP60eBoxClRexIbKcHJ0x57Ww/S9NNWtldBIfB7egoSj6UVcTHRLWZyPoZsOXHY4bYOf8ANNg21jT1KWwOXSWUx60v7tVxEXs8XAEUnmuMbuh3yAnjv3UoRdl7wcaQ5jq2/+vaAWZm0WlWN3CCY3y2mE0OZZg9HRCQu+o+58wt658sDIpP7PXGjyA5h23W9+i8QtyQ1PtqCXKj8zktivW" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "ZZ8JkzXRCgYMWcMYdOn9sdDBeUA", + "x5t": "ZZ8JkzXRCgYMWcMYdOn9sdDBeUA", + "n": "iJd0N795eVyYQvWe417HOF_GHlRgOsPZRh1KwNHyWP_WKrjlOl8ftPAs-Sspv-s8v68TilHYkY9pjUdEkxBvBolJiPP6ntAKIKk_Fa4K7sutgQdyxKehhRnk4hAIc-mUM9ROrkyr4dIi-Au9T7aWaBSG5dCffXQBQ1DBVbIUMNOwr4ewQYeb49ujxzE6dPiCB-uDX2Z_hjy9M8wMrHS8e2vKDYqx3AJ3xyiDjIDB-wBEe-SF5bKVNSfExcsiL0KzV_iQkKQNALJrakX4Mw-hC3ssv7q3NQcza9kpyew0TKytSOcJcIheX9Cse22F-y1h873iOkYWiebNIu5TeTjVww", + "e": "AQAB", + "x5c": [ + "MIIC6TCCAdGgAwIBAgIIHxgoSKq7mWgwDQYJKoZIhvcNAQELBQAwIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMB4XDTI0MDYxMDE4MjgyOFoXDTI5MDYxMDE4MjgyOFowIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiJd0N795eVyYQvWe417HOF/GHlRgOsPZRh1KwNHyWP/WKrjlOl8ftPAs+Sspv+s8v68TilHYkY9pjUdEkxBvBolJiPP6ntAKIKk/Fa4K7sutgQdyxKehhRnk4hAIc+mUM9ROrkyr4dIi+Au9T7aWaBSG5dCffXQBQ1DBVbIUMNOwr4ewQYeb49ujxzE6dPiCB+uDX2Z/hjy9M8wMrHS8e2vKDYqx3AJ3xyiDjIDB+wBEe+SF5bKVNSfExcsiL0KzV/iQkKQNALJrakX4Mw+hC3ssv7q3NQcza9kpyew0TKytSOcJcIheX9Cse22F+y1h873iOkYWiebNIu5TeTjVwwIDAQABoyEwHzAdBgNVHQ4EFgQUcLvbIYVCbexuF1KXcKysM8kS6EMwDQYJKoZIhvcNAQELBQADggEBAF95Wf/yAfmHksmL42JiCemjsHN0KlZ2NsGTj2+zbDXbttj8zm+ZA74bPlAWI5aFvKfxxpC3Chfi26+GhKVeVRA65KyokTulQzE+BWbqphQZoH6Iz07J3GB3uUthPQbedtj6SDD/zE4jcmhmrY8o0lU5zJhkp9T5f8644ZR6rJRIXpFbDwmbsFM5H4Nz7D5FG+A4uYumICoTaiQjJ+cu/k8sDM8ut6R2cGmwlRMIGzD8HzNeGuaRtXsFqCGAI+qRbW29hJoFNZxhQBeFRDdBvwbNIa/o6ZAzKq81E4SdV1d33oM3vWDMBlR3b46a1d+Unm1Ou8uJ2yDfqMrZ7/NGNV8=" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "dGtHQMhGltJUCcH_SQW64nEUoYE", + "x5t": "dGtHQMhGltJUCcH_SQW64nEUoYE", + "n": "io_Qh_kyYBnXCXPV54XbUZheP_fpWo5M0-_aWJQ6i-CebDHQVpxHurahUYj446qEvUBFK-goUEDU1Ah87F_KXNDQhsJq0F422joJPIzsHSsed_k0KlYnkJgCeUC8yHmtgSnNjH7jCnnBZ6Oznt0rdEw9MVd_2ofWgoA28XRUQ_arpXgGo8EWSPWuLGsG3cKTsSVW-1d_JSZ56S73j5YBDQz11ZPVm13nWohrGEgPBgswCCLUsZod0t1oTiRmKihRom-FhWvsfFixUZ4D39XSk51UjWttu1gnhhxhV7PVqlaqbvQ1D2urlpgMnAgyeQrIUC-3L-fN6hwD_1NZCaQdeQ", + "e": "AQAB", + "x5c": [ + "MIIDCzCCAfOgAwIBAgIRAKdbZc1Eb0WDFn5HsLQuwwYwDQYJKoZIhvcNAQELBQAwKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MB4XDTI0MDcxNDE1MDI1MFoXDTI5MDcxNDE1MDI1MFowKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAio/Qh/kyYBnXCXPV54XbUZheP/fpWo5M0+/aWJQ6i+CebDHQVpxHurahUYj446qEvUBFK+goUEDU1Ah87F/KXNDQhsJq0F422joJPIzsHSsed/k0KlYnkJgCeUC8yHmtgSnNjH7jCnnBZ6Oznt0rdEw9MVd/2ofWgoA28XRUQ/arpXgGo8EWSPWuLGsG3cKTsSVW+1d/JSZ56S73j5YBDQz11ZPVm13nWohrGEgPBgswCCLUsZod0t1oTiRmKihRom+FhWvsfFixUZ4D39XSk51UjWttu1gnhhxhV7PVqlaqbvQ1D2urlpgMnAgyeQrIUC+3L+fN6hwD/1NZCaQdeQIDAQABoy4wLDAdBgNVHQ4EFgQUS8ytdfEdWAsFJApZy/6lA7wlfgowCwYDVR0PBAQDAgLEMA0GCSqGSIb3DQEBCwUAA4IBAQBDnyg3sAC3S64Pm4xA81r2kts96usCRu7tF34f3RJX7Met+rJMrllpRT8zVTzFTaPjHsJvhl5F/ApD0lZN6noy7UwNjbnMoC/lYluPLDuQE4ClstsgpNBdSNF0l+tWk085sIM7LF3wAuf17Yp5jIXCyokbbDJb5+XpNGZm4ukTLADajk/jk76z7p94shgV1XMla3fV+1d7jDL6UlbIvXNUSp3swvSLQPv90sSI2OUwTTulNZDokmeWtLUedTTIpnu9y+vLJWbKiwtenYbj3zM7VN/Qr5aXl4w3Ajx+QKnRydv1se8ycMabu28OFXgP92AsY1/NW4BF6321OOq2OmbC" + ], + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "pb_TKRXVJY-27vIG_A81PMH-cdY", + "x5t": "pb_TKRXVJY-27vIG_A81PMH-cdY", + "n": "iM24cYc714exgvGQeAuw6pqYqkSf7NEuLug5jCYcGqa2APSSjzks5h7en-uEEnE0q03VeaJB6PWxaE3GTfGKXzr-sPudGCTTOsgnY3t4ms3DLeyhZvWi5ADc4JtpLBQOxYm1f4ReGwryZqsOHdvqNiYn7B7PyN_3dVbUuXWaueCJ3hhW5JyXkRGD75cOsgOm7GU3tYtOcxm29yjOzNcQXOiL_fChEz6G6bjOHzFYISgv5m7TffaOEFF4T4RoP4AQ35zvxjHx8XkBQPTz661TjTN1h_mYsFEwa2cDcErjJ4dJTdKSkM-VFPDklcSXsrDhkOw42ZeuKAoQTVep5EJ71w", + "e": "AQAB", + "x5c": [ + "MIIDCjCCAfKgAwIBAgIQNas2IybvbYSgWXpOzctlSDANBgkqhkiG9w0BAQsFADApMScwJQYDVQQDEx5MaXZlIElEIFNUUyBTaWduaW5nIFB1YmxpYyBLZXkwHhcNMjQwNzAxMTgzMzA1WhcNMjkwNzAxMTgzMzA1WjApMScwJQYDVQQDEx5MaXZlIElEIFNUUyBTaWduaW5nIFB1YmxpYyBLZXkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCIzbhxhzvXh7GC8ZB4C7DqmpiqRJ/s0S4u6DmMJhwaprYA9JKPOSzmHt6f64QScTSrTdV5okHo9bFoTcZN8YpfOv6w+50YJNM6yCdje3iazcMt7KFm9aLkANzgm2ksFA7FibV/hF4bCvJmqw4d2+o2JifsHs/I3/d1VtS5dZq54IneGFbknJeREYPvlw6yA6bsZTe1i05zGbb3KM7M1xBc6Iv98KETPobpuM4fMVghKC/mbtN99o4QUXhPhGg/gBDfnO/GMfHxeQFA9PPrrVONM3WH+ZiwUTBrZwNwSuMnh0lN0pKQz5UU8OSVxJeysOGQ7DjZl64oChBNV6nkQnvXAgMBAAGjLjAsMB0GA1UdDgQWBBRsbUU+6mjS1sX/3Ek+xKEA6JTeLDALBgNVHQ8EBAMCAsQwDQYJKoZIhvcNAQELBQADggEBACO1MS24nE70L0Tcw4NRv80uZ8b5OWAfsAO+AN7zwXeo6J7TN/sslMuQ9FtL3Coot2ItdYaFHmfzKijuCV17EiWdXXccwoGEZqp3y2gvyYCof2OVQK4/KZVPUhI8wg2kR8dn09B9fdiMmwqd2+ZezWbgvSz1fQz5gZCg5FbFFojYvQL65bIq3tUZBtAT7ixrcGOfFbYzZbpi4mJdJItidd3Oh+TXfexzRL5Cw7Zn4LGlUVUOwildBfYtB+Fr022wutr/adxjJV7wgr6AxaTlls/hQz6+TOs8Vmyeb8KsU9CJZRXPIBKvZwAyMJsDZ3l4x+XPAZYQo3i6Oa4F5ROR9ZM=" + ], + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "yBm7yTZhZ3SWV3-9inQcQk1X51U", + "x5t": "yBm7yTZhZ3SWV3-9inQcQk1X51U", + "n": "nwNwsp54S9SUESzWUZXc0dY19bOVn4smmRSxANxPblU0nQEBpDPumlBVYmHI3XXVIshrh2DAl4BSVfQhVKLCu35Vyv7_P9cLvmqM_dvIHEjtrQPPFIBlH6fitB4v5zs7i7_zV-mTteGsNoUWg-TtHHKekJBrrBxoJ633vvaZ9AEFP8OdZoVGjXW1Wb76nczV8uhjgF9u69XrOPVrYB7YcxtiA-jRzn8AQRt8SfkrIvEjDL5ejtxRNyucz8dFzmbrCazoUY3oeei6UHjdtFgiODs4KE29e6p1Lm4CexjkcIrFWXkoxytOKEsB5zCGq8pQeI-tGmoCBhVnhNw7u5okjQ", + "e": "AQAB", + "x5c": [ + "MIIDCzCCAfOgAwIBAgIRAMtLWPmqFNLXNg6BbZWr9EAwDQYJKoZIhvcNAQELBQAwKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MB4XDTI0MDYxODIyMzE1MFoXDTI5MDYxODIyMzE1MFowKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnwNwsp54S9SUESzWUZXc0dY19bOVn4smmRSxANxPblU0nQEBpDPumlBVYmHI3XXVIshrh2DAl4BSVfQhVKLCu35Vyv7/P9cLvmqM/dvIHEjtrQPPFIBlH6fitB4v5zs7i7/zV+mTteGsNoUWg+TtHHKekJBrrBxoJ633vvaZ9AEFP8OdZoVGjXW1Wb76nczV8uhjgF9u69XrOPVrYB7YcxtiA+jRzn8AQRt8SfkrIvEjDL5ejtxRNyucz8dFzmbrCazoUY3oeei6UHjdtFgiODs4KE29e6p1Lm4CexjkcIrFWXkoxytOKEsB5zCGq8pQeI+tGmoCBhVnhNw7u5okjQIDAQABoy4wLDAdBgNVHQ4EFgQUFGb4FaXDu89wqG9A6JK1xLUMKPUwCwYDVR0PBAQDAgLEMA0GCSqGSIb3DQEBCwUAA4IBAQAw3cpDyl03Kgka9P2BJR6xU+C+IiWpJVLbLbdvBepqGI1/NqXCrG7E4INS5oRsFVO8DuYXws7Ko5kKCTV+iqkGngtG9b/JFP8QBcRrhngHTnE8EevwLkDtqFvpBdNnzTmOJDP4FdtYRuucJqx7aLE1MXr2jEkKfY7YLu2YEmOG6hnZfqWeCRm+g9eUolbhexllsdtj3bi9V9c8anXPLUsEeY/BRT7n4TBGJBWDD9kYEgoMKPLp58Om8aY6BucKN6vjf/v9RR//2ggCXX+qrZP3ebj9cXI6dWtgn5WwkBTfufIXnbbrzyCp/jdCP7q8SXbG2MeFiqKLKul5q5neiIdm" + ], + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0" + } + ] + } + """; public static OpenIdConnectConfiguration AADCommonV2Config { get @@ -372,6 +586,7 @@ public static OpenIdConnectConfiguration AADCommonV2Config config.AdditionalData.Add("cloud_graph_host_name", "graph.windows.net"); config.AdditionalData.Add("msgraph_host", "graph.microsoft.com"); config.AdditionalData.Add("rbac_url", "https://pas.windows.net"); + config.JsonWebKeySet = JsonWebKeySet.Create(AADCommonV2JwksString); return config; } diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs index 22454614f5..2e3c38f305 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens.Json.Tests; using Xunit; @@ -23,6 +24,7 @@ public void Deserialize(OpenIdConnectTheoryData theoryData) OpenIdConnectConfiguration configuration = new OpenIdConnectConfiguration(theoryData.Json); OpenIdConnectConfiguration configurationUpperCase = new OpenIdConnectConfiguration(JsonUtilities.SetPropertiesToUpperCase(theoryData.Json)); theoryData.ExpectedException.ProcessNoException(context); + context.PropertiesToIgnoreWhenComparing.Add(typeof(OpenIdConnectConfiguration), new List { "JsonWebKeySet" }); IdentityComparer.AreEqual(configuration, theoryData.CompareTo, context); IdentityComparer.AreEqual(configurationUpperCase, theoryData.CompareTo, context); diff --git a/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs b/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs index 7d71601577..711d00ce46 100644 --- a/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs @@ -69,28 +69,34 @@ public void GetMetadataTest(DocumentRetrieverTheoryData theoryData) } [Fact] - public void ConfigurationManagerUsingCustomClass() + public async Task ConfigurationManagerUsingCustomClass() { var docRetriever = new FileDocumentRetriever(); var configManager = new ConfigurationManager("IssuerMetadata.json", new IssuerConfigurationRetriever(), docRetriever); - var context = new CompareContext($"{this}.GetConfiguration"); + var context = new CompareContext($"{this}.ConfigurationManagerUsingCustomClass"); var configuration = configManager.GetConfigurationAsync().Result; configManager.MetadataAddress = "IssuerMetadata.json"; var configuration2 = configManager.GetConfigurationAsync().Result; - if (!IdentityComparer.AreEqual(configuration.Issuer, configuration2.Issuer)) + if (!IdentityComparer.AreEqual(configuration.Issuer, configuration2.Issuer, context)) context.Diffs.Add("!IdentityComparer.AreEqual(configuration, configuration2)"); // AutomaticRefreshInterval should pick up new bits. configManager = new ConfigurationManager("IssuerMetadata.json", new IssuerConfigurationRetriever(), docRetriever); configManager.RequestRefresh(); configuration = configManager.GetConfigurationAsync().Result; - TestUtilities.SetField(configManager, "_lastRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); + TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); configManager.MetadataAddress = "IssuerMetadata2.json"; configManager.RequestRefresh(); - configuration2 = configManager.GetConfigurationAsync().Result; - if (IdentityComparer.AreEqual(configuration.Issuer, configuration2.Issuer)) - context.Diffs.Add("IdentityComparer.AreEqual(configuration, configuration2)"); + + // Wait for the refresh to complete. + await Task.Delay(250).ContinueWith(_ => + { + + configuration2 = configManager.GetConfigurationAsync().GetAwaiter().GetResult(); + if (IdentityComparer.AreEqual(configuration.Issuer, configuration2.Issuer)) + context.Diffs.Add("IdentityComparer.AreEqual(configuration.Issuer, configuration2.Issuer)"); + }); TestUtilities.AssertFailIfErrors(context); } diff --git a/test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs b/test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs new file mode 100644 index 0000000000..1a1f81fe6f --- /dev/null +++ b/test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.IdentityModel.TestUtils +{ + /// + /// Returns a string set in the constructor. + /// Simplifies testing. + /// + public class InMemoryDocumentRetriever : IDocumentRetriever + { + private readonly IDictionary _configurations; + + /// + /// Initializes a new instance of the class. + /// + public InMemoryDocumentRetriever(IDictionary configuration) + { + _configurations = configuration; + } + + /// + /// Returns the document passed in constructor in dictionary./> + /// + /// Fully qualified path to a file. Ignored for now. + /// Ignored for now. + /// UTF8 decoding of bytes in the file. + public async Task GetDocumentAsync(string address, CancellationToken cancel) + { + return await Task.FromResult(_configurations[address]).ConfigureAwait(false); + } + } +} diff --git a/test/Microsoft.IdentityModel.TestUtils/SampleListener.cs b/test/Microsoft.IdentityModel.TestUtils/SampleListener.cs index dd02c50729..76dee7c906 100644 --- a/test/Microsoft.IdentityModel.TestUtils/SampleListener.cs +++ b/test/Microsoft.IdentityModel.TestUtils/SampleListener.cs @@ -8,7 +8,7 @@ namespace Microsoft.IdentityModel.TestUtils { public class SampleListener : EventListener { - public string TraceBuffer { get; set; } + public string TraceBuffer { get; set; } = string.Empty; protected override void OnEventWritten(EventWrittenEventArgs eventData) {