From 68ef7b34ecb2285bf2cd790a0380d6f202f961a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=B4t=C3=A9=2C=20Dany=20=28DGRI=29?= Date: Thu, 20 Feb 2025 23:52:56 -0500 Subject: [PATCH 1/3] feat(jwe): Adding full support Jwe in jwks store --- .../DefaultStore/DataProtectionStore.cs | 52 ++++++++++++------- .../DefaultStore/InMemoryStore.cs | 12 +++-- .../Interfaces/IJsonWebKeyStore.cs | 5 +- .../Interfaces/IJwtService.cs | 8 +-- .../Jwa/Algorithm.cs | 1 + .../Jwa/JwtKeyType.cs | 11 ++++ .../Jwt/JwtService.cs | 46 ++++++++++------ .../JwtOptions.cs | 1 + src/NetDevPack.Security.Jwt.Core/Model/Key.cs | 4 +- .../Model/KeyMaterial.cs | 22 +++++--- .../DatabaseJsonWebKeyStore.cs | 21 +++++--- .../FileSystemStore.cs | 21 +++++--- .../StoreTests/GenericStoreServiceTest.cs | 2 +- 13 files changed, 142 insertions(+), 64 deletions(-) create mode 100644 src/NetDevPack.Security.Jwt.Core/Jwa/JwtKeyType.cs diff --git a/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs b/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs index 70ae425..fcb1ad2 100644 --- a/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs +++ b/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Runtime.InteropServices; using System.Text.Json; @@ -11,6 +12,7 @@ using Microsoft.Extensions.Options; using Microsoft.Win32; using NetDevPack.Security.Jwt.Core.Interfaces; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Core.DefaultStore; @@ -52,13 +54,13 @@ public DataProtectionStore( _memoryCache = memoryCache; _dataProtector = provider.CreateProtector(nameof(KeyMaterial)); ; } - public Task Store(KeyMaterial securityParamteres) + public Task Store(KeyMaterial securityParameters) { - var possiblyEncryptedKeyElement = _dataProtector.Protect(JsonSerializer.Serialize(securityParamteres)); + var possiblyEncryptedKeyElement = _dataProtector.Protect(JsonSerializer.Serialize(securityParameters)); // build the element var keyElement = new XElement(Name, - new XAttribute(IdAttributeName, securityParamteres.Id), + new XAttribute(IdAttributeName, securityParameters.Id), new XAttribute(VersionAttributeName, 1), new XElement(CreationDateElementName, DateTimeOffset.UtcNow), new XElement(ActivationDateElementName, DateTimeOffset.UtcNow), @@ -68,7 +70,7 @@ public Task Store(KeyMaterial securityParamteres) possiblyEncryptedKeyElement)); // Persist it to the underlying repository and trigger the cancellation token. - var friendlyName = string.Format(CultureInfo.InvariantCulture, "key-{0}", securityParamteres.KeyId); + var friendlyName = string.Format(CultureInfo.InvariantCulture, "key-{0}", securityParameters.KeyId); KeyRepository.StoreElement(keyElement, friendlyName); ClearCache(); @@ -77,11 +79,13 @@ public Task Store(KeyMaterial securityParamteres) - public async Task GetCurrent() + public async Task GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - if (!_memoryCache.TryGetValue(JwkContants.CurrentJwkCache, out KeyMaterial keyMaterial)) + var cacheKey = JwkContants.CurrentJwkCache + jwtKeyType; + + if (!_memoryCache.TryGetValue(cacheKey, out KeyMaterial keyMaterial)) { - var keys = await GetLastKeys(1); + var keys = await GetLastKeys(1, jwtKeyType); keyMaterial = keys.FirstOrDefault(); // Set cache options. var cacheEntryOptions = new MemoryCacheEntryOptions() @@ -89,7 +93,7 @@ public async Task GetCurrent() .SetSlidingExpiration(_options.Value.CacheTime); if (keyMaterial != null) - _memoryCache.Set(JwkContants.CurrentJwkCache, keyMaterial, cacheEntryOptions); + _memoryCache.Set(cacheKey, keyMaterial, cacheEntryOptions); } return keyMaterial; @@ -146,10 +150,11 @@ private IReadOnlyCollection GetKeys() } - public Task> GetLastKeys(int quantity = 5) + public Task> GetLastKeys(int quantity = 5, JwtKeyType? jwtKeyType = null) { + var cacheKey = JwkContants.JwksCache + jwtKeyType; - if (!_memoryCache.TryGetValue(JwkContants.JwksCache, out IReadOnlyCollection keys)) + if (!_memoryCache.TryGetValue(cacheKey, out IReadOnlyCollection keys)) { keys = GetKeys(); @@ -159,13 +164,20 @@ public Task> GetLastKeys(int quantity = 5) .SetSlidingExpiration(_options.Value.CacheTime); if (keys.Any()) - _memoryCache.Set(JwkContants.JwksCache, keys, cacheEntryOptions); + { + keys = keys + .Where(s => jwtKeyType == null || s.Use == (jwtKeyType == JwtKeyType.Jws ? "sig" : "enc")) + .OrderByDescending(s => s.CreationDate) + .ToList().AsReadOnly(); + + _memoryCache.Set(cacheKey, keys, cacheEntryOptions); + } } return Task.FromResult(keys - .OrderByDescending(s => s.CreationDate) - .ToList() - .AsReadOnly()); + .GroupBy(s => s.Use) + .SelectMany(g => g.Take(quantity)) + .ToList().AsReadOnly()); } public Task Get(string keyId) @@ -185,10 +197,10 @@ public async Task Clear() public async Task Revoke(KeyMaterial keyMaterial, string reason = null) { - if(keyMaterial == null) + if (keyMaterial == null) return; - - var keys = await GetLastKeys(); + + var keys = await GetLastKeys(jwtKeyType: keyMaterial.Use.Equals("sig", StringComparison.InvariantCultureIgnoreCase) ? JwtKeyType.Jws : JwtKeyType.Jwe); var key = keys.First(f => f.Id == keyMaterial.Id); if (key is { IsRevoked: true }) @@ -214,7 +226,11 @@ public async Task Revoke(KeyMaterial keyMaterial, string reason = null) private void ClearCache() { _memoryCache.Remove(JwkContants.JwksCache); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jwe); _memoryCache.Remove(JwkContants.CurrentJwkCache); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jwe); } /// diff --git a/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs b/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs index 4f373a1..01d4d66 100644 --- a/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs +++ b/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using NetDevPack.Security.Jwt.Core.Interfaces; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Core.DefaultStore; @@ -18,7 +19,7 @@ public Task Store(KeyMaterial keyMaterial) return Task.CompletedTask; } - public Task GetCurrent() + public Task GetCurrent(JwtKeyType jwtKeyType) { return Task.FromResult(_store.OrderByDescending(s => s.CreationDate).FirstOrDefault()); } @@ -40,12 +41,15 @@ public async Task Revoke(KeyMaterial keyMaterial, string reason = null) } } - public Task> GetLastKeys(int quantity) + public Task> GetLastKeys(int quantity, JwtKeyType? jwtKeyType) { return Task.FromResult( _store - .OrderByDescending(s => s.CreationDate) - .Take(quantity).ToList().AsReadOnly()); + .Where(s => jwtKeyType == null || s.Use == (jwtKeyType == JwtKeyType.Jws ? "sig" : "enc")) + .OrderByDescending(s => s.CreationDate) + .GroupBy(s => s.Use) + .SelectMany(g => g.Take(quantity)) + .ToList().AsReadOnly()); } public Task Get(string keyId) diff --git a/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs b/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs index 8aacc4f..23bd3d6 100644 --- a/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs +++ b/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using Microsoft.IdentityModel.Tokens; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Core.Interfaces; @@ -7,9 +8,9 @@ namespace NetDevPack.Security.Jwt.Core.Interfaces; public interface IJsonWebKeyStore { Task Store(KeyMaterial keyMaterial); - Task GetCurrent(); + Task GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws); Task Revoke(KeyMaterial keyMaterial, string reason=default); - Task> GetLastKeys(int quantity); + Task> GetLastKeys(int quantity, JwtKeyType? jwtKeyType = null); Task Get(string keyId); Task Clear(); } \ No newline at end of file diff --git a/src/NetDevPack.Security.Jwt.Core/Interfaces/IJwtService.cs b/src/NetDevPack.Security.Jwt.Core/Interfaces/IJwtService.cs index b5ea662..270b528 100644 --- a/src/NetDevPack.Security.Jwt.Core/Interfaces/IJwtService.cs +++ b/src/NetDevPack.Security.Jwt.Core/Interfaces/IJwtService.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using Microsoft.IdentityModel.Tokens; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Core.Interfaces; @@ -11,13 +12,14 @@ public interface IJwtService /// If you want to use JWE, you must select RSA. Or use `CryptographicKey` class /// /// - Task GenerateKey(); - Task GetCurrentSecurityKey(); + Task GenerateKey(JwtKeyType jwtKeyType = JwtKeyType.Jws); + Task GetCurrentSecurityKey(JwtKeyType jwtKeyType = JwtKeyType.Jws); Task GetCurrentSigningCredentials(); Task GetCurrentEncryptingCredentials(); + Task> GetLastKeys(int i, JwtKeyType jwtKeyType); Task> GetLastKeys(int? i = null); Task RevokeKey(string keyId, string reason = null); - Task GenerateNewKey(); + Task GenerateNewKey(JwtKeyType jwtKeyType = JwtKeyType.Jws); } [Obsolete("Deprecate, use IJwtServiceInstead")] public interface IJsonWebKeySetService : IJwtService{} \ No newline at end of file diff --git a/src/NetDevPack.Security.Jwt.Core/Jwa/Algorithm.cs b/src/NetDevPack.Security.Jwt.Core/Jwa/Algorithm.cs index 097d07c..77bf802 100644 --- a/src/NetDevPack.Security.Jwt.Core/Jwa/Algorithm.cs +++ b/src/NetDevPack.Security.Jwt.Core/Jwa/Algorithm.cs @@ -58,6 +58,7 @@ private Algorithm() public AlgorithmType AlgorithmType { get; internal set; } public CryptographyType CryptographyType { get; internal set; } public JwtType JwtType => CryptographyType == CryptographyType.Encryption ? JwtType.Jwe : JwtType.Jws; + public string Use => CryptographyType == CryptographyType.Encryption ? "enc" : "sig"; public string Alg { get; internal set; } public string Curve { get; set; } diff --git a/src/NetDevPack.Security.Jwt.Core/Jwa/JwtKeyType.cs b/src/NetDevPack.Security.Jwt.Core/Jwa/JwtKeyType.cs new file mode 100644 index 0000000..58978a9 --- /dev/null +++ b/src/NetDevPack.Security.Jwt.Core/Jwa/JwtKeyType.cs @@ -0,0 +1,11 @@ +namespace NetDevPack.Security.Jwt.Core.Jwa; + +/// +/// Jws will use Digital Signatures algorithms +/// Jwe will use Encryption algorithms +/// +public enum JwtKeyType +{ + Jws = 1, + Jwe = 2 +} \ No newline at end of file diff --git a/src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs b/src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs index b3e6746..4e8938b 100644 --- a/src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs +++ b/src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using NetDevPack.Security.Jwt.Core.Interfaces; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Core.Jwt @@ -16,9 +17,9 @@ public JwtService(IJsonWebKeyStore store, IOptions options) _store = store; _options = options; } - public async Task GenerateKey() + public async Task GenerateKey(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - var key = new CryptographicKey(_options.Value.Jws); + var key = new CryptographicKey(jwtKeyType == JwtKeyType.Jws ? _options.Value.Jws : _options.Value.Jwe); var model = new KeyMaterial(key); await _store.Store(model); @@ -26,48 +27,61 @@ public async Task GenerateKey() return model.GetSecurityKey(); } - public async Task GetCurrentSecurityKey() + public async Task GetCurrentSecurityKey(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - var current = await _store.GetCurrent(); + var current = await _store.GetCurrent(jwtKeyType); if (NeedsUpdate(current)) { // According NIST - https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r4.pdf - Private key should be removed when no longer needs await _store.Revoke(current); - var newKey = await GenerateKey(); + var newKey = await GenerateKey(jwtKeyType); return newKey; } // options has change. Change current key - if (!await CheckCompatibility(current)) - current = await _store.GetCurrent(); + if (!await CheckCompatibility(current, jwtKeyType)) + current = await _store.GetCurrent(jwtKeyType); return current; } public async Task GetCurrentSigningCredentials() { - var current = await GetCurrentSecurityKey(); + var current = await GetCurrentSecurityKey(JwtKeyType.Jws); return new SigningCredentials(current, _options.Value.Jws); } public async Task GetCurrentEncryptingCredentials() { - var current = await GetCurrentSecurityKey(); + var current = await GetCurrentSecurityKey(JwtKeyType.Jwe); return new EncryptingCredentials(current, _options.Value.Jwe.Alg, _options.Value.Jwe.EncryptionAlgorithmContent); } public Task> GetLastKeys(int? i = null) { - return _store.GetLastKeys(_options.Value.AlgorithmsToKeep); + 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); + } + + public Task> GetLastKeys(int i, JwtKeyType jwtKeyType) + { + return _store.GetLastKeys(_options.Value.AlgorithmsToKeep, jwtKeyType); } - private async Task CheckCompatibility(KeyMaterial currentKey) + private async Task CheckCompatibility(KeyMaterial currentKey, JwtKeyType jwtKeyType) { - if (currentKey.Type != _options.Value.Jws.Kty()) + if (jwtKeyType == JwtKeyType.Jws && currentKey.Type != _options.Value.Jws.Kty() + || jwtKeyType == JwtKeyType.Jwe && currentKey.Type != _options.Value.Jwe.Kty()) { - await GenerateKey(); + await GenerateKey(jwtKeyType); return false; } return true; @@ -80,11 +94,11 @@ public async Task RevokeKey(string keyId, string reason = null) await _store.Revoke(key, reason); } - public async Task GenerateNewKey() + public async Task GenerateNewKey(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - var oldCurrent = await _store.GetCurrent(); + var oldCurrent = await _store.GetCurrent(jwtKeyType); await _store.Revoke(oldCurrent); - return await GenerateKey(); + return await GenerateKey(jwtKeyType); } diff --git a/src/NetDevPack.Security.Jwt.Core/JwtOptions.cs b/src/NetDevPack.Security.Jwt.Core/JwtOptions.cs index e00b40c..cddc93f 100644 --- a/src/NetDevPack.Security.Jwt.Core/JwtOptions.cs +++ b/src/NetDevPack.Security.Jwt.Core/JwtOptions.cs @@ -10,4 +10,5 @@ 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; } \ No newline at end of file diff --git a/src/NetDevPack.Security.Jwt.Core/Model/Key.cs b/src/NetDevPack.Security.Jwt.Core/Model/Key.cs index ee496cb..c8e0973 100644 --- a/src/NetDevPack.Security.Jwt.Core/Model/Key.cs +++ b/src/NetDevPack.Security.Jwt.Core/Model/Key.cs @@ -12,6 +12,7 @@ public KeyMaterial() { } public KeyMaterial(CryptographicKey cryptographicKey) { CreationDate = DateTime.UtcNow; + Use = cryptographicKey.Algorithm.Use; Parameters = JsonSerializer.Serialize(cryptographicKey.GetJsonWebKey(), typeof(JsonWebKey)); Type = cryptographicKey.Algorithm.Kty(); KeyId = cryptographicKey.Key.KeyId; @@ -20,6 +21,7 @@ public KeyMaterial(CryptographicKey cryptographicKey) public Guid Id { get; set; } = Guid.NewGuid(); public string KeyId { get; set; } public string Type { get; set; } + public string Use { get; set; } public string Parameters { get; set; } public bool IsRevoked { get; set; } public string? RevokedReason { get; set; } @@ -34,7 +36,7 @@ public JsonWebKey GetSecurityKey() public void Revoke(string reason=default) { - var jsonWebKey = GetSecurityKey(); + var jsonWebKey = GetSecurityKey(); var publicWebKey = PublicJsonWebKey.FromJwk(jsonWebKey); ExpiredAt = DateTime.UtcNow; IsRevoked = true; diff --git a/src/NetDevPack.Security.Jwt.Core/Model/KeyMaterial.cs b/src/NetDevPack.Security.Jwt.Core/Model/KeyMaterial.cs index e0f1c69..196ede2 100644 --- a/src/NetDevPack.Security.Jwt.Core/Model/KeyMaterial.cs +++ b/src/NetDevPack.Security.Jwt.Core/Model/KeyMaterial.cs @@ -23,14 +23,22 @@ public CryptographicKey(Algorithm algorithm) public Algorithm Algorithm { get; set; } public SecurityKey Key { get; set; } - public JsonWebKey GetJsonWebKey() => Algorithm.AlgorithmType switch + public JsonWebKey GetJsonWebKey() { - AlgorithmType.RSA => JsonWebKeyConverter.ConvertFromRSASecurityKey((RsaSecurityKey)Key), - AlgorithmType.ECDsa => JsonWebKeyConverter.ConvertFromECDsaSecurityKey((ECDsaSecurityKey)Key), - AlgorithmType.HMAC => JsonWebKeyConverter.ConvertFromSymmetricSecurityKey((SymmetricSecurityKey)Key), - AlgorithmType.AES => JsonWebKeyConverter.ConvertFromSymmetricSecurityKey((SymmetricSecurityKey)Key), - _ => throw new ArgumentOutOfRangeException() - }; + var jsonWebKey = Algorithm.AlgorithmType switch + { + AlgorithmType.RSA => JsonWebKeyConverter.ConvertFromRSASecurityKey((RsaSecurityKey)Key), + AlgorithmType.ECDsa => JsonWebKeyConverter.ConvertFromECDsaSecurityKey((ECDsaSecurityKey)Key), + AlgorithmType.HMAC => JsonWebKeyConverter.ConvertFromSymmetricSecurityKey((SymmetricSecurityKey)Key), + AlgorithmType.AES => JsonWebKeyConverter.ConvertFromSymmetricSecurityKey((SymmetricSecurityKey)Key), + _ => throw new ArgumentOutOfRangeException() + }; + + jsonWebKey.Use = Algorithm.CryptographyType == CryptographyType.DigitalSignature ? "sig" : "enc"; + jsonWebKey.Alg = Algorithm.Alg; // Assure-toi que `Algorithm.Name` contient l'algorithme correct + + return jsonWebKey; + } private SecurityKey GenerateRsa() { diff --git a/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs b/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs index dfa2c08..34291b4 100644 --- a/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs +++ b/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Options; using NetDevPack.Security.Jwt.Core; using NetDevPack.Security.Jwt.Core.Interfaces; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Store.EntityFrameworkCore @@ -39,9 +40,11 @@ public async Task Store(KeyMaterial securityParamteres) ClearCache(); } - public async Task GetCurrent() + public async Task GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - if (!_memoryCache.TryGetValue(JwkContants.CurrentJwkCache, out KeyMaterial credentials)) + var cacheKey = JwkContants.CurrentJwkCache + jwtKeyType; + + if (!_memoryCache.TryGetValue(cacheKey, out KeyMaterial credentials)) { #if NET5_0_OR_GREATER credentials = await _context.SecurityKeys.Where(X => X.IsRevoked == false).OrderByDescending(d => d.CreationDate).AsNoTrackingWithIdentityResolution().FirstOrDefaultAsync(); @@ -55,7 +58,7 @@ public async Task GetCurrent() .SetSlidingExpiration(_options.Value.CacheTime); if (credentials != null) - _memoryCache.Set(JwkContants.CurrentJwkCache, credentials, cacheEntryOptions); + _memoryCache.Set(cacheKey, credentials, cacheEntryOptions); return credentials; } @@ -63,9 +66,11 @@ public async Task GetCurrent() return credentials; } - public async Task> GetLastKeys(int quantity = 5) + public async Task> GetLastKeys(int quantity = 5, JwtKeyType? jwtKeyType = null) { - if (!_memoryCache.TryGetValue(JwkContants.JwksCache, out ReadOnlyCollection keys)) + var cacheKey = JwkContants.JwksCache + jwtKeyType; + + if (!_memoryCache.TryGetValue(cacheKey, out ReadOnlyCollection keys)) { #if NET5_0_OR_GREATER keys = _context.SecurityKeys.OrderByDescending(d => d.CreationDate).Take(quantity).AsNoTrackingWithIdentityResolution().ToList().AsReadOnly(); @@ -78,7 +83,7 @@ public async Task> GetLastKeys(int quantity = 5) .SetSlidingExpiration(_options.Value.CacheTime); if (keys.Any()) - _memoryCache.Set(JwkContants.JwksCache, keys, cacheEntryOptions); + _memoryCache.Set(cacheKey, keys, cacheEntryOptions); return keys; } @@ -118,7 +123,11 @@ public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = nul private void ClearCache() { _memoryCache.Remove(JwkContants.JwksCache); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jwe); _memoryCache.Remove(JwkContants.CurrentJwkCache); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jwe); } } } diff --git a/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs b/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs index 7c8a8c7..52e1c41 100644 --- a/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs +++ b/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs @@ -6,6 +6,7 @@ using NetDevPack.Security.Jwt.Core; using NetDevPack.Security.Jwt.Core.Interfaces; using NetDevPack.Security.Jwt.Core.Model; +using NetDevPack.Security.Jwt.Core.Jwa; namespace NetDevPack.Security.Jwt.Store.FileSystem { @@ -68,9 +69,11 @@ public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = nul } - public Task GetCurrent() + public Task GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - if (!_memoryCache.TryGetValue(JwkContants.CurrentJwkCache, out KeyMaterial credentials)) + var cacheKey = JwkContants.CurrentJwkCache + jwtKeyType; + + if (!_memoryCache.TryGetValue(cacheKey, out KeyMaterial credentials)) { credentials = GetKey(GetCurrentFile()); // Set cache options. @@ -78,7 +81,7 @@ public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = nul // Keep in cache for this time, reset time if accessed. .SetSlidingExpiration(_options.Value.CacheTime); if (credentials != null) - _memoryCache.Set(JwkContants.CurrentJwkCache, credentials, cacheEntryOptions); + _memoryCache.Set(cacheKey, credentials, cacheEntryOptions); } return Task.FromResult(credentials); @@ -92,9 +95,11 @@ private KeyMaterial GetKey(string file) } - public Task> GetLastKeys(int quantity = 5) + public Task> GetLastKeys(int quantity = 5, JwtKeyType? jwtKeyType = null) { - if (!_memoryCache.TryGetValue(JwkContants.JwksCache, out IReadOnlyCollection keys)) + var cacheKey = JwkContants.JwksCache + jwtKeyType; + + if (!_memoryCache.TryGetValue(cacheKey, out IReadOnlyCollection keys)) { keys = KeysPath.GetFiles("*.key") .Take(quantity) @@ -107,7 +112,7 @@ public Task> GetLastKeys(int quantity = 5) .SetSlidingExpiration(_options.Value.CacheTime); if (keys.Any()) - _memoryCache.Set(JwkContants.JwksCache, keys, cacheEntryOptions); + _memoryCache.Set(cacheKey, keys, cacheEntryOptions); } return Task.FromResult(keys.ToList().AsReadOnly()); @@ -141,7 +146,11 @@ public Task Clear() private void ClearCache() { _memoryCache.Remove(JwkContants.JwksCache); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jwe); _memoryCache.Remove(JwkContants.CurrentJwkCache); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jwe); } } } diff --git a/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs b/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs index 87909aa..8c308e8 100644 --- a/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs +++ b/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs @@ -21,7 +21,7 @@ namespace NetDevPack.Security.Jwt.Tests.StoreTests; public abstract class GenericStoreServiceTest : IClassFixture where TWarmup : class, IWarmupTest { - private static SemaphoreSlim TestSync = new(1); + private static SemaphoreSlim TestSync = new(1,1); protected readonly IJsonWebKeyStore _store; private readonly IOptions _options; public TWarmup WarmupData { get; } From 36352326d83f4418c1a6200c37654aec07678a9a Mon Sep 17 00:00:00 2001 From: anisite Date: Thu, 20 Feb 2025 23:52:56 -0500 Subject: [PATCH 2/3] feat(jwe): Adding full support Jwe in jwks store --- .../DefaultStore/DataProtectionStore.cs | 52 ++++++++++++------- .../DefaultStore/InMemoryStore.cs | 12 +++-- .../Interfaces/IJsonWebKeyStore.cs | 5 +- .../Interfaces/IJwtService.cs | 8 +-- .../Jwa/Algorithm.cs | 1 + .../Jwa/JwtKeyType.cs | 11 ++++ .../Jwt/JwtService.cs | 46 ++++++++++------ .../JwtOptions.cs | 1 + src/NetDevPack.Security.Jwt.Core/Model/Key.cs | 4 +- .../Model/KeyMaterial.cs | 22 +++++--- .../DatabaseJsonWebKeyStore.cs | 21 +++++--- .../FileSystemStore.cs | 21 +++++--- .../StoreTests/GenericStoreServiceTest.cs | 2 +- 13 files changed, 142 insertions(+), 64 deletions(-) create mode 100644 src/NetDevPack.Security.Jwt.Core/Jwa/JwtKeyType.cs diff --git a/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs b/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs index 70ae425..fcb1ad2 100644 --- a/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs +++ b/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Runtime.InteropServices; using System.Text.Json; @@ -11,6 +12,7 @@ using Microsoft.Extensions.Options; using Microsoft.Win32; using NetDevPack.Security.Jwt.Core.Interfaces; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Core.DefaultStore; @@ -52,13 +54,13 @@ public DataProtectionStore( _memoryCache = memoryCache; _dataProtector = provider.CreateProtector(nameof(KeyMaterial)); ; } - public Task Store(KeyMaterial securityParamteres) + public Task Store(KeyMaterial securityParameters) { - var possiblyEncryptedKeyElement = _dataProtector.Protect(JsonSerializer.Serialize(securityParamteres)); + var possiblyEncryptedKeyElement = _dataProtector.Protect(JsonSerializer.Serialize(securityParameters)); // build the element var keyElement = new XElement(Name, - new XAttribute(IdAttributeName, securityParamteres.Id), + new XAttribute(IdAttributeName, securityParameters.Id), new XAttribute(VersionAttributeName, 1), new XElement(CreationDateElementName, DateTimeOffset.UtcNow), new XElement(ActivationDateElementName, DateTimeOffset.UtcNow), @@ -68,7 +70,7 @@ public Task Store(KeyMaterial securityParamteres) possiblyEncryptedKeyElement)); // Persist it to the underlying repository and trigger the cancellation token. - var friendlyName = string.Format(CultureInfo.InvariantCulture, "key-{0}", securityParamteres.KeyId); + var friendlyName = string.Format(CultureInfo.InvariantCulture, "key-{0}", securityParameters.KeyId); KeyRepository.StoreElement(keyElement, friendlyName); ClearCache(); @@ -77,11 +79,13 @@ public Task Store(KeyMaterial securityParamteres) - public async Task GetCurrent() + public async Task GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - if (!_memoryCache.TryGetValue(JwkContants.CurrentJwkCache, out KeyMaterial keyMaterial)) + var cacheKey = JwkContants.CurrentJwkCache + jwtKeyType; + + if (!_memoryCache.TryGetValue(cacheKey, out KeyMaterial keyMaterial)) { - var keys = await GetLastKeys(1); + var keys = await GetLastKeys(1, jwtKeyType); keyMaterial = keys.FirstOrDefault(); // Set cache options. var cacheEntryOptions = new MemoryCacheEntryOptions() @@ -89,7 +93,7 @@ public async Task GetCurrent() .SetSlidingExpiration(_options.Value.CacheTime); if (keyMaterial != null) - _memoryCache.Set(JwkContants.CurrentJwkCache, keyMaterial, cacheEntryOptions); + _memoryCache.Set(cacheKey, keyMaterial, cacheEntryOptions); } return keyMaterial; @@ -146,10 +150,11 @@ private IReadOnlyCollection GetKeys() } - public Task> GetLastKeys(int quantity = 5) + public Task> GetLastKeys(int quantity = 5, JwtKeyType? jwtKeyType = null) { + var cacheKey = JwkContants.JwksCache + jwtKeyType; - if (!_memoryCache.TryGetValue(JwkContants.JwksCache, out IReadOnlyCollection keys)) + if (!_memoryCache.TryGetValue(cacheKey, out IReadOnlyCollection keys)) { keys = GetKeys(); @@ -159,13 +164,20 @@ public Task> GetLastKeys(int quantity = 5) .SetSlidingExpiration(_options.Value.CacheTime); if (keys.Any()) - _memoryCache.Set(JwkContants.JwksCache, keys, cacheEntryOptions); + { + keys = keys + .Where(s => jwtKeyType == null || s.Use == (jwtKeyType == JwtKeyType.Jws ? "sig" : "enc")) + .OrderByDescending(s => s.CreationDate) + .ToList().AsReadOnly(); + + _memoryCache.Set(cacheKey, keys, cacheEntryOptions); + } } return Task.FromResult(keys - .OrderByDescending(s => s.CreationDate) - .ToList() - .AsReadOnly()); + .GroupBy(s => s.Use) + .SelectMany(g => g.Take(quantity)) + .ToList().AsReadOnly()); } public Task Get(string keyId) @@ -185,10 +197,10 @@ public async Task Clear() public async Task Revoke(KeyMaterial keyMaterial, string reason = null) { - if(keyMaterial == null) + if (keyMaterial == null) return; - - var keys = await GetLastKeys(); + + var keys = await GetLastKeys(jwtKeyType: keyMaterial.Use.Equals("sig", StringComparison.InvariantCultureIgnoreCase) ? JwtKeyType.Jws : JwtKeyType.Jwe); var key = keys.First(f => f.Id == keyMaterial.Id); if (key is { IsRevoked: true }) @@ -214,7 +226,11 @@ public async Task Revoke(KeyMaterial keyMaterial, string reason = null) private void ClearCache() { _memoryCache.Remove(JwkContants.JwksCache); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jwe); _memoryCache.Remove(JwkContants.CurrentJwkCache); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jwe); } /// diff --git a/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs b/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs index 4f373a1..01d4d66 100644 --- a/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs +++ b/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using NetDevPack.Security.Jwt.Core.Interfaces; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Core.DefaultStore; @@ -18,7 +19,7 @@ public Task Store(KeyMaterial keyMaterial) return Task.CompletedTask; } - public Task GetCurrent() + public Task GetCurrent(JwtKeyType jwtKeyType) { return Task.FromResult(_store.OrderByDescending(s => s.CreationDate).FirstOrDefault()); } @@ -40,12 +41,15 @@ public async Task Revoke(KeyMaterial keyMaterial, string reason = null) } } - public Task> GetLastKeys(int quantity) + public Task> GetLastKeys(int quantity, JwtKeyType? jwtKeyType) { return Task.FromResult( _store - .OrderByDescending(s => s.CreationDate) - .Take(quantity).ToList().AsReadOnly()); + .Where(s => jwtKeyType == null || s.Use == (jwtKeyType == JwtKeyType.Jws ? "sig" : "enc")) + .OrderByDescending(s => s.CreationDate) + .GroupBy(s => s.Use) + .SelectMany(g => g.Take(quantity)) + .ToList().AsReadOnly()); } public Task Get(string keyId) diff --git a/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs b/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs index 8aacc4f..23bd3d6 100644 --- a/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs +++ b/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using Microsoft.IdentityModel.Tokens; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Core.Interfaces; @@ -7,9 +8,9 @@ namespace NetDevPack.Security.Jwt.Core.Interfaces; public interface IJsonWebKeyStore { Task Store(KeyMaterial keyMaterial); - Task GetCurrent(); + Task GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws); Task Revoke(KeyMaterial keyMaterial, string reason=default); - Task> GetLastKeys(int quantity); + Task> GetLastKeys(int quantity, JwtKeyType? jwtKeyType = null); Task Get(string keyId); Task Clear(); } \ No newline at end of file diff --git a/src/NetDevPack.Security.Jwt.Core/Interfaces/IJwtService.cs b/src/NetDevPack.Security.Jwt.Core/Interfaces/IJwtService.cs index b5ea662..270b528 100644 --- a/src/NetDevPack.Security.Jwt.Core/Interfaces/IJwtService.cs +++ b/src/NetDevPack.Security.Jwt.Core/Interfaces/IJwtService.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using Microsoft.IdentityModel.Tokens; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Core.Interfaces; @@ -11,13 +12,14 @@ public interface IJwtService /// If you want to use JWE, you must select RSA. Or use `CryptographicKey` class /// /// - Task GenerateKey(); - Task GetCurrentSecurityKey(); + Task GenerateKey(JwtKeyType jwtKeyType = JwtKeyType.Jws); + Task GetCurrentSecurityKey(JwtKeyType jwtKeyType = JwtKeyType.Jws); Task GetCurrentSigningCredentials(); Task GetCurrentEncryptingCredentials(); + Task> GetLastKeys(int i, JwtKeyType jwtKeyType); Task> GetLastKeys(int? i = null); Task RevokeKey(string keyId, string reason = null); - Task GenerateNewKey(); + Task GenerateNewKey(JwtKeyType jwtKeyType = JwtKeyType.Jws); } [Obsolete("Deprecate, use IJwtServiceInstead")] public interface IJsonWebKeySetService : IJwtService{} \ No newline at end of file diff --git a/src/NetDevPack.Security.Jwt.Core/Jwa/Algorithm.cs b/src/NetDevPack.Security.Jwt.Core/Jwa/Algorithm.cs index 097d07c..77bf802 100644 --- a/src/NetDevPack.Security.Jwt.Core/Jwa/Algorithm.cs +++ b/src/NetDevPack.Security.Jwt.Core/Jwa/Algorithm.cs @@ -58,6 +58,7 @@ private Algorithm() public AlgorithmType AlgorithmType { get; internal set; } public CryptographyType CryptographyType { get; internal set; } public JwtType JwtType => CryptographyType == CryptographyType.Encryption ? JwtType.Jwe : JwtType.Jws; + public string Use => CryptographyType == CryptographyType.Encryption ? "enc" : "sig"; public string Alg { get; internal set; } public string Curve { get; set; } diff --git a/src/NetDevPack.Security.Jwt.Core/Jwa/JwtKeyType.cs b/src/NetDevPack.Security.Jwt.Core/Jwa/JwtKeyType.cs new file mode 100644 index 0000000..58978a9 --- /dev/null +++ b/src/NetDevPack.Security.Jwt.Core/Jwa/JwtKeyType.cs @@ -0,0 +1,11 @@ +namespace NetDevPack.Security.Jwt.Core.Jwa; + +/// +/// Jws will use Digital Signatures algorithms +/// Jwe will use Encryption algorithms +/// +public enum JwtKeyType +{ + Jws = 1, + Jwe = 2 +} \ No newline at end of file diff --git a/src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs b/src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs index b3e6746..4e8938b 100644 --- a/src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs +++ b/src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using NetDevPack.Security.Jwt.Core.Interfaces; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Core.Jwt @@ -16,9 +17,9 @@ public JwtService(IJsonWebKeyStore store, IOptions options) _store = store; _options = options; } - public async Task GenerateKey() + public async Task GenerateKey(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - var key = new CryptographicKey(_options.Value.Jws); + var key = new CryptographicKey(jwtKeyType == JwtKeyType.Jws ? _options.Value.Jws : _options.Value.Jwe); var model = new KeyMaterial(key); await _store.Store(model); @@ -26,48 +27,61 @@ public async Task GenerateKey() return model.GetSecurityKey(); } - public async Task GetCurrentSecurityKey() + public async Task GetCurrentSecurityKey(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - var current = await _store.GetCurrent(); + var current = await _store.GetCurrent(jwtKeyType); if (NeedsUpdate(current)) { // According NIST - https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r4.pdf - Private key should be removed when no longer needs await _store.Revoke(current); - var newKey = await GenerateKey(); + var newKey = await GenerateKey(jwtKeyType); return newKey; } // options has change. Change current key - if (!await CheckCompatibility(current)) - current = await _store.GetCurrent(); + if (!await CheckCompatibility(current, jwtKeyType)) + current = await _store.GetCurrent(jwtKeyType); return current; } public async Task GetCurrentSigningCredentials() { - var current = await GetCurrentSecurityKey(); + var current = await GetCurrentSecurityKey(JwtKeyType.Jws); return new SigningCredentials(current, _options.Value.Jws); } public async Task GetCurrentEncryptingCredentials() { - var current = await GetCurrentSecurityKey(); + var current = await GetCurrentSecurityKey(JwtKeyType.Jwe); return new EncryptingCredentials(current, _options.Value.Jwe.Alg, _options.Value.Jwe.EncryptionAlgorithmContent); } public Task> GetLastKeys(int? i = null) { - return _store.GetLastKeys(_options.Value.AlgorithmsToKeep); + 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); + } + + public Task> GetLastKeys(int i, JwtKeyType jwtKeyType) + { + return _store.GetLastKeys(_options.Value.AlgorithmsToKeep, jwtKeyType); } - private async Task CheckCompatibility(KeyMaterial currentKey) + private async Task CheckCompatibility(KeyMaterial currentKey, JwtKeyType jwtKeyType) { - if (currentKey.Type != _options.Value.Jws.Kty()) + if (jwtKeyType == JwtKeyType.Jws && currentKey.Type != _options.Value.Jws.Kty() + || jwtKeyType == JwtKeyType.Jwe && currentKey.Type != _options.Value.Jwe.Kty()) { - await GenerateKey(); + await GenerateKey(jwtKeyType); return false; } return true; @@ -80,11 +94,11 @@ public async Task RevokeKey(string keyId, string reason = null) await _store.Revoke(key, reason); } - public async Task GenerateNewKey() + public async Task GenerateNewKey(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - var oldCurrent = await _store.GetCurrent(); + var oldCurrent = await _store.GetCurrent(jwtKeyType); await _store.Revoke(oldCurrent); - return await GenerateKey(); + return await GenerateKey(jwtKeyType); } diff --git a/src/NetDevPack.Security.Jwt.Core/JwtOptions.cs b/src/NetDevPack.Security.Jwt.Core/JwtOptions.cs index e00b40c..cddc93f 100644 --- a/src/NetDevPack.Security.Jwt.Core/JwtOptions.cs +++ b/src/NetDevPack.Security.Jwt.Core/JwtOptions.cs @@ -10,4 +10,5 @@ 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; } \ No newline at end of file diff --git a/src/NetDevPack.Security.Jwt.Core/Model/Key.cs b/src/NetDevPack.Security.Jwt.Core/Model/Key.cs index ee496cb..c8e0973 100644 --- a/src/NetDevPack.Security.Jwt.Core/Model/Key.cs +++ b/src/NetDevPack.Security.Jwt.Core/Model/Key.cs @@ -12,6 +12,7 @@ public KeyMaterial() { } public KeyMaterial(CryptographicKey cryptographicKey) { CreationDate = DateTime.UtcNow; + Use = cryptographicKey.Algorithm.Use; Parameters = JsonSerializer.Serialize(cryptographicKey.GetJsonWebKey(), typeof(JsonWebKey)); Type = cryptographicKey.Algorithm.Kty(); KeyId = cryptographicKey.Key.KeyId; @@ -20,6 +21,7 @@ public KeyMaterial(CryptographicKey cryptographicKey) public Guid Id { get; set; } = Guid.NewGuid(); public string KeyId { get; set; } public string Type { get; set; } + public string Use { get; set; } public string Parameters { get; set; } public bool IsRevoked { get; set; } public string? RevokedReason { get; set; } @@ -34,7 +36,7 @@ public JsonWebKey GetSecurityKey() public void Revoke(string reason=default) { - var jsonWebKey = GetSecurityKey(); + var jsonWebKey = GetSecurityKey(); var publicWebKey = PublicJsonWebKey.FromJwk(jsonWebKey); ExpiredAt = DateTime.UtcNow; IsRevoked = true; diff --git a/src/NetDevPack.Security.Jwt.Core/Model/KeyMaterial.cs b/src/NetDevPack.Security.Jwt.Core/Model/KeyMaterial.cs index e0f1c69..196ede2 100644 --- a/src/NetDevPack.Security.Jwt.Core/Model/KeyMaterial.cs +++ b/src/NetDevPack.Security.Jwt.Core/Model/KeyMaterial.cs @@ -23,14 +23,22 @@ public CryptographicKey(Algorithm algorithm) public Algorithm Algorithm { get; set; } public SecurityKey Key { get; set; } - public JsonWebKey GetJsonWebKey() => Algorithm.AlgorithmType switch + public JsonWebKey GetJsonWebKey() { - AlgorithmType.RSA => JsonWebKeyConverter.ConvertFromRSASecurityKey((RsaSecurityKey)Key), - AlgorithmType.ECDsa => JsonWebKeyConverter.ConvertFromECDsaSecurityKey((ECDsaSecurityKey)Key), - AlgorithmType.HMAC => JsonWebKeyConverter.ConvertFromSymmetricSecurityKey((SymmetricSecurityKey)Key), - AlgorithmType.AES => JsonWebKeyConverter.ConvertFromSymmetricSecurityKey((SymmetricSecurityKey)Key), - _ => throw new ArgumentOutOfRangeException() - }; + var jsonWebKey = Algorithm.AlgorithmType switch + { + AlgorithmType.RSA => JsonWebKeyConverter.ConvertFromRSASecurityKey((RsaSecurityKey)Key), + AlgorithmType.ECDsa => JsonWebKeyConverter.ConvertFromECDsaSecurityKey((ECDsaSecurityKey)Key), + AlgorithmType.HMAC => JsonWebKeyConverter.ConvertFromSymmetricSecurityKey((SymmetricSecurityKey)Key), + AlgorithmType.AES => JsonWebKeyConverter.ConvertFromSymmetricSecurityKey((SymmetricSecurityKey)Key), + _ => throw new ArgumentOutOfRangeException() + }; + + jsonWebKey.Use = Algorithm.CryptographyType == CryptographyType.DigitalSignature ? "sig" : "enc"; + jsonWebKey.Alg = Algorithm.Alg; // Assure-toi que `Algorithm.Name` contient l'algorithme correct + + return jsonWebKey; + } private SecurityKey GenerateRsa() { diff --git a/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs b/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs index dfa2c08..34291b4 100644 --- a/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs +++ b/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Options; using NetDevPack.Security.Jwt.Core; using NetDevPack.Security.Jwt.Core.Interfaces; +using NetDevPack.Security.Jwt.Core.Jwa; using NetDevPack.Security.Jwt.Core.Model; namespace NetDevPack.Security.Jwt.Store.EntityFrameworkCore @@ -39,9 +40,11 @@ public async Task Store(KeyMaterial securityParamteres) ClearCache(); } - public async Task GetCurrent() + public async Task GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - if (!_memoryCache.TryGetValue(JwkContants.CurrentJwkCache, out KeyMaterial credentials)) + var cacheKey = JwkContants.CurrentJwkCache + jwtKeyType; + + if (!_memoryCache.TryGetValue(cacheKey, out KeyMaterial credentials)) { #if NET5_0_OR_GREATER credentials = await _context.SecurityKeys.Where(X => X.IsRevoked == false).OrderByDescending(d => d.CreationDate).AsNoTrackingWithIdentityResolution().FirstOrDefaultAsync(); @@ -55,7 +58,7 @@ public async Task GetCurrent() .SetSlidingExpiration(_options.Value.CacheTime); if (credentials != null) - _memoryCache.Set(JwkContants.CurrentJwkCache, credentials, cacheEntryOptions); + _memoryCache.Set(cacheKey, credentials, cacheEntryOptions); return credentials; } @@ -63,9 +66,11 @@ public async Task GetCurrent() return credentials; } - public async Task> GetLastKeys(int quantity = 5) + public async Task> GetLastKeys(int quantity = 5, JwtKeyType? jwtKeyType = null) { - if (!_memoryCache.TryGetValue(JwkContants.JwksCache, out ReadOnlyCollection keys)) + var cacheKey = JwkContants.JwksCache + jwtKeyType; + + if (!_memoryCache.TryGetValue(cacheKey, out ReadOnlyCollection keys)) { #if NET5_0_OR_GREATER keys = _context.SecurityKeys.OrderByDescending(d => d.CreationDate).Take(quantity).AsNoTrackingWithIdentityResolution().ToList().AsReadOnly(); @@ -78,7 +83,7 @@ public async Task> GetLastKeys(int quantity = 5) .SetSlidingExpiration(_options.Value.CacheTime); if (keys.Any()) - _memoryCache.Set(JwkContants.JwksCache, keys, cacheEntryOptions); + _memoryCache.Set(cacheKey, keys, cacheEntryOptions); return keys; } @@ -118,7 +123,11 @@ public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = nul private void ClearCache() { _memoryCache.Remove(JwkContants.JwksCache); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jwe); _memoryCache.Remove(JwkContants.CurrentJwkCache); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jwe); } } } diff --git a/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs b/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs index 7c8a8c7..52e1c41 100644 --- a/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs +++ b/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs @@ -6,6 +6,7 @@ using NetDevPack.Security.Jwt.Core; using NetDevPack.Security.Jwt.Core.Interfaces; using NetDevPack.Security.Jwt.Core.Model; +using NetDevPack.Security.Jwt.Core.Jwa; namespace NetDevPack.Security.Jwt.Store.FileSystem { @@ -68,9 +69,11 @@ public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = nul } - public Task GetCurrent() + public Task GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws) { - if (!_memoryCache.TryGetValue(JwkContants.CurrentJwkCache, out KeyMaterial credentials)) + var cacheKey = JwkContants.CurrentJwkCache + jwtKeyType; + + if (!_memoryCache.TryGetValue(cacheKey, out KeyMaterial credentials)) { credentials = GetKey(GetCurrentFile()); // Set cache options. @@ -78,7 +81,7 @@ public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = nul // Keep in cache for this time, reset time if accessed. .SetSlidingExpiration(_options.Value.CacheTime); if (credentials != null) - _memoryCache.Set(JwkContants.CurrentJwkCache, credentials, cacheEntryOptions); + _memoryCache.Set(cacheKey, credentials, cacheEntryOptions); } return Task.FromResult(credentials); @@ -92,9 +95,11 @@ private KeyMaterial GetKey(string file) } - public Task> GetLastKeys(int quantity = 5) + public Task> GetLastKeys(int quantity = 5, JwtKeyType? jwtKeyType = null) { - if (!_memoryCache.TryGetValue(JwkContants.JwksCache, out IReadOnlyCollection keys)) + var cacheKey = JwkContants.JwksCache + jwtKeyType; + + if (!_memoryCache.TryGetValue(cacheKey, out IReadOnlyCollection keys)) { keys = KeysPath.GetFiles("*.key") .Take(quantity) @@ -107,7 +112,7 @@ public Task> GetLastKeys(int quantity = 5) .SetSlidingExpiration(_options.Value.CacheTime); if (keys.Any()) - _memoryCache.Set(JwkContants.JwksCache, keys, cacheEntryOptions); + _memoryCache.Set(cacheKey, keys, cacheEntryOptions); } return Task.FromResult(keys.ToList().AsReadOnly()); @@ -141,7 +146,11 @@ public Task Clear() private void ClearCache() { _memoryCache.Remove(JwkContants.JwksCache); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jwe); _memoryCache.Remove(JwkContants.CurrentJwkCache); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jws); + _memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jwe); } } } diff --git a/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs b/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs index 87909aa..8c308e8 100644 --- a/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs +++ b/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs @@ -21,7 +21,7 @@ namespace NetDevPack.Security.Jwt.Tests.StoreTests; public abstract class GenericStoreServiceTest : IClassFixture where TWarmup : class, IWarmupTest { - private static SemaphoreSlim TestSync = new(1); + private static SemaphoreSlim TestSync = new(1,1); protected readonly IJsonWebKeyStore _store; private readonly IOptions _options; public TWarmup WarmupData { get; } From c94c1e6c4171270cc84b69c79eb5aeff144d7a2a Mon Sep 17 00:00:00 2001 From: anisite Date: Fri, 21 Feb 2025 05:21:17 +0000 Subject: [PATCH 3/3] rollback semaphore --- .../StoreTests/GenericStoreServiceTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs b/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs index 8c308e8..87909aa 100644 --- a/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs +++ b/tests/NetDevPack.Security.Jwt.Tests/StoreTests/GenericStoreServiceTest.cs @@ -21,7 +21,7 @@ namespace NetDevPack.Security.Jwt.Tests.StoreTests; public abstract class GenericStoreServiceTest : IClassFixture where TWarmup : class, IWarmupTest { - private static SemaphoreSlim TestSync = new(1,1); + private static SemaphoreSlim TestSync = new(1); protected readonly IJsonWebKeyStore _store; private readonly IOptions _options; public TWarmup WarmupData { get; }