mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Merge pull request #2299 from DuendeSoftware/jmdc/hybrid
Use hybridcache for replay detection in JwtBearer Extensions
This commit is contained in:
commit
79eeec9e22
7 changed files with 41 additions and 15 deletions
|
|
@ -9,6 +9,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||
<PackageReference Include="Duende.IdentityModel" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -387,8 +387,7 @@ internal class DPoPProofValidator : IDPoPProofValidator
|
|||
// longer than the likelihood of proof token expiration, which is done before replay
|
||||
skew *= 2;
|
||||
var cacheDuration = dPoPOptions.ProofTokenValidityDuration + skew;
|
||||
var expiration = TimeProvider.GetUtcNow().Add(cacheDuration);
|
||||
await ReplayCache.Add(result.TokenIdHash!, expiration, cancellationToken);
|
||||
await ReplayCache.Add(result.TokenIdHash!, cacheDuration, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public interface IReplayCache
|
|||
/// <summary>
|
||||
/// Adds a hashed jti to the cache.
|
||||
/// </summary>
|
||||
Task Add(string jtiHash, DateTimeOffset expiration, CancellationToken cancellationToken = default);
|
||||
Task Add(string jtiHash, TimeSpan expiration, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,35 +1,61 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
|
||||
namespace Duende.AspNetCore.Authentication.JwtBearer.DPoP;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of the replay cache using IDistributedCache
|
||||
/// Default implementation of the replay cache using Hybrid Cache
|
||||
/// </summary>
|
||||
internal class ReplayCache : IReplayCache
|
||||
{
|
||||
private const string Prefix = "DPoPJwtBearerEvents-DPoPReplay-jti-";
|
||||
|
||||
private readonly IDistributedCache _cache;
|
||||
private readonly HybridCache _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs new instances of <see cref="ReplayCache"/>.
|
||||
/// </summary>
|
||||
public ReplayCache(IDistributedCache cache) => _cache = cache;
|
||||
public ReplayCache(HybridCache cache) => _cache = cache;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Add(string handle, DateTimeOffset expiration, CancellationToken cancellationToken)
|
||||
public async Task Add(string handle, TimeSpan expiration, CancellationToken ct)
|
||||
{
|
||||
var options = new DistributedCacheEntryOptions
|
||||
var options = new HybridCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpiration = expiration
|
||||
Expiration = expiration
|
||||
};
|
||||
|
||||
await _cache.SetAsync(Prefix + handle, [], options, cancellationToken);
|
||||
await _cache.SetAsync(Prefix + handle, true, options, cancellationToken: ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Exists(string handle, CancellationToken cancellationToken) => await _cache.GetAsync(Prefix + handle, cancellationToken) != null;
|
||||
public async Task<bool> Exists(string handle, CancellationToken ct) =>
|
||||
(await _cache.GetOrDefaultAsync<bool?>(Prefix + handle, ct)) != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for HybridCache. This is needed because HybridCache does not have a GetOrDefaultAsync method.
|
||||
/// https://github.com/dotnet/extensions/issues/5688#issuecomment-2692247434
|
||||
/// </summary>
|
||||
internal static class HybridCacheExtensions
|
||||
{
|
||||
private static readonly HybridCacheEntryOptions ReadOnlyEntryOptions = new()
|
||||
{
|
||||
Flags = HybridCacheEntryFlags.DisableLocalCacheWrite
|
||||
| HybridCacheEntryFlags.DisableDistributedCacheWrite
|
||||
| HybridCacheEntryFlags.DisableUnderlyingData
|
||||
};
|
||||
|
||||
extension(HybridCache cache)
|
||||
{
|
||||
internal async ValueTask<T?> GetOrDefaultAsync<T>(string key, CancellationToken ct = default) =>
|
||||
await cache.GetOrCreateAsync<T?>(
|
||||
key,
|
||||
// The factory will never be invoked because the ReadOnlyEntryOptions set the DisableUnderlyingData flag
|
||||
cancel => throw new InvalidOperationException("Can't Happen"),
|
||||
ReadOnlyEntryOptions,
|
||||
cancellationToken: ct);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,5 @@ public static class AssertionExtensions
|
|||
result.Error.ShouldBe(OidcConstants.TokenErrors.InvalidDPoPProof);
|
||||
}
|
||||
|
||||
public static void ReplayCacheShouldNotBeCalled(this TestDPoPProofValidator validator) => validator.TestReplayCache.DidNotReceive().Add(Arg.Any<string>(), Arg.Any<DateTimeOffset>());
|
||||
public static void ReplayCacheShouldNotBeCalled(this TestDPoPProofValidator validator) => validator.TestReplayCache.DidNotReceive().Add(Arg.Any<string>(), Arg.Any<TimeSpan>());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,7 @@ public class ReplayTests : DPoPProofValidatorTestBase
|
|||
var skew = validateIat && validateNonce
|
||||
? Math.Max(clientClockSkew, serverClockSkew)
|
||||
: (validateIat ? clientClockSkew : serverClockSkew);
|
||||
var expectedExpiration = ProofValidator.TestTimeProvider.GetUtcNow()
|
||||
.Add(TimeSpan.FromSeconds(skew * 2))
|
||||
var expectedExpiration = TimeSpan.FromSeconds(skew * 2)
|
||||
.Add(TimeSpan.FromSeconds(ValidFor));
|
||||
await ReplayCache.Received().Add(TokenIdHash, expectedExpiration);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public class ApiHost : GenericHost
|
|||
|
||||
private void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHybridCache();
|
||||
services.AddRouting();
|
||||
services.AddAuthorization();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue