Skip to content

Commit

Permalink
Introdduce a LKG configuration cache to store each valid base configu…
Browse files Browse the repository at this point in the history
…ration

instead of a single entry of configuration.
  • Loading branch information
ciaozhang authored and Brent Schmaltz committed Mar 29, 2023
1 parent 4fddab7 commit 9e9a05d
Show file tree
Hide file tree
Showing 28 changed files with 698 additions and 701 deletions.
27 changes: 15 additions & 12 deletions src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1230,8 +1230,6 @@ private async Task<TokenValidationResult> ValidateTokenAsync(JsonWebToken jsonWe

return tokenValidationResult;
}
// using 'GetType()' instead of 'is' as SecurityTokenUnableToValidException (and others) extend SecurityTokenInvalidSignatureException
// we want to make sure that the clause for SecurityTokenUnableToValidateException is hit so that the ValidationFailure is checked
else if (TokenUtilities.IsRecoverableException(tokenValidationResult.Exception))
{
// If we were still unable to validate, attempt to refresh the configuration and validate using it
Expand All @@ -1258,14 +1256,22 @@ private async Task<TokenValidationResult> ValidateTokenAsync(JsonWebToken jsonWe
}
}

if (TokenUtilities.IsRecoverableConfiguration(validationParameters, currentConfiguration, out currentConfiguration))
if (validationParameters.ConfigurationManager.UseLastKnownGoodConfiguration)
{
validationParameters.RefreshBeforeValidation = false;
validationParameters.ValidateWithLKG = true;
tokenValidationResult = ValidateToken(jsonWebToken, validationParameters, currentConfiguration);
var recoverableException = tokenValidationResult.Exception;

if (tokenValidationResult.IsValid)
return tokenValidationResult;
foreach (BaseConfiguration lkgConfiguration in validationParameters.ConfigurationManager.GetValidLkgConfigurations())
{
if (!lkgConfiguration.Equals(currentConfiguration) && TokenUtilities.IsRecoverableConfiguration(jsonWebToken.Kid, currentConfiguration, lkgConfiguration, recoverableException))
{
tokenValidationResult = ValidateToken(jsonWebToken, validationParameters, lkgConfiguration);

if (tokenValidationResult.IsValid)
return tokenValidationResult;
}
}
}
}
}
Expand Down Expand Up @@ -1534,16 +1540,13 @@ private static JsonWebToken ValidateSignature(JsonWebToken jwtToken, TokenValida

if (!validationParameters.ValidateSignatureLast)
{
InternalValidators.ValidateLifetimeAndIssuerAfterSignatureNotValidatedJwt(
InternalValidators.ValidateAfterSignatureFailed(
jwtToken,
notBefore,
expires,
jwtToken.Kid,
jwtToken.Audiences,
validationParameters,
configuration,
exceptionStrings,
numKeysInTokenValidationParameters,
numKeysInConfiguration);
configuration);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Protocols.Configuration;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.IdentityModel.Protocols
Expand All @@ -33,7 +34,7 @@ public class ConfigurationManager<T> : BaseConfigurationManager, IConfigurationM
/// Static initializer for a new object. Static initializers run before the first instance of the type is created.
/// </summary>
static ConfigurationManager()
{
{
}

/// <summary>
Expand All @@ -42,7 +43,7 @@ static ConfigurationManager()
/// <param name="metadataAddress">The address to obtain configuration.</param>
/// <param name="configRetriever">The <see cref="IConfigurationRetriever{T}"/></param>
public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> configRetriever)
: this(metadataAddress, configRetriever, new HttpDocumentRetriever())
: this(metadataAddress, configRetriever, new HttpDocumentRetriever(), new LastKnownGoodConfigurationCacheOptions())
{
}

Expand All @@ -53,7 +54,7 @@ public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> c
/// <param name="configRetriever">The <see cref="IConfigurationRetriever{T}"/></param>
/// <param name="httpClient">The client to use when obtaining configuration.</param>
public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> configRetriever, HttpClient httpClient)
: this(metadataAddress, configRetriever, new HttpDocumentRetriever(httpClient))
: this(metadataAddress, configRetriever, new HttpDocumentRetriever(httpClient), new LastKnownGoodConfigurationCacheOptions())
{
}

Expand All @@ -67,6 +68,22 @@ public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> c
/// <exception cref="ArgumentNullException">If 'configRetriever' is null.</exception>
/// <exception cref="ArgumentNullException">If 'docRetriever' is null.</exception>
public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> configRetriever, IDocumentRetriever docRetriever)
: this(metadataAddress, configRetriever, docRetriever, new LastKnownGoodConfigurationCacheOptions())
{
}

/// <summary>
/// Instantiates a new <see cref="ConfigurationManager{T}"/> that manages automatic and controls refreshing on configuration data.
/// </summary>
/// <param name="metadataAddress">The address to obtain configuration.</param>
/// <param name="configRetriever">The <see cref="IConfigurationRetriever{T}"/></param>
/// <param name="docRetriever">The <see cref="IDocumentRetriever"/> that reaches out to obtain the configuration.</param>
/// <param name="lkgCacheOptions">The <see cref="LastKnownGoodConfigurationCacheOptions"/></param>
/// <exception cref="ArgumentNullException">If 'metadataAddress' is null or empty.</exception>
/// <exception cref="ArgumentNullException">If 'configRetriever' is null.</exception>
/// <exception cref="ArgumentNullException">If 'docRetriever' is null.</exception>
/// <exception cref="ArgumentNullException">If 'lkgCacheOptions' is null.</exception>
public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> configRetriever, IDocumentRetriever docRetriever, LastKnownGoodConfigurationCacheOptions lkgCacheOptions)
{
if (string.IsNullOrWhiteSpace(metadataAddress))
throw LogHelper.LogArgumentNullException(nameof(metadataAddress));
Expand All @@ -77,10 +94,19 @@ public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> c
if (docRetriever == null)
throw LogHelper.LogArgumentNullException(nameof(docRetriever));

if (lkgCacheOptions == null)
throw LogHelper.LogArgumentNullException(nameof(lkgCacheOptions));

MetadataAddress = metadataAddress;
_docRetriever = docRetriever;
_configRetriever = configRetriever;
_refreshLock = new SemaphoreSlim(1);

_lastKnownGoodConfigurationCache = new EventBasedLRUCache<BaseConfiguration, DateTime>(
lkgCacheOptions.LastKnownGoodConfigurationSizeLimit,
TaskCreationOptions.None,
lkgCacheOptions.BaseConfigurationComparer,
true);
}

/// <summary>
Expand All @@ -92,7 +118,21 @@ public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> c
/// <param name="configValidator">The <see cref="IConfigurationValidator{T}"/></param>
/// <exception cref="ArgumentNullException">If 'configValidator' is null.</exception>
public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> configRetriever, IDocumentRetriever docRetriever, IConfigurationValidator<T> configValidator)
:this(metadataAddress, configRetriever, docRetriever)
: this(metadataAddress, configRetriever, docRetriever, configValidator, new LastKnownGoodConfigurationCacheOptions())
{
}

