diff --git a/CHANGELOG.md b/CHANGELOG.md index df37ce8..4b387a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [7.1.0](https://github.com/NetDevPack/Security.Jwt/compare/v7.0.2...v7.1.0) (2023-04-14) + + +### Features + +* new demos [skip ci] ([a0a5f62](https://github.com/NetDevPack/Security.Jwt/commit/a0a5f622f48dc98361ffbe327905feef2a816722)) +* Revoked Reason ([c7f10bc](https://github.com/NetDevPack/Security.Jwt/commit/c7f10bcdb392ebef79140b9c3dff00b49c2aab7a)) +* **revoke:** revoke key with given reason ([350fcaa](https://github.com/NetDevPack/Security.Jwt/commit/350fcaa26ba3645344d3d060a42787aa55f98fa1)) + ## [7.0.2](https://github.com/NetDevPack/Security.Jwt/compare/v7.0.1...v7.0.2) (2022-12-05) diff --git a/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs b/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs index 1927c1b..70ae425 100644 --- a/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs +++ b/src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs @@ -37,6 +37,7 @@ internal class DataProtectionStore : IJsonWebKeyStore private IXmlRepository KeyRepository => _keyManagementOptions.Value.XmlRepository ?? GetFallbackKeyRepositoryEncryptorPair(); private const string Name = "NetDevPackSecurityJwt"; + internal const string DefaultRevocationReason = "Revoked"; public DataProtectionStore( ILoggerFactory loggerFactory, @@ -98,7 +99,7 @@ private IReadOnlyCollection GetKeys() { var allElements = KeyRepository.GetAllElements(); var keys = new List(); - var revokedKeys = new List(); + var revokedKeys = new List(); foreach (var element in allElements) { if (element.Name == Name) @@ -124,7 +125,7 @@ private IReadOnlyCollection GetKeys() if (key.IsExpired(_options.Value.DaysUntilExpire)) { //Revoke(key).Wait(); - revokedKeys.Add(key.Id.ToString()); + revokedKeys.Add(new RevokedKeyInfo(key.Id.ToString())); } keys.Add(key); @@ -132,13 +133,14 @@ private IReadOnlyCollection GetKeys() else if (element.Name == RevocationElementName) { var keyIdAsString = (string)element.Element(Name)!.Attribute(IdAttributeName)!; - revokedKeys.Add(keyIdAsString); + var reason = (string)element.Element(ReasonElementName); + revokedKeys.Add(new RevokedKeyInfo(keyIdAsString, reason)); } } foreach (var revokedKey in revokedKeys) { - keys.FirstOrDefault(a => a.Id.ToString().Equals(revokedKey))?.Revoke(); + keys.FirstOrDefault(a => a.Id.ToString().Equals(revokedKey.Id))?.Revoke(revokedKey.RevokedReason); } return keys.ToList(); } @@ -181,7 +183,7 @@ public async Task Clear() } - public async Task Revoke(KeyMaterial keyMaterial) + public async Task Revoke(KeyMaterial keyMaterial, string reason = null) { if(keyMaterial == null) return; @@ -193,12 +195,13 @@ public async Task Revoke(KeyMaterial keyMaterial) return; keyMaterial.Revoke(); + var revokeReason = reason ?? DefaultRevocationReason; var revocationElement = new XElement(RevocationElementName, new XAttribute(VersionAttributeName, 1), new XElement(RevocationDateElementName, DateTimeOffset.UtcNow), new XElement(Name, new XAttribute(IdAttributeName, keyMaterial.Id)), - new XElement(ReasonElementName, "Revoked")); + new XElement(ReasonElementName, revokeReason)); // Persist it to the underlying repository and trigger the cancellation token diff --git a/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs b/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs index 0601f24..4f373a1 100644 --- a/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs +++ b/src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs @@ -6,7 +6,7 @@ namespace NetDevPack.Security.Jwt.Core.DefaultStore; internal class InMemoryStore : IJsonWebKeyStore { - + internal const string DefaultRevocationReason = "Revoked"; private static readonly List _store = new(); private readonly SemaphoreSlim _slim = new(1); public Task Store(KeyMaterial keyMaterial) @@ -23,12 +23,12 @@ public Task GetCurrent() return Task.FromResult(_store.OrderByDescending(s => s.CreationDate).FirstOrDefault()); } - public async Task Revoke(KeyMaterial keyMaterial) + public async Task Revoke(KeyMaterial keyMaterial, string reason = null) { if(keyMaterial == null) return; - - keyMaterial.Revoke(); + var revokeReason = reason ?? DefaultRevocationReason; + keyMaterial.Revoke(revokeReason); var oldOne = _store.Find(f => f.Id == keyMaterial.Id); if (oldOne != null) { diff --git a/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs b/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs index 988c9a5..8aacc4f 100644 --- a/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs +++ b/src/NetDevPack.Security.Jwt.Core/Interfaces/IJsonWebKeyStore.cs @@ -8,7 +8,7 @@ public interface IJsonWebKeyStore { Task Store(KeyMaterial keyMaterial); Task GetCurrent(); - Task Revoke(KeyMaterial keyMaterial); + Task Revoke(KeyMaterial keyMaterial, string reason=default); Task> GetLastKeys(int quantity); Task Get(string keyId); Task Clear(); diff --git a/src/NetDevPack.Security.Jwt.Core/Model/Key.cs b/src/NetDevPack.Security.Jwt.Core/Model/Key.cs index 36ea4c0..b6934a3 100644 --- a/src/NetDevPack.Security.Jwt.Core/Model/Key.cs +++ b/src/NetDevPack.Security.Jwt.Core/Model/Key.cs @@ -22,6 +22,7 @@ public KeyMaterial(CryptographicKey cryptographicKey) public string Type { get; set; } public string Parameters { get; set; } public bool IsRevoked { get; set; } + public string? RevokedReason { get; set; } public DateTime CreationDate { get; set; } public DateTime? ExpiredAt { get; set; } @@ -30,15 +31,18 @@ public JsonWebKey GetSecurityKey() return JsonSerializer.Deserialize(Parameters); } - public void Revoke() + public void Revoke(string reason=default) { var jsonWebKey = GetSecurityKey(); var publicWebKey = PublicJsonWebKey.FromJwk(jsonWebKey); ExpiredAt = DateTime.UtcNow; IsRevoked = true; + RevokedReason = reason; Parameters = JsonSerializer.Serialize(publicWebKey.ToNativeJwk(), new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }); } + + public bool IsExpired(int valueDaysUntilExpire) { return CreationDate.AddDays(valueDaysUntilExpire) < DateTime.UtcNow.Date; diff --git a/src/NetDevPack.Security.Jwt.Core/Model/RevokedKeyInfo.cs b/src/NetDevPack.Security.Jwt.Core/Model/RevokedKeyInfo.cs new file mode 100644 index 0000000..becb8df --- /dev/null +++ b/src/NetDevPack.Security.Jwt.Core/Model/RevokedKeyInfo.cs @@ -0,0 +1,7 @@ +namespace NetDevPack.Security.Jwt.Core.Model; + +record RevokedKeyInfo(string Id, string? RevokedReason=default) +{ + public string Id { get; } = Id; + public string? RevokedReason { get; } = RevokedReason; +} \ No newline at end of file diff --git a/src/NetDevPack.Security.Jwt.Core/NetDevPack.Security.Jwt.Core.csproj b/src/NetDevPack.Security.Jwt.Core/NetDevPack.Security.Jwt.Core.csproj index 5cbc635..b6c0418 100644 --- a/src/NetDevPack.Security.Jwt.Core/NetDevPack.Security.Jwt.Core.csproj +++ b/src/NetDevPack.Security.Jwt.Core/NetDevPack.Security.Jwt.Core.csproj @@ -24,6 +24,11 @@ + + + <_Parameter1>NetDevPack.Security.Jwt.Tests + + diff --git a/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs b/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs index bb1a634..93f901d 100644 Binary files a/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs and b/src/NetDevPack.Security.Jwt.Store.EntityFrameworkCore/DatabaseJsonWebKeyStore.cs differ diff --git a/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs b/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs index a53ce57..7c8a8c7 100644 --- a/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs +++ b/src/NetDevPack.Security.Jwt.Store.FileSystem/FileSystemStore.cs @@ -51,7 +51,7 @@ public bool NeedsUpdate() return !File.Exists(GetCurrentFile()) || File.GetCreationTimeUtc(GetCurrentFile()).AddDays(_options.Value.DaysUntilExpire) < DateTime.UtcNow.Date; } - public async Task Revoke(KeyMaterial? securityKeyWithPrivate) + public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = null) { if (securityKeyWithPrivate == null) return; diff --git a/tests/NetDevPack.Security.Jwt.Tests/StoreTests/DataProtectionStoreTest.cs b/tests/NetDevPack.Security.Jwt.Tests/StoreTests/DataProtectionStoreTest.cs index 3927917..e396d39 100644 --- a/tests/NetDevPack.Security.Jwt.Tests/StoreTests/DataProtectionStoreTest.cs +++ b/tests/NetDevPack.Security.Jwt.Tests/StoreTests/DataProtectionStoreTest.cs @@ -1,4 +1,11 @@ -using NetDevPack.Security.Jwt.Tests.Warmups; +using System.Linq; +using Microsoft.IdentityModel.Tokens; +using NetDevPack.Security.Jwt.Tests.Warmups; +using System.Threading.Tasks; +using FluentAssertions; +using NetDevPack.Security.Jwt.Core.DefaultStore; +using NetDevPack.Security.Jwt.Core.Jwa; +using NetDevPack.Security.Jwt.Core.Model; using Xunit; namespace NetDevPack.Security.Jwt.Tests.StoreTests; @@ -9,4 +16,6 @@ public class DataProtectionStoreTest : GenericStoreServiceTest : IClassFixture where TWarmup : class, IWarmupTest { private static SemaphoreSlim TestSync = new(1); - private readonly IJsonWebKeyStore _store; + protected readonly IJsonWebKeyStore _store; private readonly IOptions _options; public TWarmup WarmupData { get; } @@ -498,6 +499,42 @@ public async Task ShouldGenerateAndValidateJweAndJws() } + [Fact] + public async Task Should_Read_Default_Revocation_Reason() + { + var keyMaterial = await StoreRandomKey(); + /*Revoke*/ + await _store.Revoke(keyMaterial); + await CheckRevocationReasonIsStored(keyMaterial.KeyId, DataProtectionStore.DefaultRevocationReason); + } + + [Theory] + [InlineData("ManualRevocation")] + [InlineData("StolenKey")] + public async Task Should_Read_NonDefault_Revocation_Reason(string reason) + { + var keyMaterial = await StoreRandomKey(); + /*Revoke with reason*/ + await _store.Revoke(keyMaterial, reason); + await CheckRevocationReasonIsStored(keyMaterial.KeyId, reason); + } + + private async Task CheckRevocationReasonIsStored(string keyId, string revocationReason) + { + var dbKey = (await _store.GetLastKeys(5)).First(w => w.KeyId == keyId); + dbKey.Type.Should().NotBeNullOrEmpty(); + dbKey.RevokedReason.Should().BeEquivalentTo(revocationReason); + } + + private async Task StoreRandomKey() + { + var alg = Algorithm.Create(DigitalSignaturesAlgorithm.RsaSha512); + var key = new CryptographicKey(alg); + var keyMaterial = new KeyMaterial(key); + await _store.Store(keyMaterial); + return keyMaterial; + } + private Task GenerateKey()