Skip to content

Commit 60bdc08

Browse files
Merge pull request #69 from MTESSDev/feature/fully-support-jwe
Feature/fully support jwe
2 parents 17aa6f1 + c94c1e6 commit 60bdc08

File tree

12 files changed

+141
-63
lines changed

12 files changed

+141
-63
lines changed

src/NetDevPack.Security.Jwt.Core/DefaultStore/DataProtectionStore.cs

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.ObjectModel;
1+
using System.Collections.Generic;
2+
using System.Collections.ObjectModel;
23
using System.Globalization;
34
using System.Runtime.InteropServices;
45
using System.Text.Json;
@@ -11,6 +12,7 @@
1112
using Microsoft.Extensions.Options;
1213
using Microsoft.Win32;
1314
using NetDevPack.Security.Jwt.Core.Interfaces;
15+
using NetDevPack.Security.Jwt.Core.Jwa;
1416
using NetDevPack.Security.Jwt.Core.Model;
1517

1618
namespace NetDevPack.Security.Jwt.Core.DefaultStore;
@@ -52,13 +54,13 @@ public DataProtectionStore(
5254
_memoryCache = memoryCache;
5355
_dataProtector = provider.CreateProtector(nameof(KeyMaterial)); ;
5456
}
55-
public Task Store(KeyMaterial securityParamteres)
57+
public Task Store(KeyMaterial securityParameters)
5658
{
57-
var possiblyEncryptedKeyElement = _dataProtector.Protect(JsonSerializer.Serialize(securityParamteres));
59+
var possiblyEncryptedKeyElement = _dataProtector.Protect(JsonSerializer.Serialize(securityParameters));
5860

5961
// build the <key> element
6062
var keyElement = new XElement(Name,
61-
new XAttribute(IdAttributeName, securityParamteres.Id),
63+
new XAttribute(IdAttributeName, securityParameters.Id),
6264
new XAttribute(VersionAttributeName, 1),
6365
new XElement(CreationDateElementName, DateTimeOffset.UtcNow),
6466
new XElement(ActivationDateElementName, DateTimeOffset.UtcNow),
@@ -68,7 +70,7 @@ public Task Store(KeyMaterial securityParamteres)
6870
possiblyEncryptedKeyElement));
6971

7072
// Persist it to the underlying repository and trigger the cancellation token.
71-
var friendlyName = string.Format(CultureInfo.InvariantCulture, "key-{0}", securityParamteres.KeyId);
73+
var friendlyName = string.Format(CultureInfo.InvariantCulture, "key-{0}", securityParameters.KeyId);
7274
KeyRepository.StoreElement(keyElement, friendlyName);
7375
ClearCache();
7476

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

7880

7981

80-
public async Task<KeyMaterial> GetCurrent()
82+
public async Task<KeyMaterial> GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws)
8183
{
82-
if (!_memoryCache.TryGetValue(JwkContants.CurrentJwkCache, out KeyMaterial keyMaterial))
84+
var cacheKey = JwkContants.CurrentJwkCache + jwtKeyType;
85+
86+
if (!_memoryCache.TryGetValue(cacheKey, out KeyMaterial keyMaterial))
8387
{
84-
var keys = await GetLastKeys(1);
88+
var keys = await GetLastKeys(1, jwtKeyType);
8589
keyMaterial = keys.FirstOrDefault();
8690
// Set cache options.
8791
var cacheEntryOptions = new MemoryCacheEntryOptions()
8892
// Keep in cache for this time, reset time if accessed.
8993
.SetSlidingExpiration(_options.Value.CacheTime);
9094

9195
if (keyMaterial != null)
92-
_memoryCache.Set(JwkContants.CurrentJwkCache, keyMaterial, cacheEntryOptions);
96+
_memoryCache.Set(cacheKey, keyMaterial, cacheEntryOptions);
9397
}
9498

9599
return keyMaterial;
@@ -146,10 +150,11 @@ private IReadOnlyCollection<KeyMaterial> GetKeys()
146150
}
147151

148152

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

152-
if (!_memoryCache.TryGetValue(JwkContants.JwksCache, out IReadOnlyCollection<KeyMaterial> keys))
157+
if (!_memoryCache.TryGetValue(cacheKey, out IReadOnlyCollection<KeyMaterial> keys))
153158
{
154159
keys = GetKeys();
155160

@@ -159,13 +164,20 @@ public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity = 5)
159164
.SetSlidingExpiration(_options.Value.CacheTime);
160165

