Skip to content
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
11 changes: 7 additions & 4 deletions src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,24 @@ namespace NetDevPack.Security.Jwt.Core.DefaultStore;

internal class InMemoryStore : IJsonWebKeyStore
{
internal const string DefaultRevocationReason = "Revoked";
private static readonly List<KeyMaterial> _store = new();
private readonly SemaphoreSlim _slim = new(1);
internal const string DefaultRevocationReason = "Revoked";

public Task Store(KeyMaterial keyMaterial)
{
if (keyMaterial is null) throw new InvalidOperationException("Can't store empty value.");

_slim.Wait();
_store.Add(keyMaterial);
_slim.Release();

return Task.CompletedTask;
}

public Task<KeyMaterial> GetCurrent(JwtKeyType jwtKeyType)
public Task<KeyMaterial> GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws)
{
return Task.FromResult(_store.OrderByDescending(s => s.CreationDate).FirstOrDefault());
return Task.FromResult(_store.Where(s => s.Use == (jwtKeyType == JwtKeyType.Jws ? "sig" : "enc")).OrderByDescending(s => s.CreationDate).FirstOrDefault());
}

public async Task Revoke(KeyMaterial keyMaterial, string reason = null)
Expand All @@ -41,7 +44,7 @@ public async Task Revoke(KeyMaterial keyMaterial, string reason = null)
}
}

public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity, JwtKeyType? jwtKeyType)
public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity, JwtKeyType? jwtKeyType = null)
{
return Task.FromResult(
_store
Expand Down
9 changes: 1 addition & 8 deletions src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,7 @@ public async Task<EncryptingCredentials> GetCurrentEncryptingCredentials()

public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int? i = null)
{
JwtKeyType? jwtKeyType = null;

if (_options.Value.ExposedKeyType == JwtType.Jws)
jwtKeyType = JwtKeyType.Jws;
else if (_options.Value.ExposedKeyType == JwtType.Jwe)
jwtKeyType = JwtKeyType.Jwe;

return _store.GetLastKeys(_options.Value.AlgorithmsToKeep, jwtKeyType);
return _store.GetLastKeys(_options.Value.AlgorithmsToKeep, null);
}

public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int i, JwtKeyType jwtKeyType)
Expand Down
1 change: 0 additions & 1 deletion src/NetDevPack.Security.Jwt.Core/JwtOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ public class JwtOptions
public string KeyPrefix { get; set; } = $"{Environment.MachineName}_";
public int AlgorithmsToKeep { get; set; } = 2;
public TimeSpan CacheTime { get; set; } = TimeSpan.FromMinutes(15);
public JwtType ExposedKeyType { get; set; } = JwtType.Jws;
}
2 changes: 1 addition & 1 deletion src/NetDevPack.Security.Jwt.Core/Model/KeyMaterial.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public JsonWebKey GetJsonWebKey()
};

jsonWebKey.Use = Algorithm.CryptographyType == CryptographyType.DigitalSignature ? "sig" : "enc";
jsonWebKey.Alg = Algorithm.Alg; // Assure-toi que `Algorithm.Name` contient l'algorithme correct
jsonWebKey.Alg = Algorithm.Alg;

return jsonWebKey;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal class DatabaseJsonWebKeyStore<TContext> : IJsonWebKeyStore
private readonly IOptions<JwtOptions> _options;
private readonly IMemoryCache _memoryCache;
private readonly ILogger<DatabaseJsonWebKeyStore<TContext>> _logger;
internal const string DefaultRevocationReason = "Revoked";

