diff --git a/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/AspNetCore.Authentication.JwtBearer.csproj b/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/AspNetCore.Authentication.JwtBearer.csproj
index d0d87a42a..3aa5e0817 100644
--- a/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/AspNetCore.Authentication.JwtBearer.csproj
+++ b/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/AspNetCore.Authentication.JwtBearer.csproj
@@ -9,6 +9,7 @@
+
diff --git a/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/DPoPProofValidator.cs b/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/DPoPProofValidator.cs
index d80d7a1e4..82675426a 100644
--- a/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/DPoPProofValidator.cs
+++ b/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/DPoPProofValidator.cs
@@ -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);
}
///
diff --git a/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/IReplayCache.cs b/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/IReplayCache.cs
index 96e13086b..94e244a0d 100644
--- a/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/IReplayCache.cs
+++ b/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/IReplayCache.cs
@@ -11,7 +11,7 @@ public interface IReplayCache
///
/// Adds a hashed jti to the cache.
///
- Task Add(string jtiHash, DateTimeOffset expiration, CancellationToken cancellationToken = default);
+ Task Add(string jtiHash, TimeSpan expiration, CancellationToken cancellationToken = default);
///
diff --git a/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/ReplayCache.cs b/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/ReplayCache.cs
index a0f7c32b5..d3eb2deed 100644
--- a/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/ReplayCache.cs
+++ b/aspnetcore-authentication-jwtbearer/src/AspNetCore.Authentication.JwtBearer/DPoP/ReplayCache.cs
@@ -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;
///
-/// Default implementation of the replay cache using IDistributedCache
+/// Default implementation of the replay cache using Hybrid Cache
///
internal class ReplayCache : IReplayCache
{
private const string Prefix = "DPoPJwtBearerEvents-DPoPReplay-jti-";
- private readonly IDistributedCache _cache;
+ private readonly HybridCache _cache;
///
/// Constructs new instances of .
///
- public ReplayCache(IDistributedCache cache) => _cache = cache;
+ public ReplayCache(HybridCache cache) => _cache = cache;
///
- 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);
}
///
- public async Task Exists(string handle, CancellationToken cancellationToken) => await _cache.GetAsync(Prefix + handle, cancellationToken) != null;
+ public async Task Exists(string handle, CancellationToken ct) =>
+ (await _cache.GetOrDefaultAsync(Prefix + handle, ct)) != null;
+}
+
+///
+/// Extension methods for HybridCache. This is needed because HybridCache does not have a GetOrDefaultAsync method.
+/// https://github.com/dotnet/extensions/issues/5688#issuecomment-2692247434
+///
+internal static class HybridCacheExtensions
+{
+ private static readonly HybridCacheEntryOptions ReadOnlyEntryOptions = new()
+ {
+ Flags = HybridCacheEntryFlags.DisableLocalCacheWrite
+ | HybridCacheEntryFlags.DisableDistributedCacheWrite
+ | HybridCacheEntryFlags.DisableUnderlyingData
+ };
+
+ extension(HybridCache cache)
+ {
+ internal async ValueTask GetOrDefaultAsync(string key, CancellationToken ct = default) =>
+ await cache.GetOrCreateAsync(
+ key,
+ // The factory will never be invoked because the ReadOnlyEntryOptions set the DisableUnderlyingData flag
+ cancel => throw new InvalidOperationException("Can't Happen"),
+ ReadOnlyEntryOptions,
+ cancellationToken: ct);
+ }
}
diff --git a/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/DPoP/AssertionExtensions.cs b/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/DPoP/AssertionExtensions.cs
index 6bb801b4a..e89c7030f 100644
--- a/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/DPoP/AssertionExtensions.cs
+++ b/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/DPoP/AssertionExtensions.cs
@@ -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(), Arg.Any());
+ public static void ReplayCacheShouldNotBeCalled(this TestDPoPProofValidator validator) => validator.TestReplayCache.DidNotReceive().Add(Arg.Any(), Arg.Any());
}
diff --git a/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/DPoP/ReplayTests.cs b/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/DPoP/ReplayTests.cs
index 0c6c3babe..1e217b212 100644
--- a/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/DPoP/ReplayTests.cs
+++ b/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/DPoP/ReplayTests.cs
@@ -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);
}
diff --git a/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/TestFramework/ApiHost.cs b/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/TestFramework/ApiHost.cs
index 85d1eaf93..107b74a30 100644
--- a/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/TestFramework/ApiHost.cs
+++ b/aspnetcore-authentication-jwtbearer/test/AspNetCore.Authentication.JwtBearer.Tests/TestFramework/ApiHost.cs
@@ -28,6 +28,7 @@ public class ApiHost : GenericHost
private void ConfigureServices(IServiceCollection services)
{
+ services.AddHybridCache();
services.AddRouting();
services.AddAuthorization();