/// <summary>
/// Instantiates a new <see cref="ConfigurationManager{T}"/> with cinfiguration validator that manages automatic and controls refreshing on configuration data.
/// </summary>
/// <param name="metadataAddress">The address to obtain configuration.</param>
/// <param name="configRetriever">The <see cref="IConfigurationRetriever{T}"/></param>
/// <param name="docRetriever">The <see cref="IDocumentRetriever"/> that reaches out to obtain the configuration.</param>
/// <param name="configValidator">The <see cref="IConfigurationValidator{T}"/></param>
/// <param name="lkgCacheOptions">The <see cref="LastKnownGoodConfigurationCacheOptions"/></param>
/// <exception cref="ArgumentNullException">If 'configValidator' is null.</exception>
public ConfigurationManager(string metadataAddress, IConfigurationRetriever<T> configRetriever, IDocumentRetriever docRetriever, IConfigurationValidator<T> configValidator, LastKnownGoodConfigurationCacheOptions lkgCacheOptions)
: this(metadataAddress, configRetriever, docRetriever, lkgCacheOptions)
{
if (configValidator == null)
throw LogHelper.LogArgumentNullException(nameof(configValidator));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;

using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Logging;

namespace Microsoft.IdentityModel.Protocols.Configuration
{
/// <summary>
/// Specifies the LastKnownGoodConfigurationCacheOptions which can be used to configure the internal LKG configuration cache.
/// See <see cref="EventBasedLRUCache{TKey, TValue}"/> for more details.
/// </summary>
public class LastKnownGoodConfigurationCacheOptions
{
private IEqualityComparer<BaseConfiguration> _baseConfigurationComparer = new BaseConfigurationComparer();
private int _lastKnownGoodConfigurationSizeLimit = DefaultLastKnownGoodConfigurationSizeLimit;

/// <summary>
/// 10 is the default size limit of the cache (in number of items) for last known good configuration.
/// </summary>
public static readonly int DefaultLastKnownGoodConfigurationSizeLimit = 10;

/// <summary>
/// Gets or sets the BaseConfgiurationComparer that to compare <see cref="BaseConfiguration"/>.
/// </summary>
public IEqualityComparer<BaseConfiguration> BaseConfigurationComparer
{
get { return _baseConfigurationComparer; }
set
{
_baseConfigurationComparer = value ?? throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(value)));
}
}

/// <summary>
/// The size limit of the cache (in number of items) for last known good configuration.
/// </summary>
public int LastKnownGoodConfigurationSizeLimit
{
get { return _lastKnownGoodConfigurationSizeLimit; }
set
{
_lastKnownGoodConfigurationSizeLimit = (value > 0) ? value : throw LogHelper.LogExceptionMessage(new ArgumentOutOfRangeException(nameof(value)));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Protocols.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Threading;
Expand All @@ -28,6 +29,11 @@ public StaticConfigurationManager(T configuration)
throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(configuration), LogHelper.FormatInvariant(LogMessages.IDX20000, LogHelper.MarkAsNonPII(nameof(configuration)))));

_configuration = configuration;
_lastKnownGoodConfigurationCache = new EventBasedLRUCache<BaseConfiguration, DateTime>(
LastKnownGoodConfigurationCacheOptions.DefaultLastKnownGoodConfigurationSizeLimit,
TaskCreationOptions.None,
new BaseConfigurationComparer(),
true);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1084,14 +1084,8 @@ private SamlSecurityToken ValidateSignature(SamlSecurityToken samlToken, string
if (keyMatched)
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10514, keysAttempted, samlToken.Assertion.Signature.KeyInfo, exceptionStrings, samlToken)));

if (samlToken.Assertion.Conditions != null)
InternalValidators.ValidateLifetimeAndIssuerAfterSignatureNotValidatedSaml(
samlToken,
samlToken.Assertion.Conditions.NotBefore,
samlToken.Assertion.Conditions.NotOnOrAfter,
samlToken.Assertion.Signature.KeyInfo.ToString(),
validationParameters,
exceptionStrings);
ValidateIssuer(samlToken.Issuer, samlToken, validationParameters);
ValidateConditions(samlToken, validationParameters);
}

if (keysAttempted.Length > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -466,14 +466,8 @@ private Saml2SecurityToken ValidateSignature(Saml2SecurityToken samlToken, strin
if (keyMatched)
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10514, keysAttempted, samlToken.Assertion.Signature.KeyInfo, exceptionStrings, samlToken)));