public DatabaseJsonWebKeyStore(TContext context, ILogger<DatabaseJsonWebKeyStore<TContext>> logger, IOptions<JwtOptions> options, IMemoryCache memoryCache)
{
Expand All @@ -46,10 +47,11 @@ public async Task<KeyMaterial> GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws

if (!_memoryCache.TryGetValue(cacheKey, out KeyMaterial credentials))
{
var keyType = (jwtKeyType == JwtKeyType.Jws ? "sig" : "enc");
#if NET5_0_OR_GREATER
credentials = await _context.SecurityKeys.Where(X => X.IsRevoked == false).OrderByDescending(d => d.CreationDate).AsNoTrackingWithIdentityResolution().FirstOrDefaultAsync();
credentials = await _context.SecurityKeys.Where(X => X.IsRevoked == false).Where(s => s.Use == keyType).OrderByDescending(d => d.CreationDate).AsNoTrackingWithIdentityResolution().FirstOrDefaultAsync();
#else
credentials = await _context.SecurityKeys.Where(X => X.IsRevoked == false).OrderByDescending(d => d.CreationDate).AsNoTracking().FirstOrDefaultAsync();
credentials = await _context.SecurityKeys.Where(X => X.IsRevoked == false).Where(s => s.Use == keyType).OrderByDescending(d => d.CreationDate).AsNoTracking().FirstOrDefaultAsync();
#endif

// Set cache options.
Expand All @@ -73,9 +75,11 @@ public async Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity = 5,
if (!_memoryCache.TryGetValue(cacheKey, out ReadOnlyCollection<KeyMaterial> keys))
{
#if NET5_0_OR_GREATER
keys = _context.SecurityKeys.OrderByDescending(d => d.CreationDate).Take(quantity).AsNoTrackingWithIdentityResolution().ToList().AsReadOnly();
keys = (await _context.SecurityKeys.Where(s => jwtKeyType == null || s.Use == (jwtKeyType == JwtKeyType.Jws ? "sig" : "enc"))
.OrderByDescending(d => d.CreationDate).AsNoTrackingWithIdentityResolution().ToListAsync()).AsReadOnly();
#else
keys = _context.SecurityKeys.OrderByDescending(d => d.CreationDate).Take(quantity).AsNoTracking().ToList().AsReadOnly();
keys = _context.SecurityKeys.Where(s => jwtKeyType == null || s.Use == (jwtKeyType == JwtKeyType.Jws ? "sig" : "enc"))
.OrderByDescending(d => d.CreationDate).AsNoTracking().ToList().AsReadOnly();
#endif
// Set cache options.
var cacheEntryOptions = new MemoryCacheEntryOptions()
Expand All @@ -84,11 +88,11 @@ public async Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity = 5,

if (keys.Any())
_memoryCache.Set(cacheKey, keys, cacheEntryOptions);

return keys;
}

return keys;
return keys.GroupBy(s => s.Use)
.SelectMany(g => g.Take(quantity))
.ToList().AsReadOnly();
}

public Task<KeyMaterial> Get(string keyId)
Expand All @@ -113,7 +117,7 @@ public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = nul
if (securityKeyWithPrivate == null)
return;

securityKeyWithPrivate.Revoke(reason);
securityKeyWithPrivate.Revoke(reason ?? DefaultRevocationReason);
_context.Attach(securityKeyWithPrivate);
_context.SecurityKeys.Update(securityKeyWithPrivate);
await _context.SaveChangesAsync();
Expand Down
41 changes: 25 additions & 16 deletions src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace NetDevPack.Security.Jwt.Store.FileSystem
{
public class FileSystemStore : IJsonWebKeyStore
{
internal const string DefaultRevocationReason = "Revoked";
private readonly IOptions<JwtOptions> _options;
private readonly IMemoryCache _memoryCache;
public DirectoryInfo KeysPath { get; }
Expand All @@ -25,39 +26,43 @@ public FileSystemStore(DirectoryInfo keysPath, IOptions<JwtOptions> options, IMe
KeysPath.Create();
}

private string GetCurrentFile()
private string GetCurrentFile(JwtKeyType jwtKeyType)
{
var files = Directory.GetFiles(KeysPath.FullName, $"*current*.key");
var files = Directory.GetFiles(KeysPath.FullName, $"*current*.{jwtKeyType}.key");
if (files.Any())
return Path.Combine(KeysPath.FullName, files.First());
return files.First();

return Path.Combine(KeysPath.FullName, $"{_options.Value.KeyPrefix}current.key");
return Path.Combine(KeysPath.FullName, $"{_options.Value.KeyPrefix}current.{jwtKeyType}.key");
}

public async Task Store(KeyMaterial securityParamteres)
{
if (!KeysPath.Exists)
KeysPath.Create();

JwtKeyType keyType = securityParamteres.Use.Equals("enc", StringComparison.InvariantCultureIgnoreCase) ? JwtKeyType.Jwe : JwtKeyType.Jws;

// Datetime it's just to be easy searchable.
if (File.Exists(GetCurrentFile()))
File.Copy(GetCurrentFile(), Path.Combine(KeysPath.FullName, $"{_options.Value.KeyPrefix}old-{DateTime.Now:yyyy-MM-dd}-{securityParamteres.KeyId}.key"));
if (File.Exists(GetCurrentFile(keyType)))
File.Copy(GetCurrentFile(keyType), Path.Combine(KeysPath.FullName, $"{_options.Value.KeyPrefix}old-{DateTime.Now:yyyy-MM-dd}-{securityParamteres.KeyId}.key"));

await File.WriteAllTextAsync(Path.Combine(KeysPath.FullName, $"{_options.Value.KeyPrefix}current-{securityParamteres.KeyId}.key"), JsonSerializer.Serialize(securityParamteres, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }));
await File.WriteAllTextAsync(Path.Combine(KeysPath.FullName, $"{_options.Value.KeyPrefix}current-{securityParamteres.KeyId}.{keyType}.key"), JsonSerializer.Serialize(securityParamteres, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }));
ClearCache();
}

public bool NeedsUpdate()
public bool NeedsUpdate(KeyMaterial current)
{
return !File.Exists(GetCurrentFile()) || File.GetCreationTimeUtc(GetCurrentFile()).AddDays(_options.Value.DaysUntilExpire) < DateTime.UtcNow.Date;
JwtKeyType keyType = current.Use.Equals("enc", StringComparison.InvariantCultureIgnoreCase) ? JwtKeyType.Jwe : JwtKeyType.Jws;

return !File.Exists(GetCurrentFile(keyType)) || File.GetCreationTimeUtc(GetCurrentFile(keyType)).AddDays(_options.Value.DaysUntilExpire) < DateTime.UtcNow.Date;
}

public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = null)
{
if (securityKeyWithPrivate == null)
return;

securityKeyWithPrivate?.Revoke();
securityKeyWithPrivate?.Revoke(reason ?? DefaultRevocationReason);
foreach (var fileInfo in KeysPath.GetFiles("*.key"))
{
var key = GetKey(fileInfo.FullName);
Expand All @@ -75,7 +80,7 @@ public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = nul

if (!_memoryCache.TryGetValue(cacheKey, out KeyMaterial credentials))
{
credentials = GetKey(GetCurrentFile());
credentials = GetKey(GetCurrentFile(jwtKeyType));
// Set cache options.
var cacheEntryOptions = new MemoryCacheEntryOptions()
// Keep in cache for this time, reset time if accessed.
Expand All @@ -87,9 +92,9 @@ public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = nul
return Task.FromResult(credentials);
}

private KeyMaterial GetKey(string file)
private KeyMaterial? GetKey(string file)
{
if (!File.Exists(file)) throw new FileNotFoundException("Check configuration - cannot find auth key file: " + file);
if (!File.Exists(file)) return null;
var keyParams = JsonSerializer.Deserialize<KeyMaterial>(File.ReadAllText(file));
return keyParams!;

Expand All @@ -101,8 +106,9 @@ public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity = 5, JwtKe

if (!_memoryCache.TryGetValue(cacheKey, out IReadOnlyCollection<KeyMaterial> keys))
{
keys = KeysPath.GetFiles("*.key")
.Take(quantity)
var type = jwtKeyType == null ? "*" : jwtKeyType.ToString();

keys = KeysPath.GetFiles($"*.{type}.key")
.Select(s => s.FullName)
.Select(GetKey).ToList().AsReadOnly();

Expand All @@ -115,7 +121,10 @@ public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity = 5, JwtKe
_memoryCache.Set(cacheKey, keys, cacheEntryOptions);
}

return Task.FromResult(keys.ToList().AsReadOnly());
return Task.FromResult(keys
.GroupBy(s => s.Use)
.SelectMany(g => g.Take(quantity))
.ToList().AsReadOnly());
}

public Task<KeyMaterial?> Get(string keyId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ public JwtServiceTest(WarmupInMemoryStore warmup)
_jwksService = warmup.Services.GetRequiredService<IJwtService>();
}



[Fact]
public async Task Should_Create_New_Key()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using NetDevPack.Security.Jwt.Tests.Warmups;
using Xunit;

namespace NetDevPack.Security.Jwt.Tests.StoreTests;

[Trait("Category", "DatabaseInMemory Tests")]
public class DatabaseInMemoryStoreTests : GenericStoreServiceTest<WarmupDatabaseInMemoryStore>
{
public DatabaseInMemoryStoreTests(WarmupDatabaseInMemoryStore unifiedContext) : base(unifiedContext)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

namespace NetDevPack.Security.Jwt.Tests.StoreTests
{
//[Trait("Category", "InMemory Tests")]
//public class FileSystemStoreTests : GenericStoreServiceTest<WarmupFileStore>
//{
// public FileSystemStoreTests(WarmupFileStore unifiedContext) : base(unifiedContext)
// {
// }
//}
[Trait("Category", "InMemory Tests")]
public class FileSystemStoreTests : GenericStoreServiceTest<WarmupFileStore>
{
public FileSystemStoreTests(WarmupFileStore unifiedContext) : base(unifiedContext)
{
}

}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
Expand All @@ -23,13 +24,15 @@ public abstract class GenericStoreServiceTest<TWarmup> : IClassFixture<TWarmup>
{
private static SemaphoreSlim TestSync = new(1);
protected readonly IJsonWebKeyStore _store;
protected readonly IJwtService _jwtService;
private readonly IOptions<JwtOptions> _options;
public TWarmup WarmupData { get; }

public GenericStoreServiceTest(TWarmup warmup)
{
WarmupData = warmup;
_store = WarmupData.Services.GetRequiredService<IJsonWebKeyStore>();
_jwtService = WarmupData.Services.GetRequiredService<IJwtService>();
_options = WarmupData.Services.GetRequiredService<IOptions<JwtOptions>>();
this.WarmupData.Clear();
}
Expand Down Expand Up @@ -519,6 +522,28 @@ public async Task Should_Read_NonDefault_Revocation_Reason(string reason)
await CheckRevocationReasonIsStored(keyMaterial.KeyId, reason);
}

[Fact]
public async Task Should_Generate_Different_Keys_For_JWS_And_JWE_And_Retrieve_Them_Correctly()
{
var defaultVal = await _jwtService.GetCurrentSecurityKey();
var jwe = await _jwtService.GetCurrentSecurityKey(JwtKeyType.Jwe);
var jws = await _jwtService.GetCurrentSecurityKey(JwtKeyType.Jws);

var getLast2DefaultVal = await _jwtService.GetLastKeys(1);
var getLastJwe = (await _jwtService.GetLastKeys(1, JwtKeyType.Jwe)).First();
var getLastJws = (await _jwtService.GetLastKeys(1, JwtKeyType.Jws)).First();

jws.KeyId.Should().NotBe(jwe.KeyId);
getLastJws.KeyId.Should().NotBe(getLastJwe.KeyId);
defaultVal.KeyId.Should().Be(jws.KeyId);
jwe.KeyId.Should().Be(getLastJwe.KeyId);
jws.KeyId.Should().Be(getLastJws.KeyId);

getLast2DefaultVal.Should().HaveCount(2);
getLast2DefaultVal.Should().ContainSingle(x => x.Use == "enc");
getLast2DefaultVal.Should().ContainSingle(x => x.Use == "sig");
}

private async Task CheckRevocationReasonIsStored(string keyId, string revocationReason)
{
var dbKey = (await _store.GetLastKeys(5)).First(w => w.KeyId == keyId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

namespace NetDevPack.Security.Jwt.Tests.Warmups;

public class WarmupDatabaseInMemory : IWarmupTest
public class WarmupDatabaseInMemoryStore : IWarmupTest
{
private readonly IJsonWebKeyStore _jsonWebKeyStore;
public ServiceProvider Services { get; set; }

public WarmupDatabaseInMemory()
public WarmupDatabaseInMemoryStore()
{
var serviceCollection = new ServiceCollection();

Expand All @@ -21,11 +21,7 @@ public WarmupDatabaseInMemory()
serviceCollection.AddLogging();
serviceCollection.AddDbContext<AspNetGeneralContext>(DatabaseOptions);

serviceCollection.AddJwksManager(o =>
{
o.Jws = Algorithm.Create(AlgorithmType.AES, JwtType.Jws);
o.Jwe = Algorithm.Create(AlgorithmType.AES, JwtType.Jwe);
})
serviceCollection.AddJwksManager()
.PersistKeysToDatabaseStore<AspNetGeneralContext>();
Services = serviceCollection.BuildServiceProvider();
_jsonWebKeyStore = Services.GetRequiredService<IJsonWebKeyStore>();
Expand Down
18 changes: 16 additions & 2 deletions tests/NetDevPack.Security.Jwt.Tests/Warmups/WarmupFileStore.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NetDevPack.Security.Jwt.Core;
Expand All @@ -11,19 +12,32 @@ public class WarmupFileStore : IWarmupTest
{
private readonly IJsonWebKeyStore _jsonWebKeyStore;
public ServiceProvider Services { get; set; }
public DirectoryInfo _directoryInfo;

public WarmupFileStore()
{
_directoryInfo = TempDirectoryTest();

var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging();
serviceCollection.AddMemoryCache();
serviceCollection.AddJwksManager().PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "files")));
serviceCollection.AddJwksManager().PersistKeysToFileSystem(_directoryInfo);
Services = serviceCollection.BuildServiceProvider();
_jsonWebKeyStore = Services.GetRequiredService<IJsonWebKeyStore>();
}

public DirectoryInfo TempDirectoryTest()
{
// Créer un répertoire temporaire unique
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
return Directory.CreateDirectory(tempDir);
}

public async Task Clear()
{
await _jsonWebKeyStore.Clear();
_directoryInfo.Delete(true);
Directory.CreateDirectory(_directoryInfo.FullName);
}
}
}
Loading