-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from reecerussell/add-caching
Added support for caching token verification results
- Loading branch information
Showing
17 changed files
with
505 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.