161166
if (keys.Any())
162-
_memoryCache.Set(JwkContants.JwksCache, keys, cacheEntryOptions);
167+
{
168+
keys = keys
169+
.Where(s => jwtKeyType == null || s.Use == (jwtKeyType == JwtKeyType.Jws ? "sig" : "enc"))
170+
.OrderByDescending(s => s.CreationDate)
171+
.ToList().AsReadOnly();
172+
173+
_memoryCache.Set(cacheKey, keys, cacheEntryOptions);
174+
}
163175
}
164176

165177
return Task.FromResult(keys
166-
.OrderByDescending(s => s.CreationDate)
167-
.ToList()
168-
.AsReadOnly());
178+
.GroupBy(s => s.Use)
179+
.SelectMany(g => g.Take(quantity))
180+
.ToList().AsReadOnly());
169181
}
170182

171183
public Task<KeyMaterial> Get(string keyId)
@@ -185,10 +197,10 @@ public async Task Clear()
185197

186198
public async Task Revoke(KeyMaterial keyMaterial, string reason = null)
187199
{
188-
if(keyMaterial == null)
200+
if (keyMaterial == null)
189201
return;
190-
191-
var keys = await GetLastKeys();
202+
203+
var keys = await GetLastKeys(jwtKeyType: keyMaterial.Use.Equals("sig", StringComparison.InvariantCultureIgnoreCase) ? JwtKeyType.Jws : JwtKeyType.Jwe);
192204
var key = keys.First(f => f.Id == keyMaterial.Id);
193205

194206
if (key is { IsRevoked: true })
@@ -214,7 +226,11 @@ public async Task Revoke(KeyMaterial keyMaterial, string reason = null)
214226
private void ClearCache()
215227
{
216228
_memoryCache.Remove(JwkContants.JwksCache);
229+
_memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jws);
230+
_memoryCache.Remove(JwkContants.JwksCache + JwtKeyType.Jwe);
217231
_memoryCache.Remove(JwkContants.CurrentJwkCache);
232+
_memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jws);
233+
_memoryCache.Remove(JwkContants.CurrentJwkCache + JwtKeyType.Jwe);
218234
}
219235

220236
/// <summary>

src/NetDevPack.Security.Jwt.Core/DefaultStore/InMemoryStore.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.ObjectModel;
22
using NetDevPack.Security.Jwt.Core.Interfaces;
3+
using NetDevPack.Security.Jwt.Core.Jwa;
34
using NetDevPack.Security.Jwt.Core.Model;
45

56
namespace NetDevPack.Security.Jwt.Core.DefaultStore;
@@ -18,7 +19,7 @@ public Task Store(KeyMaterial keyMaterial)
1819
return Task.CompletedTask;
1920
}
2021

21-
public Task<KeyMaterial> GetCurrent()
22+
public Task<KeyMaterial> GetCurrent(JwtKeyType jwtKeyType)
2223
{
2324
return Task.FromResult(_store.OrderByDescending(s => s.CreationDate).FirstOrDefault());
2425
}
@@ -40,12 +41,15 @@ public async Task Revoke(KeyMaterial keyMaterial, string reason = null)
4041
}
4142
}
4243

43-
public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity)
44+
public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity, JwtKeyType? jwtKeyType)
4445
{
4546
return Task.FromResult(
4647
_store
47-
.OrderByDescending(s => s.CreationDate)
48-
.Take(quantity).ToList().AsReadOnly());
48+
.Where(s => jwtKeyType == null || s.Use == (jwtKeyType == JwtKeyType.Jws ? "sig" : "enc"))
49+
.OrderByDescending(s => s.CreationDate)
50+
.GroupBy(s => s.Use)
51+
.SelectMany(g => g.Take(quantity))
52+
.ToList().AsReadOnly());
4953
}
5054

5155
public Task<KeyMaterial> Get(string keyId)
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
using System.Collections.ObjectModel;
22
using Microsoft.IdentityModel.Tokens;
3+
using NetDevPack.Security.Jwt.Core.Jwa;
34
using NetDevPack.Security.Jwt.Core.Model;
45

56
namespace NetDevPack.Security.Jwt.Core.Interfaces;
67

78
public interface IJsonWebKeyStore
89
{
910
Task Store(KeyMaterial keyMaterial);
10-
Task<KeyMaterial> GetCurrent();
11+
Task<KeyMaterial> GetCurrent(JwtKeyType jwtKeyType = JwtKeyType.Jws);
1112
Task Revoke(KeyMaterial keyMaterial, string reason=default);
12-
Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity);
13+
Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity, JwtKeyType? jwtKeyType = null);
1314
Task<KeyMaterial> Get(string keyId);
1415
Task Clear();
1516
}

