Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xiao/LKG configuration cache #2007

Merged
merged 1 commit into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
brentschmaltz marked this conversation as resolved.
Show resolved Hide resolved

if (tokenValidationResult.IsValid)
return tokenValidationResult;
foreach (BaseConfiguration lkgConfiguration in validationParameters.ConfigurationManager.GetValidLkgConfigurations())
{
if (!lkgConfiguration.Equals(currentConfiguration) && TokenUtilities.IsRecoverableConfiguration(jsonWebToken.Kid, currentConfiguration, lkgConfiguration, recoverableException))
brentschmaltz marked this conversation as resolved.
Show resolved Hide resolved
{
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