(feature): jwk and jws handling

This commit is contained in:
Maksym Sadovnychyy 2025-11-11 18:26:37 +01:00
parent 186e99a6f4
commit e619cf276c
15 changed files with 784 additions and 13 deletions

View File

@ -27,6 +27,8 @@
- [Checksum Utility](#checksum-utility)
- [Password Hasher](#password-hasher)
- [JWT Generator](#jwt-generator)
- [JWK Generator](#jwk-generator)
- [JWS Generator](#jws-generator)
- [TOTP Generator](#totp-generator)
- [Web API Models](#web-api-models)
- [Sagas](#sagas)
@ -964,6 +966,62 @@ JwtGenerator.TryGenerateToken(secret, issuer, audience, 60, "user", roles, out v
---
### JWK Generator
The `JwkGenerator` class provides methods for generating and managing JSON Web Keys (JWK) for cryptographic operations. It supports RSA, EC, and symmetric keys, and provides thumbprint computation and key serialization.
#### Features
- Key Generation: Generate RSA, EC, and symmetric (octet) JWKs, with or without private key material.
- Thumbprint Computation: Compute RFC 7638-compliant thumbprints for JWKs.
- Key Serialization: Export and import JWKs for interoperability.
- Try Pattern: All methods use the Try-pattern for safe error handling.
#### Example Usage
```csharp
// Generate a new RSA JWK (public only)
JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk, out var error);
// Generate a new EC JWK (private)
JwkGenerator.TryGenerateEc(JwkCurve.P256, true, null, null, null, out var ecJwk, out var error);
// Compute a thumbprint
JwkGenerator.TryComputeThumbprint(jwk, out var thumbprint, out var error);
```
---
### JWS Generator
The `JwsGenerator` class provides methods for creating and verifying JSON Web Signatures (JWS) using JWKs. It supports signing payloads with RSA keys and produces JWS objects with protected headers, payload, and signature.
#### Features
- JWS Creation: Sign string, byte[], or object payloads using RSA JWKs.
- Try Pattern: All methods use the Try-pattern for safe error handling.
- Key Authorization: Generate key authorization strings for ACME/Let's Encrypt flows.
#### Example Usage
```csharp
// Sign a payload
JwsGenerator.TryEncode(rsa, jwk, new JwsHeader(), "payload", out var jws, out var error);
// Generate key authorization
JwsGenerator.TryGetKeyAuthorization(jwk, "token", out var keyAuth, out var error);
```
---
### JWT Generator
The `JwtGenerator` class provides methods for generating and validating JSON Web Tokens (JWTs).
#### Features
- Token Generation: Generate JWTs with claims and metadata.
- Token Validation: Validate JWTs against a secret.
#### Example Usage
```csharp
JwtGenerator.TryGenerateToken(secret, issuer, audience, 60, "user", roles, out var token, out var error);
```
---
### TOTP Generator
The `TotpGenerator` class provides methods for generating and validating Time-Based One-Time Passwords (TOTP).

View File

@ -0,0 +1,107 @@
using MaksIT.Core.Security.JWK;
namespace MaksIT.Core.Tests.Security;
public class JwkGeneratorTests
{
[Fact]
public void GenerateRsa_PublicKey_ShouldHaveRequiredFields()
{
var result = JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jwk);
Assert.Equal(JwkKeyType.Rsa.Name, jwk.Kty);
Assert.NotNull(jwk.N);
Assert.NotNull(jwk.E);
Assert.Null(jwk.D);
Assert.Null(jwk.P);
Assert.Null(jwk.Q);
Assert.Null(jwk.DP);
Assert.Null(jwk.DQ);
Assert.Null(jwk.QI);
Assert.False(string.IsNullOrWhiteSpace(jwk.Kid));
}
[Fact]
public void GenerateRsa_PrivateKey_ShouldHavePrivateFields()
{
var result = JwkGenerator.TryGenerateRsa(2048, true, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jwk);
Assert.Equal(JwkKeyType.Rsa.Name, jwk.Kty);
Assert.NotNull(jwk.D);
Assert.NotNull(jwk.P);
Assert.NotNull(jwk.Q);
Assert.NotNull(jwk.DP);
Assert.NotNull(jwk.DQ);
Assert.NotNull(jwk.QI);
}
[Theory]
[InlineData("P-256")]
[InlineData("P-384")]
[InlineData("P-521")]
public void GenerateEc_PublicKey_ShouldHaveRequiredFields(string curve)
{
var curveObj = JwkCurve.GetAll<JwkCurve>().First(c => c.Name == curve);
var result = JwkGenerator.TryGenerateEc(curveObj, false, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jwk);
Assert.Equal(JwkKeyType.Ec.Name, jwk.Kty);
Assert.Equal(curve, jwk.Crv);
Assert.NotNull(jwk.X);
Assert.NotNull(jwk.Y);
Assert.Null(jwk.D_EC);
Assert.False(string.IsNullOrWhiteSpace(jwk.Kid));
}
[Theory]
[InlineData("P-256")]
[InlineData("P-384")]
[InlineData("P-521")]
public void GenerateEc_PrivateKey_ShouldHavePrivateField(string curve)
{
var curveObj = JwkCurve.GetAll<JwkCurve>().First(c => c.Name == curve);
var result = JwkGenerator.TryGenerateEc(curveObj, true, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jwk);
Assert.Equal(JwkKeyType.Ec.Name, jwk.Kty);
Assert.Equal(curve, jwk.Crv);
Assert.NotNull(jwk.D_EC);
}
[Fact]
public void GenerateOct_ShouldHaveRequiredFields()
{
var result = JwkGenerator.TryGenerateOct(256, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jwk);
Assert.Equal(JwkKeyType.Oct.Name, jwk.Kty);
Assert.NotNull(jwk.K);
Assert.False(string.IsNullOrWhiteSpace(jwk.Kid));
}
[Fact]
public void TryComputeThumbprint_ShouldReturnValidThumbprint()
{
var result = JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jwk);
var thumbResult = JwkGenerator.TryComputeThumbprint(jwk, out var thumb, out var error);
Assert.True(thumbResult, error);
Assert.False(string.IsNullOrWhiteSpace(thumb));
Assert.Null(error);
}
[Fact]
public void ComputeKid_ShouldBeUniqueForDifferentKeys()
{
var result1 = JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk1, out var errorMessage1);
var result2 = JwkGenerator.TryGenerateRsa(2048, false, null, null, null, out var jwk2, out var errorMessage2);
Assert.True(result1, errorMessage1);
Assert.True(result2, errorMessage2);
Assert.NotNull(jwk1);
Assert.NotNull(jwk2);
Assert.NotEqual(jwk1.Kid, jwk2.Kid);
}
}

View File

@ -0,0 +1,84 @@
using System.Text;
using System.Security.Cryptography;
using MaksIT.Core.Security.JWK;
using MaksIT.Core.Security.JWS;
namespace MaksIT.Core.Tests.Security;
public class JwsGeneratorTests {
private static (RSA rsa, Jwk jwk) GenerateRsaAndJwk() {
var rsa = RSA.Create(2048);
var result = JwkGenerator.TryGenerateRsaFromRsa(rsa, true, null, null, null, out var jwk, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jwk);
return (rsa, jwk);
}
[Fact]
public void Encode_WithStringPayload_ProducesValidJws() {
var (rsa, jwk) = GenerateRsaAndJwk();
var header = new JwsHeader();
var payload = "test-payload";
var result = JwsGenerator.TryEncode(rsa, jwk, header, payload, out var jws, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jws);
Assert.False(string.IsNullOrEmpty(jws.Protected));
Assert.False(string.IsNullOrEmpty(jws.Payload));
Assert.False(string.IsNullOrEmpty(jws.Signature));
}
[Fact]
public void Encode_WithByteArrayPayload_ProducesValidJws() {
var (rsa, jwk) = GenerateRsaAndJwk();
var header = new JwsHeader();
var payload = Encoding.UTF8.GetBytes("test-bytes");
var result = JwsGenerator.TryEncode(rsa, jwk, header, payload, out var jws, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jws);
Assert.False(string.IsNullOrEmpty(jws.Protected));
Assert.False(string.IsNullOrEmpty(jws.Payload));
Assert.False(string.IsNullOrEmpty(jws.Signature));
}
[Fact]
public void Encode_WithGenericPayload_ProducesValidJws() {
var (rsa, jwk) = GenerateRsaAndJwk();
var header = new JwsHeader();
var payload = new { foo = "bar", n = 42 };
var result = JwsGenerator.TryEncode(rsa, jwk, header, payload, out var jws, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jws);
Assert.False(string.IsNullOrEmpty(jws.Protected));
Assert.False(string.IsNullOrEmpty(jws.Payload));
Assert.False(string.IsNullOrEmpty(jws.Signature));
}
[Fact]
public void Encode_PostAsGet_ProducesValidJws() {
var (rsa, jwk) = GenerateRsaAndJwk();
var header = new JwsHeader();
var result = JwsGenerator.TryEncode(rsa, jwk, header, out var jws, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(jws);
Assert.False(string.IsNullOrEmpty(jws.Protected));
Assert.Equal(string.Empty, jws.Payload);
Assert.False(string.IsNullOrEmpty(jws.Signature));
}
[Fact]
public void GetKeyAuthorization_ReturnsExpectedFormat() {
var (rsa, jwk) = GenerateRsaAndJwk();
var token = "test-token";
var result = JwsGenerator.TryGetKeyAuthorization(jwk, token, out var keyAuth, out var errorMessage);
Assert.True(result, errorMessage);
Assert.NotNull(keyAuth);
Assert.StartsWith(token + ".", keyAuth);
Assert.True(keyAuth.Length > token.Length + 1);
}
}

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata -->
<PackageId>MaksIT.Core</PackageId>
<Version>1.5.3</Version>
<Version>1.5.4</Version>
<Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company>
<Product>MaksIT.Core</Product>

View File

@ -0,0 +1,55 @@
using System;
using System.Buffers;
using System.Text;
namespace MaksIT.Core.Security.JWK;
/// <summary>
/// Provides RFC 4648-compliant Base64Url encoding and decoding utilities.
/// </summary>
public static class Base64UrlUtility
{
/// <summary>
/// Encodes a byte array to a Base64Url string (RFC 4648 §5).
/// </summary>
public static string Encode(byte[] data)
{
if (data == null) throw new ArgumentNullException(nameof(data));
string base64 = Convert.ToBase64String(data);
return base64.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
/// <summary>
/// Encodes a UTF-8 string to a Base64Url string (RFC 4648 §5).
/// </summary>
public static string Encode(string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
return Encode(Encoding.UTF8.GetBytes(value));
}
/// <summary>
/// Decodes a Base64Url string to a byte array.
/// </summary>
public static byte[] Decode(string base64Url)
{
if (base64Url == null) throw new ArgumentNullException(nameof(base64Url));
string padded = base64Url.Replace('-', '+').Replace('_', '/');
switch (base64Url.Length % 4)
{
case 2: padded += "=="; break;
case 3: padded += "="; break;
}
return Convert.FromBase64String(padded);
}
/// <summary>
/// Decodes a Base64Url string to a UTF-8 string.
/// </summary>
public static string DecodeToString(string base64Url)
{
return Encoding.UTF8.GetString(Decode(base64Url));
}
}

View File

@ -0,0 +1,73 @@
using System.Text.Json.Serialization;
namespace MaksIT.Core.Security.JWK;
/// <summary>
/// Standard JWK class supporting RSA, EC, and octet keys.
/// </summary>
public class Jwk {
// Common fields
[JsonPropertyName("kty")]
public string? Kty { get; set; }
[JsonPropertyName("kid")]
public string? Kid { get; set; }
[JsonPropertyName("alg")]
public string? Alg { get; set; }
[JsonPropertyName("use")]
public string? Use { get; set; }
[JsonPropertyName("key_ops")]
public string[]? KeyOps { get; set; }
// RSA fields
[JsonPropertyName("n")]
public string? N { get; set; } // Modulus
[JsonPropertyName("e")]
public string? E { get; set; } // Exponent
[JsonPropertyName("d")]
public string? D { get; set; } // Private exponent
[JsonPropertyName("p")]
public string? P { get; set; }
[JsonPropertyName("q")]
public string? Q { get; set; }
[JsonPropertyName("dp")]
public string? DP { get; set; }
[JsonPropertyName("dq")]
public string? DQ { get; set; }
[JsonPropertyName("qi")]
public string? QI { get; set; }
// EC fields
[JsonPropertyName("crv")]
public string? Crv { get; set; }
[JsonPropertyName("x")]
public string? X { get; set; }
[JsonPropertyName("y")]
public string? Y { get; set; }
[JsonPropertyName("d_ec")]
public string? D_EC { get; set; } // EC private key
// Symmetric (octet) fields
[JsonPropertyName("k")]
public string? K { get; set; }
// Backward compatibility for old code
[JsonIgnore]
public string? Exponent { get => E; set => E = value; }
[JsonIgnore]
public string? Modulus { get => N; set => N = value; }
}

View File

@ -0,0 +1,17 @@
using MaksIT.Core.Abstractions;
namespace MaksIT.Core.Security.JWK;
public sealed class JwkAlgorithm : Enumeration {
public static readonly JwkAlgorithm Rs256 = new(1, "RS256");
public static readonly JwkAlgorithm Rs512 = new(2, "RS512");
public static readonly JwkAlgorithm Es256 = new(3, "ES256");
public static readonly JwkAlgorithm Es384 = new(4, "ES384");
public static readonly JwkAlgorithm Es512 = new(5, "ES512");
public static readonly JwkAlgorithm A128Gcm = new(6, "A128GCM");
public static readonly JwkAlgorithm A256Gcm = new(7, "A256GCM");
public static readonly JwkAlgorithm A512Gcm = new(8, "A512GCM");
private JwkAlgorithm(int id, string name) : base(id, name) { }
}

View File

@ -0,0 +1,12 @@
using MaksIT.Core.Abstractions;
namespace MaksIT.Core.Security.JWK;
public sealed class JwkCurve : Enumeration {
public static readonly JwkCurve P256 = new(1, "P-256");
public static readonly JwkCurve P384 = new(2, "P-384");
public static readonly JwkCurve P521 = new(3, "P-521");
private JwkCurve(int id, string name) : base(id, name) { }
}

View File

@ -0,0 +1,213 @@
using System.Text;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Diagnostics.CodeAnalysis;
using MaksIT.Core.Extensions;
namespace MaksIT.Core.Security.JWK;
/// <summary>
/// Provides utilities for JWK (JSON Web Key) operations, including RFC 7638 thumbprint computation and key generation.
/// </summary>
public static class JwkGenerator {
public static bool TryGenerateRsa(int keySize, bool includePrivate, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) {
try {
jwk = GenerateRsa(keySize, includePrivate, alg, use, keyOps);
errorMessage = null;
return true;
} catch (Exception ex) {
jwk = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryGenerateEc(JwkCurve? curve, bool includePrivate, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) {
try {
jwk = GenerateEc(curve, includePrivate, alg, use, keyOps);
errorMessage = null;
return true;
} catch (Exception ex) {
jwk = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryGenerateOct(int keySizeBits, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) {
try {
jwk = GenerateOct(keySizeBits, alg, use, keyOps);
errorMessage = null;
return true;
} catch (Exception ex) {
jwk = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryGenerateRsaFromRsa(RSA rsa, bool includePrivate, JwkAlgorithm? alg, string? use, string[]? keyOps, [NotNullWhen(true)] out Jwk? jwk, [NotNullWhen(false)] out string? errorMessage) {
try {
jwk = GenerateRsaFromRsa(rsa, includePrivate, alg, use, keyOps);
errorMessage = null;
return true;
} catch (Exception ex) {
jwk = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryComputeThumbprint(
Jwk jwk,
[NotNullWhen(true)] out string? thumbprint,
[NotNullWhen(false)] out string? errorMessage) {
thumbprint = null;
errorMessage = null;
if (jwk == null) {
errorMessage = "JWK cannot be null.";
return false;
}
if (string.IsNullOrEmpty(jwk.E) || string.IsNullOrEmpty(jwk.N)) {
errorMessage = "JWK must have Exponent and Modulus set.";
return false;
}
try {
// RFC 7638: Lexicographic order: e, kty, n
var orderedJwk = new OrderedJwk {
E = jwk.E!,
Kty = "RSA",
N = jwk.N!
};
var json = orderedJwk.ToJson();
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
thumbprint = Base64UrlEncode(hash);
return true;
}
catch (Exception ex) {
errorMessage = ex.Message;
return false;
}
}
private static Jwk GenerateRsa(int keySize = 2048, bool includePrivate = false, JwkAlgorithm? alg = null, string? use = null, string[]? keyOps = null) {
using var rsa = RSA.Create(keySize);
var parameters = rsa.ExportParameters(includePrivate);
var jwk = new Jwk {
Kty = JwkKeyType.Rsa.Name,
N = Base64UrlEncode(parameters.Modulus!),
E = Base64UrlEncode(parameters.Exponent!),
Alg = (alg ?? (keySize >= 4096 ? JwkAlgorithm.Rs512 : JwkAlgorithm.Rs256)).Name,
Use = use,
KeyOps = keyOps,
};
if (includePrivate) {
jwk.D = Base64UrlEncode(parameters.D!);
jwk.P = Base64UrlEncode(parameters.P!);
jwk.Q = Base64UrlEncode(parameters.Q!);
jwk.DP = Base64UrlEncode(parameters.DP!);
jwk.DQ = Base64UrlEncode(parameters.DQ!);
jwk.QI = Base64UrlEncode(parameters.InverseQ!);
}
jwk.Kid = ComputeKid(jwk);
return jwk;
}
private static Jwk GenerateEc(JwkCurve? curve = null, bool includePrivate = false, JwkAlgorithm? alg = null, string? use = null, string[]? keyOps = null) {
curve ??= JwkCurve.P256;
ECCurve ecCurve = curve.Name switch {
"P-256" => ECCurve.NamedCurves.nistP256,
"P-384" => ECCurve.NamedCurves.nistP384,
"P-521" => ECCurve.NamedCurves.nistP521,
_ => throw new ArgumentException($"Unsupported curve: {curve.Name}")
};
using var ec = ECDsa.Create(ecCurve);
var parameters = ec.ExportParameters(includePrivate);
var jwk = new Jwk {
Kty = JwkKeyType.Ec.Name,
Crv = curve.Name,
X = Base64UrlEncode(parameters.Q.X!),
Y = Base64UrlEncode(parameters.Q.Y!),
Alg = (alg ?? (curve == JwkCurve.P384 ? JwkAlgorithm.Es384 : curve == JwkCurve.P521 ? JwkAlgorithm.Es512 : JwkAlgorithm.Es256)).Name,
Use = use,
KeyOps = keyOps,
};
if (includePrivate && parameters.D != null) {
jwk.D_EC = Base64UrlEncode(parameters.D);
}
jwk.Kid = ComputeKid(jwk);
return jwk;
}
private static Jwk GenerateOct(int keySizeBits = 256, JwkAlgorithm? alg = null, string? use = null, string[]? keyOps = null) {
var key = RandomNumberGenerator.GetBytes(keySizeBits / 8);
var jwk = new Jwk {
Kty = JwkKeyType.Oct.Name,
K = Base64UrlEncode(key),
Alg = (alg ?? (keySizeBits == 256 ? JwkAlgorithm.A256Gcm : keySizeBits == 128 ? JwkAlgorithm.A128Gcm : JwkAlgorithm.A512Gcm)).Name,
Use = use,
KeyOps = keyOps,
};
jwk.Kid = ComputeKid(jwk);
return jwk;
}
private static Jwk GenerateRsaFromRsa(RSA rsa, bool includePrivate = false, JwkAlgorithm? alg = null, string? use = null, string[]? keyOps = null) {
if (rsa == null) throw new ArgumentNullException(nameof(rsa));
var parameters = rsa.ExportParameters(includePrivate);
var jwk = new Jwk {
Kty = JwkKeyType.Rsa.Name,
N = Base64UrlUtility.Encode(parameters.Modulus!),
E = Base64UrlUtility.Encode(parameters.Exponent!),
Alg = (alg ?? JwkAlgorithm.Rs256).Name,
Use = use,
KeyOps = keyOps,
};
if (includePrivate) {
jwk.D = Base64UrlUtility.Encode(parameters.D!);
jwk.P = Base64UrlUtility.Encode(parameters.P!);
jwk.Q = Base64UrlUtility.Encode(parameters.Q!);
jwk.DP = Base64UrlUtility.Encode(parameters.DP!);
jwk.DQ = Base64UrlUtility.Encode(parameters.DQ!);
jwk.QI = Base64UrlUtility.Encode(parameters.InverseQ!);
}
jwk.Kid = ComputeKid(jwk);
return jwk;
}
private static string Base64UrlEncode(byte[] data) {
return Base64UrlUtility.Encode(data);
}
private static string ComputeKid(Jwk jwk) {
// Use thumbprint as kid if possible
if (jwk.Kty == "RSA" && !string.IsNullOrEmpty(jwk.N) && !string.IsNullOrEmpty(jwk.E)) {
TryComputeThumbprint(jwk, out var thumb, out _);
return thumb ?? Guid.NewGuid().ToString("N");
}
// For EC and oct, use a hash of the key material
using var sha = SHA256.Create();
string keyMaterial = jwk.Kty switch {
"EC" => jwk.X + jwk.Y + jwk.Crv,
"oct" => jwk.K,
_ => null
} ?? Guid.NewGuid().ToString();
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(keyMaterial));
return Base64UrlEncode(hash);
}
// Helper class for correct property order and names
private class OrderedJwk {
[JsonPropertyName("e")]
public string E { get; set; } = default!;
[JsonPropertyName("kty")]
public string Kty { get; set; } = default!;
[JsonPropertyName("n")]
public string N { get; set; } = default!;
}
}

View File

@ -0,0 +1,12 @@
using MaksIT.Core.Abstractions;
namespace MaksIT.Core.Security.JWK;
public sealed class JwkKeyType : Enumeration {
public static readonly JwkKeyType Rsa = new(1, "RSA");
public static readonly JwkKeyType Ec = new(2, "EC");
public static readonly JwkKeyType Oct = new(3, "oct");
private JwkKeyType(int id, string name) : base(id, name) { }
}

View File

@ -0,0 +1,117 @@
using System.Text;
using System.Security.Cryptography;
using System.Diagnostics.CodeAnalysis;
using MaksIT.Core.Security.JWK;
using MaksIT.Core.Extensions;
namespace MaksIT.Core.Security.JWS;
public static class JwsGenerator {
public static bool TryEncode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, string? payload, [NotNullWhen(true)] out JwsMessage? jwsMessage, [NotNullWhen(false)] out string? errorMessage, string? keyId = null) {
try {
jwsMessage = Encode(rsa, jwk, protectedHeader, payload, keyId);
errorMessage = null;
return true;
} catch (Exception ex) {
jwsMessage = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryEncode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, byte[] payload, [NotNullWhen(true)] out JwsMessage? jwsMessage, [NotNullWhen(false)] out string? errorMessage, string? keyId = null) {
try {
jwsMessage = Encode(rsa, jwk, protectedHeader, payload, keyId);
errorMessage = null;
return true;
} catch (Exception ex) {
jwsMessage = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryEncode<T>(RSA rsa, Jwk jwk, JwsHeader protectedHeader, T payload, [NotNullWhen(true)] out JwsMessage? jwsMessage, [NotNullWhen(false)] out string? errorMessage, string? keyId = null) {
try {
jwsMessage = Encode(rsa, jwk, protectedHeader, payload, keyId);
errorMessage = null;
return true;
} catch (Exception ex) {
jwsMessage = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryEncode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, [NotNullWhen(true)] out JwsMessage? jwsMessage, [NotNullWhen(false)] out string? errorMessage, string? keyId = null) {
try {
jwsMessage = Encode(rsa, jwk, protectedHeader, keyId);
errorMessage = null;
return true;
} catch (Exception ex) {
jwsMessage = null;
errorMessage = ex.Message;
return false;
}
}
public static bool TryGetKeyAuthorization(Jwk jwk, string token, [NotNullWhen(true)] out string? keyAuthorization, [NotNullWhen(false)] out string? errorMessage) {
keyAuthorization = null;
errorMessage = null;
if (!JwkGenerator.TryComputeThumbprint(jwk, out var thumb, out errorMessage)) {
return false;
}
keyAuthorization = $"{token}.{thumb}";
return true;
}
private static JwsMessage Encode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, string? payload, string? keyId = null) {
return EncodeInternal(rsa, jwk, protectedHeader, payload, keyId);
}
private static JwsMessage Encode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, byte[] payload, string? keyId = null) {
string encodedPayload = Base64UrlUtility.Encode(payload);
return EncodeInternal(rsa, jwk, protectedHeader, encodedPayload, keyId, isPayloadEncoded: true);
}
private static JwsMessage Encode<T>(RSA rsa, Jwk jwk, JwsHeader protectedHeader, T payload, string? keyId = null) {
string encodedPayload = Base64UrlUtility.Encode(payload.ToJson());
return EncodeInternal(rsa, jwk, protectedHeader, encodedPayload, keyId, isPayloadEncoded: true);
}
private static JwsMessage Encode(RSA rsa, Jwk jwk, JwsHeader protectedHeader, string? keyId = null) {
// POST-as-GET: empty payload
return EncodeInternal(rsa, jwk, protectedHeader, null, keyId);
}
private static JwsMessage EncodeInternal(
RSA rsa,
Jwk jwk,
JwsHeader protectedHeader,
string? payload,
string? keyId,
bool isPayloadEncoded = false) {
protectedHeader.Algorithm = JwkAlgorithm.Rs256.Name;
if (!string.IsNullOrEmpty(keyId))
protectedHeader.KeyId = keyId;
else
protectedHeader.Key = jwk;
var message = new JwsMessage {
Payload = string.Empty,
Protected = Base64UrlUtility.Encode(protectedHeader.ToJson())
};
if (payload != null) {
message.Payload = isPayloadEncoded ? payload : Base64UrlUtility.Encode(payload);
}
var signingInput = Encoding.ASCII.GetBytes($"{message.Protected}.{message.Payload}");
message.Signature = Base64UrlUtility.Encode(
rsa.SignData(signingInput, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
return message;
}
}

View File

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace MaksIT.Core.Security.JWS;
public class JwsHeader {
[JsonPropertyName("alg")]
public string? Algorithm { get; set; }
[JsonPropertyName("kid")]
public string? KeyId { get; set; }
[JsonPropertyName("jwk")]
public object? Key { get; set; }
}

View File

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
namespace MaksIT.Core.Security.JWS;
public class JwsMessage {
[JsonPropertyName("protected")]
public string Protected { get; set; } = string.Empty;
[JsonPropertyName("payload")]
public string Payload { get; set; } = string.Empty;
[JsonPropertyName("signature")]
public string Signature { get; set; } = string.Empty;
}

View File

@ -1,12 +1,9 @@
using MaksIT.Core.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Core.Security.JWT;
public class CustomClaims : Enumeration {
public static readonly CustomClaims AclEntry = new(1, "acl_entry");
private CustomClaims(int id, string name) : base(id, name) { }
}

View File

@ -1,16 +1,13 @@
using Microsoft.IdentityModel.Tokens;
using System.Diagnostics.CodeAnalysis;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using System.Diagnostics.CodeAnalysis;
using Microsoft.IdentityModel.Tokens;
namespace MaksIT.Core.Security.JWT;
public static class JwtGenerator {
/// <summary>
/// Attempts to generate a JWT token using the specified request parameters.