src/NetDevPack.Security.Jwt.Core/Interfaces/IJwtService.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.ObjectModel;
22
using Microsoft.IdentityModel.Tokens;
3+
using NetDevPack.Security.Jwt.Core.Jwa;
34
using NetDevPack.Security.Jwt.Core.Model;
45

56
namespace NetDevPack.Security.Jwt.Core.Interfaces;
@@ -11,13 +12,14 @@ public interface IJwtService
1112
/// If you want to use JWE, you must select RSA. Or use `CryptographicKey` class
1213
/// </summary>
1314
/// <returns></returns>
14-
Task<SecurityKey> GenerateKey();
15-
Task<SecurityKey> GetCurrentSecurityKey();
15+
Task<SecurityKey> GenerateKey(JwtKeyType jwtKeyType = JwtKeyType.Jws);
16+
Task<SecurityKey> GetCurrentSecurityKey(JwtKeyType jwtKeyType = JwtKeyType.Jws);
1617
Task<SigningCredentials> GetCurrentSigningCredentials();
1718
Task<EncryptingCredentials> GetCurrentEncryptingCredentials();
19+
Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int i, JwtKeyType jwtKeyType);
1820
Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int? i = null);
1921
Task RevokeKey(string keyId, string reason = null);
20-
Task<SecurityKey> GenerateNewKey();
22+
Task<SecurityKey> GenerateNewKey(JwtKeyType jwtKeyType = JwtKeyType.Jws);
2123
}
2224
[Obsolete("Deprecate, use IJwtServiceInstead")]
2325
public interface IJsonWebKeySetService : IJwtService{}

src/NetDevPack.Security.Jwt.Core/Jwa/Algorithm.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ private Algorithm()
5858
public AlgorithmType AlgorithmType { get; internal set; }
5959
public CryptographyType CryptographyType { get; internal set; }
6060
public JwtType JwtType => CryptographyType == CryptographyType.Encryption ? JwtType.Jwe : JwtType.Jws;
61+
public string Use => CryptographyType == CryptographyType.Encryption ? "enc" : "sig";
6162
public string Alg { get; internal set; }
6263
public string Curve { get; set; }
6364

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace NetDevPack.Security.Jwt.Core.Jwa;
2+
3+
/// <summary>
4+
/// Jws will use Digital Signatures algorithms
5+
/// Jwe will use Encryption algorithms
6+
/// </summary>
7+
public enum JwtKeyType
8+
{
9+
Jws = 1,
10+
Jwe = 2
11+
}

src/NetDevPack.Security.Jwt.Core/Jwt/JwtService.cs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.Extensions.Options;
33
using Microsoft.IdentityModel.Tokens;
44
using NetDevPack.Security.Jwt.Core.Interfaces;
5+
using NetDevPack.Security.Jwt.Core.Jwa;
56
using NetDevPack.Security.Jwt.Core.Model;
67

78
namespace NetDevPack.Security.Jwt.Core.Jwt
@@ -16,58 +17,71 @@ public JwtService(IJsonWebKeyStore store, IOptions<JwtOptions> options)
1617
_store = store;
1718
_options = options;
1819
}
19-
public async Task<SecurityKey> GenerateKey()
20+
public async Task<SecurityKey> GenerateKey(JwtKeyType jwtKeyType = JwtKeyType.Jws)
2021
{
21-
var key = new CryptographicKey(_options.Value.Jws);
22+
var key = new CryptographicKey(jwtKeyType == JwtKeyType.Jws ? _options.Value.Jws : _options.Value.Jwe);
2223

2324
var model = new KeyMaterial(key);
2425
await _store.Store(model);
2526

2627
return model.GetSecurityKey();
2728
}
2829

