Skip to content

Commit

Permalink
Merge pull request #4 from reecerussell/add-caching
Browse files Browse the repository at this point in the history
Added support for caching token verification results
  • Loading branch information
reecerussell authored Jun 19, 2023
2 parents 132c23e + f7d7fe4 commit 9132c38
Show file tree
Hide file tree
Showing 17 changed files with 505 additions and 25 deletions.
25 changes: 25 additions & 0 deletions Rusty.Jwt.Abstractions/Caching/ITokenCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Rusty.Jwt.Caching;

/// <summary>
/// Used to cache key token verification data, to ease the load on third-part APIs.
/// </summary>
public interface ITokenCache
{
/// <summary>
/// Used to retrieve a cached token value, containing the verification result.
/// </summary>
/// <param name="token">The token to fetch the result of.</param>
/// <param name="cancellationToken">A token used to cancel the operation.</param>
/// <returns>
/// Returns a cached token value containing the verification result.
/// </returns>
Task<TokenCacheValue?> GetAsync(string token, CancellationToken cancellationToken);

/// <summary>
/// Used to set a token value in the cache.
/// </summary>
/// <param name="token">The token to cache a value of.</param>
/// <param name="value">The value to cache.</param>
/// <param name="cancellationToken">A token used to cancel the operation.</param>
Task SetAsync(string token, TokenCacheValue value, CancellationToken cancellationToken);
}
43 changes: 43 additions & 0 deletions Rusty.Jwt.Abstractions/Caching/InMemoryTokenCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Collections.Concurrent;

namespace Rusty.Jwt.Caching;

public class InMemoryTokenCache : ITokenCache
{
private readonly ISystemClock _systemClock;
private readonly ConcurrentDictionary<string, TokenCacheValue> _items;

public InMemoryTokenCache(ISystemClock systemClock)
{
_systemClock = systemClock;
_items = new ConcurrentDictionary<string, TokenCacheValue>();
}

public Task<TokenCacheValue?> GetAsync(string token, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

if (!_items.TryGetValue(token, out var result))
{
return Task.FromResult<TokenCacheValue?>(null);
}

if (result.Expiry < _systemClock.Now)
{
_ = _items.TryRemove(token, out _);

return Task.FromResult<TokenCacheValue?>(null);
}

return Task.FromResult<TokenCacheValue?>(result);
}

public Task SetAsync(string token, TokenCacheValue value, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

_ = _items.TryAdd(token, value);

return Task.CompletedTask;
}
}
7 changes: 7 additions & 0 deletions Rusty.Jwt.Abstractions/Caching/TokenCacheValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Rusty.Jwt.Caching;

public record struct TokenCacheValue
{
public bool Valid { get; set; }
public DateTime Expiry { get; set; }
}
12 changes: 12 additions & 0 deletions Rusty.Jwt.Abstractions/ISystemClock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Rusty.Jwt;

/// <summary>
/// Used to abstract logic to get the current time.
/// </summary>
public interface ISystemClock
{
/// <summary>
/// Gets the current date time.
/// </summary>
DateTime Now { get; }
}
2 changes: 1 addition & 1 deletion Rusty.Jwt.Abstractions/Rusty.Jwt.Abstractions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<RepositoryUrl>https://github.com/reecerussell/rusty-jwt.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>jwt</PackageTags>
<PackageVersion>1.0.2</PackageVersion>
<PackageVersion>1.1.0</PackageVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 0 additions & 1 deletion Rusty.Jwt.Azure.Tests/AzureEndToEndTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Azure.Identity;
using Base64Extensions;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
Expand Down
142 changes: 142 additions & 0 deletions Rusty.Jwt.Tests/Caching/InMemoryTokenCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System.Collections.Concurrent;
using System.Reflection;
using FluentAssertions;
using Moq;
using Rusty.Jwt.Caching;

namespace Rusty.Jwt.Tests.Caching;

