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
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 <key> 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),
Expand All @@ -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();

Expand All @@ -77,19 +79,21 @@ public Task Store(KeyMaterial securityParamteres)



public async Task<KeyMaterial> GetCurrent()
public async Task<KeyMaterial> 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()
// Keep in cache for this time, reset time if accessed.
.SetSlidingExpiration(_options.Value.CacheTime);

if (keyMaterial != null)
_memoryCache.Set(JwkContants.CurrentJwkCache, keyMaterial, cacheEntryOptions);
_memoryCache.Set(cacheKey, keyMaterial, cacheEntryOptions);
}

return keyMaterial;
Expand Down Expand Up @@ -146,10 +150,11 @@ private IReadOnlyCollection<KeyMaterial> GetKeys()
}


public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity = 5)
public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity = 5, JwtKeyType? jwtKeyType = null)
{
var cacheKey = JwkContants.JwksCache + jwtKeyType;

if (!_memoryCache.TryGetValue(JwkContants.JwksCache, out IReadOnlyCollection<KeyMaterial> keys))
if (!_memoryCache.TryGetValue(cacheKey, out IReadOnlyCollection<KeyMaterial> keys))
{
keys = GetKeys();

Expand All @@ -159,13 +164,20 @@ public Task<ReadOnlyCollection<KeyMaterial>> 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<KeyMaterial> Get(string keyId)
Expand All @@ -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 })
Expand All @@ -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);
}

/// <summary>
Expand Down
12 changes: 8 additions & 4 deletions src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,7 +19,7 @@ public Task Store(KeyMaterial keyMaterial)
return Task.CompletedTask;
}

public Task<KeyMaterial> GetCurrent()
public Task<KeyMaterial> GetCurrent(JwtKeyType jwtKeyType)
{
return Task.FromResult(_store.OrderByDescending(s => s.CreationDate).FirstOrDefault());
}
Expand All @@ -40,12 +41,15 @@ public async Task Revoke(KeyMaterial keyMaterial, string reason = null)
}
}

public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity)
public Task<ReadOnlyCollection<KeyMaterial>> 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<KeyMaterial> Get(string keyId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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;

public interface IJsonWebKeyStore
{
Task Store(KeyMaterial keyMaterial);
Task<KeyMaterial> GetCurrent();
Task<KeyMaterial> GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws);
Task Revoke(KeyMaterial keyMaterial, string reason=default);
Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity);
Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity, JwtKeyType? jwtKeyType = null);
Task<KeyMaterial> Get(string keyId);
Task Clear();
}
8 changes: 5 additions & 3 deletions src/NetDevPack.Security.Jwt.Core/Interfaces/IJwtService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,13 +12,14 @@ public interface IJwtService
/// If you want to use JWE, you must select RSA. Or use `CryptographicKey` class
/// </summary>
/// <returns></returns>
Task<SecurityKey> GenerateKey();
Task<SecurityKey> GetCurrentSecurityKey();
Task<SecurityKey> GenerateKey(JwtKeyType jwtKeyType = JwtKeyType.Jws);
Task<SecurityKey> GetCurrentSecurityKey(JwtKeyType jwtKeyType = JwtKeyType.Jws);
Task<SigningCredentials> GetCurrentSigningCredentials();
Task<EncryptingCredentials> GetCurrentEncryptingCredentials();
Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int i, JwtKeyType jwtKeyType);
Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int? i = null);
Task RevokeKey(string keyId, string reason = null);
Task<SecurityKey> GenerateNewKey();
Task<SecurityKey> GenerateNewKey(JwtKeyType jwtKeyType = JwtKeyType.Jws);
}
[Obsolete("Deprecate, use IJwtServiceInstead")]
public interface IJsonWebKeySetService : IJwtService{}
1 change: 1 addition & 0 deletions src/NetDevPack.Security.Jwt.Core/Jwa/Algorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
11 changes: 11 additions & 0 deletions src/NetDevPack.Security.Jwt.Core/Jwa/JwtKeyType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace NetDevPack.Security.Jwt.Core.Jwa;

/// <summary>
/// Jws will use Digital Signatures algorithms
/// Jwe will use Encryption algorithms
/// </summary>
public enum JwtKeyType
{
Jws = 1,
Jwe = 2
}
46 changes: 30 additions & 16 deletions src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,58 +17,71 @@ public JwtService(IJsonWebKeyStore store, IOptions<JwtOptions> options)
_store = store;
_options = options;
}
public async Task<SecurityKey> GenerateKey()
public async Task<SecurityKey> 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);

return model.GetSecurityKey();
}

public async Task<SecurityKey> GetCurrentSecurityKey()
public async Task<SecurityKey> 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<SigningCredentials> GetCurrentSigningCredentials()
{
var current = await GetCurrentSecurityKey();
var current = await GetCurrentSecurityKey(JwtKeyType.Jws);

return new SigningCredentials(current, _options.Value.Jws);
}

public async Task<EncryptingCredentials> 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<ReadOnlyCollection<KeyMaterial>> 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<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int i, JwtKeyType jwtKeyType)
{
return _store.GetLastKeys(_options.Value.AlgorithmsToKeep, jwtKeyType);
}

private async Task<bool> CheckCompatibility(KeyMaterial currentKey)
private async Task<bool> 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;
Expand All @@ -80,11 +94,11 @@ public async Task RevokeKey(string keyId, string reason = null)
await _store.Revoke(key, reason);
}

public async Task<SecurityKey> GenerateNewKey()
public async Task<SecurityKey> 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);

}

Expand Down
1 change: 1 addition & 0 deletions src/NetDevPack.Security.Jwt.Core/JwtOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 3 additions & 1 deletion src/NetDevPack.Security.Jwt.Core/Model/Key.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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;
Expand All @@ -20,9 +21,10 @@
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; }

Check warning on line 27 in src/NetDevPack.Security.Jwt.Core/Model/Key.cs

View workflow job for this annotation

GitHub Actions / pull-request

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 27 in src/NetDevPack.Security.Jwt.Core/Model/Key.cs

View workflow job for this annotation

GitHub Actions / pull-request

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 27 in src/NetDevPack.Security.Jwt.Core/Model/Key.cs

View workflow job for this annotation

GitHub Actions / pull-request

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 27 in src/NetDevPack.Security.Jwt.Core/Model/Key.cs

View workflow job for this annotation

GitHub Actions / pull-request

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public DateTime CreationDate { get; set; }
public DateTime? ExpiredAt { get; set; }

Expand All @@ -34,7 +36,7 @@

public void Revoke(string reason=default)
{
var jsonWebKey = GetSecurityKey();
var jsonWebKey = GetSecurityKey();
var publicWebKey = PublicJsonWebKey.FromJwk(jsonWebKey);
ExpiredAt = DateTime.UtcNow;
IsRevoked = true;
Expand Down
Loading
Loading