29-
public async Task<SecurityKey> GetCurrentSecurityKey()
30+
public async Task<SecurityKey> GetCurrentSecurityKey(JwtKeyType jwtKeyType = JwtKeyType.Jws)
3031
{
31-
var current = await _store.GetCurrent();
32+
var current = await _store.GetCurrent(jwtKeyType);
3233

3334
if (NeedsUpdate(current))
3435
{
3536
// According NIST - https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r4.pdf - Private key should be removed when no longer needs
3637
await _store.Revoke(current);
37-
var newKey = await GenerateKey();
38+
var newKey = await GenerateKey(jwtKeyType);
3839
return newKey;
3940
}
4041

4142
// options has change. Change current key
42-
if (!await CheckCompatibility(current))
43-
current = await _store.GetCurrent();
43+
if (!await CheckCompatibility(current, jwtKeyType))
44+
current = await _store.GetCurrent(jwtKeyType);
4445

4546
return current;
4647
}
4748
public async Task<SigningCredentials> GetCurrentSigningCredentials()
4849
{
49-
var current = await GetCurrentSecurityKey();
50+
var current = await GetCurrentSecurityKey(JwtKeyType.Jws);
5051

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

5455
public async Task<EncryptingCredentials> GetCurrentEncryptingCredentials()
5556
{
56-
var current = await GetCurrentSecurityKey();
57+
var current = await GetCurrentSecurityKey(JwtKeyType.Jwe);
5758

5859
return new EncryptingCredentials(current, _options.Value.Jwe.Alg, _options.Value.Jwe.EncryptionAlgorithmContent);
5960
}
6061

6162
public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int? i = null)
6263
{
63-
return _store.GetLastKeys(_options.Value.AlgorithmsToKeep);
64+
JwtKeyType? jwtKeyType = null;
65+
66+
if (_options.Value.ExposedKeyType == JwtType.Jws)
67+
jwtKeyType = JwtKeyType.Jws;
68+
else if (_options.Value.ExposedKeyType == JwtType.Jwe)
69+
jwtKeyType = JwtKeyType.Jwe;
70+
71+
return _store.GetLastKeys(_options.Value.AlgorithmsToKeep, jwtKeyType);
72+
}
73+
74+
public Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int i, JwtKeyType jwtKeyType)
75+
{
76+
return _store.GetLastKeys(_options.Value.AlgorithmsToKeep, jwtKeyType);
6477
}
6578

66-
private async Task<bool> CheckCompatibility(KeyMaterial currentKey)
79+
private async Task<bool> CheckCompatibility(KeyMaterial currentKey, JwtKeyType jwtKeyType)
6780
{
68-
if (currentKey.Type != _options.Value.Jws.Kty())
81+
if (jwtKeyType == JwtKeyType.Jws && currentKey.Type != _options.Value.Jws.Kty()
82+
|| jwtKeyType == JwtKeyType.Jwe && currentKey.Type != _options.Value.Jwe.Kty())
6983
{
70-
await GenerateKey();
84+
await GenerateKey(jwtKeyType);
7185
return false;
7286
}
7387
return true;
@@ -80,11 +94,11 @@ public async Task RevokeKey(string keyId, string reason = null)
8094
await _store.Revoke(key, reason);
8195
}
8296

83-
public async Task<SecurityKey> GenerateNewKey()
97+
public async Task<SecurityKey> GenerateNewKey(JwtKeyType jwtKeyType = JwtKeyType.Jws)
8498
{
85-
var oldCurrent = await _store.GetCurrent();
99+
var oldCurrent = await _store.GetCurrent(jwtKeyType);
86100
await _store.Revoke(oldCurrent);
87-
return await GenerateKey();
101+
return await GenerateKey(jwtKeyType);
88102

89103
}
90104

src/NetDevPack.Security.Jwt.Core/JwtOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ public class JwtOptions
1010
public string KeyPrefix { get; set; } = $"{Environment.MachineName}_";
1111
public int AlgorithmsToKeep { get; set; } = 2;
1212
public TimeSpan CacheTime { get; set; } = TimeSpan.FromMinutes(15);
13+
public JwtType ExposedKeyType { get; set; } = JwtType.Jws;
1314
}

src/NetDevPack.Security.Jwt.Core/Model/Key.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public KeyMaterial() { }
1212
public KeyMaterial(CryptographicKey cryptographicKey)
1313
{
1414
CreationDate = DateTime.UtcNow;
15+
Use = cryptographicKey.Algorithm.Use;
1516
Parameters = JsonSerializer.Serialize(cryptographicKey.GetJsonWebKey(), typeof(JsonWebKey));
1617
Type = cryptographicKey.Algorithm.Kty();
1718
KeyId = cryptographicKey.Key.KeyId;
@@ -20,6 +21,7 @@ public KeyMaterial(CryptographicKey cryptographicKey)
2021
public Guid Id { get; set; } = Guid.NewGuid();
2122
public string KeyId { get; set; }
2223
public string Type { get; set; }
24+
public string Use { get; set; }
2325
public string Parameters { get; set; }
2426
public bool IsRevoked { get; set; }
2527
public string? RevokedReason { get; set; }
@@ -34,7 +36,7 @@ public JsonWebKey GetSecurityKey()
3436

3537
public void Revoke(string reason=default)
3638
{
37-
var jsonWebKey = GetSecurityKey();
39+
var jsonWebKey = GetSecurityKey();
3840
var publicWebKey = PublicJsonWebKey.FromJwk(jsonWebKey);
3941
ExpiredAt = DateTime.UtcNow;
4042
IsRevoked = true;

0 commit comments

Comments
 (0)