diff --git a/src/Microsoft.Identity.Web.OWIN/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OWIN/InternalAPI.Unshipped.txt index e69de29bb..3cd33f600 100644 --- a/src/Microsoft.Identity.Web.OWIN/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.OWIN/InternalAPI.Unshipped.txt @@ -0,0 +1 @@ +Microsoft.Identity.Web.OpenIdConnectCachingSecurityTokenProvider.OpenIdConnectCachingSecurityTokenProvider(Microsoft.IdentityModel.Protocols.ConfigurationManager! configManager, System.TimeProvider! timeProvider) -> void \ No newline at end of file diff --git a/src/Microsoft.Identity.Web.OWIN/OpenIdConnectCachingSecurityTokenProvider.cs b/src/Microsoft.Identity.Web.OWIN/OpenIdConnectCachingSecurityTokenProvider.cs index 10950971b..8955123d9 100644 --- a/src/Microsoft.Identity.Web.OWIN/OpenIdConnectCachingSecurityTokenProvider.cs +++ b/src/Microsoft.Identity.Web.OWIN/OpenIdConnectCachingSecurityTokenProvider.cs @@ -1,13 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using Microsoft.Owin.Security.Jwt; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; namespace Microsoft.Identity.Web { @@ -15,19 +16,27 @@ namespace Microsoft.Identity.Web // the OpenID Connect metadata endpoint exposed by the STS by default. internal class OpenIdConnectCachingSecurityTokenProvider : IIssuerSecurityKeyProvider { + private readonly TimeProvider _timeProvider; public ConfigurationManager _configManager; private string? _issuer; - private IEnumerable? _keys; - private readonly string _metadataEndpoint; + private ICollection? _keys; + private DateTimeOffset _syncAfter; - private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim(); + private readonly ReaderWriterLockSlim _syncLock = new(); + + private const int IdleState = 0; + private const int RunningState = 1; + private int _state = IdleState; public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint) + : this(new ConfigurationManager(metadataEndpoint, new OpenIdConnectConfigurationRetriever()), TimeProvider.System) { } + + public OpenIdConnectCachingSecurityTokenProvider(ConfigurationManager configManager, TimeProvider timeProvider) { - _metadataEndpoint = metadataEndpoint; - _configManager = new ConfigurationManager(metadataEndpoint, new OpenIdConnectConfigurationRetriever()); + _timeProvider = timeProvider; + _configManager = configManager; - RetrieveMetadata(); + RetrieveValues(); } /// @@ -36,22 +45,7 @@ public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint) /// /// The issuer the credentials are for. /// - public string? Issuer - { - get - { - RetrieveMetadata(); - _synclock.EnterReadLock(); - try - { - return _issuer; - } - finally - { - _synclock.ExitReadLock(); - } - } - } + public string? Issuer => RetrieveValues().Issuer; /// /// Gets all known security keys. @@ -59,38 +53,43 @@ public string? Issuer /// /// All known security keys. /// - public IEnumerable? SecurityKeys - { - get - { - RetrieveMetadata(); - _synclock.EnterReadLock(); - try - { - return _keys; - } - finally - { - _synclock.ExitReadLock(); - } - } - } + public IEnumerable? SecurityKeys => RetrieveValues().Keys; - private void RetrieveMetadata() + private (string? Issuer, ICollection? Keys) RetrieveValues() { - _synclock.EnterWriteLock(); - try + _syncLock.EnterReadLock(); + string? issuer = _issuer; + ICollection? keys = _keys; + DateTimeOffset syncAfter = _syncAfter; + _syncLock.ExitReadLock(); + + // Check if it's time to refresh the stored issuer and keys + if (syncAfter < _timeProvider.GetUtcNow()) { + // Acquire lock to retrieve new metadata + if (Interlocked.CompareExchange(ref _state, RunningState, IdleState) == IdleState) + { + try + { #pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - OpenIdConnectConfiguration config = Task.Run(_configManager.GetConfigurationAsync).Result; + OpenIdConnectConfiguration config = Task.Run(_configManager.GetConfigurationAsync).Result; #pragma warning restore VSTHRD002 // Avoid problematic synchronous waits - _issuer = config.Issuer; - _keys = config.SigningKeys; - } - finally - { - _synclock.ExitWriteLock(); + // Acquire write lock to update stored values + _syncLock.EnterWriteLock(); + issuer = _issuer = config.Issuer; + keys = _keys = config.SigningKeys; + // Metadata will refresh after AutomaticRefreshInterval + jitter, so refresh 1 hour after to account for jitter + _syncAfter = _timeProvider.GetUtcNow() + _configManager.AutomaticRefreshInterval + TimeSpan.FromHours(1); + _syncLock.ExitWriteLock(); + } + finally + { + Interlocked.Exchange(ref _state, IdleState); + } + } } + + return (issuer, keys); } } }