public class InMemoryTokenCacheTests
{
[Fact]
public async Task GetAsync_WhereCacheHits_ReturnsResult()
{
var now = DateTime.UtcNow;
var token = "320742304";
var value = new TokenCacheValue
{
Valid = true,
Expiry = now.AddMinutes(5)
};
var cancellationToken = new CancellationToken();

var clock = new Mock<ISystemClock>();
clock.SetupGet(x => x.Now).Returns(now);

var cache = new InMemoryTokenCache(clock.Object);

var items = new ConcurrentDictionary<string, TokenCacheValue>
{
[token] = value
};
typeof(InMemoryTokenCache).GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(cache, items);

var result = await cache.GetAsync(token, cancellationToken);
result.Should().Be(value);
}

[Fact]
public async Task GetAsync_WhereCacheMisses_ReturnsNull()
{
var now = DateTime.UtcNow;
var token = "320742304";
var cancellationToken = new CancellationToken();

var clock = new Mock<ISystemClock>();
clock.SetupGet(x => x.Now).Returns(now);

var cache = new InMemoryTokenCache(clock.Object);

var result = await cache.GetAsync(token, cancellationToken);
result.Should().BeNull();
}

[Fact]
public async Task GetAsync_WhereCacheHitsByValueHasExpired_ReturnsNull()
{
var now = DateTime.UtcNow;
var token = "320742304";
var value = new TokenCacheValue
{
Valid = true,
Expiry = now.AddMinutes(-5)
};
var cancellationToken = new CancellationToken();

var clock = new Mock<ISystemClock>();
clock.SetupGet(x => x.Now).Returns(now);

var cache = new InMemoryTokenCache(clock.Object);

var items = new ConcurrentDictionary<string, TokenCacheValue>
{
[token] = value
};
typeof(InMemoryTokenCache).GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(cache, items);

var result = await cache.GetAsync(token, cancellationToken);
result.Should().BeNull();

// Item has been cleared from cache.
items.Count.Should().Be(0);
}

[Fact]
public async Task GetAsync_WhereCancellationHasBeenRequested_Throws()
{
var token = "320742304";
var cancellationToken = new CancellationToken(true);

var clock = new Mock<ISystemClock>();
var cache = new InMemoryTokenCache(clock.Object);

await Assert.ThrowsAsync<OperationCanceledException>(
() => cache.GetAsync(token, cancellationToken));
}

[Fact]
public async Task SetAsync_GivenValidInput_SetsInCache()
{
var now = DateTime.UtcNow;
var token = "320742304";
var value = new TokenCacheValue
{
Valid = true,
Expiry = now.AddMinutes(-5)
};
var cancellationToken = new CancellationToken();

var clock = new Mock<ISystemClock>();
var cache = new InMemoryTokenCache(clock.Object);

await cache.SetAsync(token, value, cancellationToken);

var items = (ConcurrentDictionary<string, TokenCacheValue>)typeof(InMemoryTokenCache)
.GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(cache)!;

items.Count.Should().Be(1);
items[token].Should().Be(value);
}

[Fact]
public async Task SetAsync_WhereCancellationHasBeenRequested_Throws()
{
var now = DateTime.UtcNow;
var token = "320742304";
var value = new TokenCacheValue
{
Valid = true,
Expiry = now.AddMinutes(-5)
};
var cancellationToken = new CancellationToken(true);

var clock = new Mock<ISystemClock>();
var cache = new InMemoryTokenCache(clock.Object);

await Assert.ThrowsAsync<OperationCanceledException>(
() => cache.SetAsync(token, value, cancellationToken));
}
}
25 changes: 25 additions & 0 deletions Rusty.Jwt.Tests/Caching/NoopTokenCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using FluentAssertions;
using Rusty.Jwt.Caching;

namespace Rusty.Jwt.Tests.Caching;

public class NoopTokenCacheTests
{
[Fact]
public async Task GetAsync_GivenAnyInput_ReturnsNull()
{
var cache = new NoopTokenCache();
var result = await cache.GetAsync("token", new CancellationToken());

result.Should().BeNull();
}

[Fact]
public void SetAsync_GivenAnyInput_ReturnsCompletedTask()
{
var cache = new NoopTokenCache();
var task = cache.SetAsync("token", new TokenCacheValue(), new CancellationToken());

task.IsCompleted.Should().BeTrue();
}
}
3 changes: 2 additions & 1 deletion Rusty.Jwt.Tests/HmacEndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public async Task InitializeAsync()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddRustyJwt()
.AddHmacSigningKey("foo", HashAlgorithm.SHA256, "Key1", SigningKeyMode.SignAndVerify);
.AddHmacSigningKey("foo", HashAlgorithm.SHA256, "Key1", SigningKeyMode.SignAndVerify)
.UseInMemoryTokenCache();

var services = serviceCollection.BuildServiceProvider();

Expand Down
Loading

0 comments on commit 9132c38

Please sign in to comment.