if (samlToken.Assertion.Conditions != null)
InternalValidators.ValidateLifetimeAndIssuerAfterSignatureNotValidatedSaml(
samlToken,
samlToken.Assertion.Conditions.NotBefore,
samlToken.Assertion.Conditions.NotOnOrAfter,
samlToken.Assertion.Signature.KeyInfo.ToString(),
validationParameters,
exceptionStrings);
ValidateIssuer(samlToken.Issuer, samlToken, validationParameters);
ValidateConditions(samlToken, validationParameters);
}

if (keysAttempted.Length > 0)
Expand Down
40 changes: 40 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/BaseConfigurationComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Linq;

namespace Microsoft.IdentityModel.Tokens
{
/// <summary>
/// Comparison class for a <see cref="BaseConfiguration"/>.
/// </summary>
internal class BaseConfigurationComparer : IEqualityComparer<BaseConfiguration>
{
public bool Equals(BaseConfiguration config1, BaseConfiguration config2)
{
if (config1 == null && config2 == null)
return true;
else if (config1 == null || config2 == null)
return false;
else if (config1.Issuer == config2.Issuer && config1.SigningKeys.Count == config2.SigningKeys.Count
&& !config1.SigningKeys.Select(x => x.InternalId).Except(config2.SigningKeys.Select(x => x.InternalId)).Any())
return true;
else
return false;
}

public int GetHashCode(BaseConfiguration config)
{
int defaultHash = string.Empty.GetHashCode();
int hashCode = defaultHash;
hashCode ^= string.IsNullOrEmpty(config.Issuer) ? defaultHash : config.Issuer.GetHashCode();
foreach(string internalId in config.SigningKeys.Select(x => x.InternalId))
{
hashCode ^= internalId.GetHashCode();
}

return hashCode;
}
}
}
16 changes: 16 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/BaseConfigurationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Logging;
Expand All @@ -20,6 +22,8 @@ public abstract class BaseConfigurationManager
private BaseConfiguration _lastKnownGoodConfiguration;
private DateTime? _lastKnownGoodConfigFirstUse = null;

internal EventBasedLRUCache<BaseConfiguration, DateTime> _lastKnownGoodConfigurationCache;

/// <summary>
/// Gets or sets the <see cref="TimeSpan"/> that controls how often an automatic metadata refresh should occur.
/// </summary>
Expand Down Expand Up @@ -63,6 +67,15 @@ public virtual Task<BaseConfiguration> GetBaseConfigurationAsync(CancellationTok
throw new NotImplementedException();
}

/// <summary>
/// Gets all valid last known good configurations.
/// </summary>
/// <returns>A collection of all valid last known good configurations.</returns>
internal ICollection<BaseConfiguration> GetValidLkgConfigurations()
{
return _lastKnownGoodConfigurationCache.ToArray().Where(x => x.Value.Value > DateTime.UtcNow).Select(x => x.Key).ToArray();
}

/// <summary>
/// The last known good configuration or LKG (a configuration retrieved in the past that we were able to successfully validate a token against).
/// </summary>
Expand All @@ -76,6 +89,9 @@ public BaseConfiguration LastKnownGoodConfiguration
{
_lastKnownGoodConfiguration = value ?? throw LogHelper.LogArgumentNullException(nameof(value));
_lastKnownGoodConfigFirstUse = DateTime.UtcNow;

// LRU cache will remove the expired configuration
_lastKnownGoodConfigurationCache.SetValue(_lastKnownGoodConfiguration, DateTime.UtcNow + LastKnownGoodLifetime, DateTime.UtcNow + LastKnownGoodLifetime);
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/EventBasedLRUCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Logging;
Expand Down Expand Up @@ -471,6 +472,11 @@ private void StartEventQueueTaskIfNotRunning()
}
}

internal KeyValuePair<TKey, LRUCacheItem<TKey, TValue>>[] ToArray()
{
return _map.ToArray();
}

/// Each time a node gets accessed, it gets moved to the beginning (head) of the list if the _maintainLRU == true
public bool TryGetValue(TKey key, out TValue value)
{
Expand Down
Loading

0 comments on commit 9e9a05d

Please sign in to comment.