Implement jwks endpoint and add test for it.
This also let to some improvements/cleanups of the other tests and fixtures.
This commit is contained in:
parent
a80c133e2a
commit
ccb06b260c
24 changed files with 353 additions and 107 deletions
|
|
@ -1,39 +0,0 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace IdentityShroud.Core.Messages;
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc7517.html
|
||||
|
||||
|
||||
public class JsonWebKey
|
||||
{
|
||||
[JsonPropertyName("kty")]
|
||||
public string KeyType { get; set; } = "RSA";
|
||||
|
||||
// Common values sig(nature) enc(ryption)
|
||||
[JsonPropertyName("use")]
|
||||
public string? Use { get; set; } = "sig"; // "sig" for signature, "enc" for encryption
|
||||
|
||||
// Per standard this field is optional for now we will use RS256
|
||||
[JsonPropertyName("alg")]
|
||||
public string? Algorithm { get; set; } = "RS256";
|
||||
|
||||
[JsonPropertyName("kid")]
|
||||
public required string KeyId { get; set; }
|
||||
|
||||
// RSA Public Key Components
|
||||
[JsonPropertyName("n")]
|
||||
public required string Modulus { get; set; }
|
||||
|
||||
[JsonPropertyName("e")]
|
||||
public required string Exponent { get; set; }
|
||||
|
||||
// Optional fields
|
||||
[JsonPropertyName("x5c")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public List<string> X509CertificateChain { get; set; }
|
||||
|
||||
[JsonPropertyName("x5t")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string X509CertificateThumbprint { get; set; }
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace IdentityShroud.Core.Messages;
|
||||
|
||||
public class JsonWebKeySet
|
||||
{
|
||||
[JsonPropertyName("keys")]
|
||||
public List<JsonWebKey> Keys { get; set; } = new List<JsonWebKey>();
|
||||
}
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
using IdentityShroud.Core.Security;
|
||||
|
||||
namespace IdentityShroud.Core.Model;
|
||||
|
||||
public class Client
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public string? SignatureAlgorithm { get; set; } = JsonWebAlgorithm.RS256;
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using IdentityShroud.Core.Security;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IdentityShroud.Core.Model;
|
||||
|
||||
|
|
@ -19,4 +21,10 @@ public class Realm
|
|||
public List<Client> Clients { get; init; } = [];
|
||||
|
||||
public List<Key> Keys { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be overriden per client
|
||||
/// </summary>
|
||||
public string DefaultSignatureAlgorithm { get; set; } = JsonWebAlgorithm.RS256;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,22 @@ public static class AesGcmHelper
|
|||
|
||||
public static byte[] EncryptAesGcm(byte[] plaintext, byte[] key)
|
||||
{
|
||||
using var aes = new AesGcm(key);
|
||||
byte[] nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);
|
||||
byte[] ciphertext = new byte[plaintext.Length];
|
||||
byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];
|
||||
int tagSize = AesGcm.TagByteSizes.MaxSize;
|
||||
using var aes = new AesGcm(key, tagSize);
|
||||
|
||||
Span<byte> nonce = stackalloc byte[AesGcm.NonceByteSizes.MaxSize];
|
||||
RandomNumberGenerator.Fill(nonce);
|
||||
Span<byte> ciphertext = stackalloc byte[plaintext.Length];
|
||||
Span<byte> tag = stackalloc byte[tagSize];
|
||||
|
||||
aes.Encrypt(nonce, plaintext, ciphertext, tag);
|
||||
// Return concatenated nonce|ciphertext|tag (or store separately)
|
||||
return nonce.Concat(ciphertext).Concat(tag).ToArray();
|
||||
|
||||
// Return concatenated nonce|ciphertext|tag
|
||||
var result = new byte[nonce.Length + ciphertext.Length + tag.Length];
|
||||
nonce.CopyTo(result.AsSpan(0, nonce.Length));
|
||||
ciphertext.CopyTo(result.AsSpan(nonce.Length, ciphertext.Length));
|
||||
tag.CopyTo(result.AsSpan(nonce.Length + ciphertext.Length, tag.Length));
|
||||
return result;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
|
@ -44,11 +52,10 @@ public static class AesGcmHelper
|
|||
ReadOnlySpan<byte> nonce = new(payload, 0, nonceSize);
|
||||
ReadOnlySpan<byte> ciphertext = new(payload, nonceSize, payload.Length - nonceSize - tagSize);
|
||||
ReadOnlySpan<byte> tag = new(payload, payload.Length - tagSize, tagSize);
|
||||
|
||||
|
||||
byte[] plaintext = new byte[ciphertext.Length];
|
||||
|
||||
using var aes = new AesGcm(key);
|
||||
using var aes = new AesGcm(key, tagSize);
|
||||
try
|
||||
{
|
||||
aes.Decrypt(nonce, ciphertext, tag, plaintext);
|
||||
|
|
|
|||
8
IdentityShroud.Core/Security/JsonWebAlgorithm.cs
Normal file
8
IdentityShroud.Core/Security/JsonWebAlgorithm.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
using System.Security.Cryptography;
|
||||
|
||||
namespace IdentityShroud.Core.Security;
|
||||
|
||||
public static class JsonWebAlgorithm
|
||||
{
|
||||
public const string RS256 = "RS256";
|
||||
}
|
||||
|
|
@ -4,4 +4,13 @@ namespace IdentityShroud.Core.Security;
|
|||
|
||||
public static class RsaHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Load RSA private key from PKCS#8 format
|
||||
/// </summary>
|
||||
public static RSA LoadFromPkcs8(byte[] pkcs8Key)
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
rsa.ImportPkcs8PrivateKey(pkcs8Key, out _);
|
||||
return rsa;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,4 +8,5 @@ public interface IRealmService
|
|||
Task<Realm?> FindBySlug(string slug, CancellationToken ct = default);
|
||||
|
||||
Task<Result<RealmCreateResponse>> Create(RealmCreateRequest request, CancellationToken ct = default);
|
||||
Task LoadActiveKeys(Realm realm);
|
||||
}
|
||||
|
|
@ -15,7 +15,8 @@ public class RealmService(
|
|||
{
|
||||
public async Task<Realm?> FindBySlug(string slug, CancellationToken ct = default)
|
||||
{
|
||||
return await db.Realms.SingleOrDefaultAsync(r => r.Slug == slug, ct);
|
||||
return await db.Realms
|
||||
.SingleOrDefaultAsync(r => r.Slug == slug, ct);
|
||||
}
|
||||
|
||||
public async Task<Result<RealmCreateResponse>> Create(RealmCreateRequest request, CancellationToken ct = default)
|
||||
|
|
@ -35,6 +36,15 @@ public class RealmService(
|
|||
realm.Id, realm.Slug, realm.Name);
|
||||
}
|
||||
|
||||
public async Task LoadActiveKeys(Realm realm)
|
||||
{
|
||||
await db.Entry(realm).Collection(r => r.Keys)
|
||||
.Query()
|
||||
.Where(k => k.DeactivatedAt == null)
|
||||
.LoadAsync();
|
||||
|
||||
}
|
||||
|
||||
private Key CreateKey()
|
||||
{
|
||||
using RSA rsa = RSA.Create(2048);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue