Still working on getting client credential flow complete, most of the request works but still working on generating the JWT.

This commit is contained in:
eelke 2026-03-16 19:15:04 +01:00
parent 1a8c63808a
commit 8782ef39c6
80 changed files with 1331 additions and 414 deletions

View file

@ -0,0 +1,26 @@
using System.Text.Json;
namespace IdentityShroud.Core;
public interface IJwtSignatureProvider : IDisposable
{
/*
Of the signature and MAC algorithms specified in JSON Web Algorithms
[JWA], only HMAC SHA-256 ("HS256") and "none" MUST be implemented by
conforming JWT implementations. It is RECOMMENDED that
implementations also support RSASSA-PKCS1-v1_5 with the SHA-256 hash
algorithm ("RS256") and ECDSA using the P-256 curve and the SHA-256
hash algorithm ("ES256"). Support for other algorithms and key sizes
is OPTIONAL.
*/
void WriteJwtHeaderFields(Utf8JsonWriter writer);
/// <summary>
/// Length of the binary signature in bytes.
/// </summary>
/// <returns></returns>
int GetSignatureLength();
void CalculateSignature(ReadOnlySpan<byte> jwt, Span<byte> signatureOut);
}

View file

@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
namespace IdentityShroud.Core;
[SuppressMessage("ReSharper", "InconsistentNaming")]
public readonly record struct JwtSigAlgName(string Name) : IEquatable<JwtSigAlgName>
{
// HMAC using SHA-???
public static JwtSigAlgName HS256 => new("HS256"); // REQUIRED
public static JwtSigAlgName HS384 => new("HS384");
public static JwtSigAlgName HS512 => new("HS512");
// RSASSA-PKCS1-v1_5 using SHA-???
public static JwtSigAlgName RS256 => new("RS256");
public static JwtSigAlgName RS384 => new("RS384");
public static JwtSigAlgName RS512 => new("RS512");
public static JwtSigAlgName ES256 => new("ES256"); // ECDSA using P-256 and SHA-256
public static JwtSigAlgName ES384 => new("ES384"); // ECDSA using P-384 and SHA-384
public static JwtSigAlgName ES512 => new("ES512"); // ECDSA using P-521 and SHA-512
public override string ToString() => Name;
}

View file

@ -0,0 +1,100 @@
using System.Buffers.Text;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.WebUtilities;
namespace IdentityShroud.Core;
public static class JwtSignatureGenerator
{
/// <summary>
/// Generates a JWT signature using RS256 algorithm
/// </summary>
/// <param name="headerBase64Url">Base64Url encoded header</param>
/// <param name="payloadBase64Url">Base64Url encoded payload</param>
/// <param name="privateKey">RSA private key (PEM format or RSA parameters)</param>
/// <returns>Base64Url encoded signature</returns>
public static string GenerateRS256Signature(string headerBase64Url, string payloadBase64Url, RSA privateKey)
{
// Combine header and payload with a period
string dataToSign = $"{headerBase64Url}.{payloadBase64Url}";
// Convert to bytes
byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign);
// Sign the data using RSA-SHA256
byte[] signatureBytes = privateKey.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// Convert signature to Base64Url encoding
string signature = WebEncoders.Base64UrlEncode(signatureBytes);
return signature;
}
public static string GenerateCompleteJwt(string headerBase64Url, string payloadBase64Url, RSA privateKey)
{
string signature = GenerateRS256Signature(headerBase64Url, payloadBase64Url, privateKey);
return $"{headerBase64Url}.{payloadBase64Url}.{signature}";
}
}
public static class JwtCreator
{
public static byte[] CreateEncodedJwt(ReadOnlySpan<byte> payloadUtf8, IJwtSignatureProvider signatureProvider)
{
MemoryStream memStream = new();
Utf8JsonWriter writer = new(memStream);
WriteJwtHeader(writer, signatureProvider);
writer.Flush();
memStream.Seek(0, SeekOrigin.Begin);
int headerBase64Length = Base64Url.GetEncodedLength((int)memStream.Length);
int payloadBase64Length = Base64Url.GetEncodedLength(payloadUtf8.Length);
int signatureBase64Length = Base64Url.GetEncodedLength(signatureProvider.GetSignatureLength());
int totalLength = headerBase64Length + 1 + payloadBase64Length + 1 + signatureBase64Length;
var completeJwt = new byte[totalLength];
//
var byteArray = new byte[memStream.Length];
memStream.ReadExactly(byteArray, 0, (int)memStream.Length);
int written = Base64Url.EncodeToUtf8(byteArray, completeJwt);
if (written != headerBase64Length)
throw new Exception("expected header length did not match bytes written");
completeJwt[headerBase64Length] = (byte)'.';
written = Base64Url.EncodeToUtf8(payloadUtf8, completeJwt.AsSpan().Slice(headerBase64Length + 1, payloadBase64Length));
if (written != payloadBase64Length)
throw new Exception("expected payload length did not match bytes written");
completeJwt[headerBase64Length + 1 + payloadBase64Length] = (byte)'.';
Span<byte> signature = stackalloc byte[signatureProvider.GetSignatureLength()];
signatureProvider.CalculateSignature(
completeJwt.AsSpan().Slice(0, headerBase64Length + 1 + payloadBase64Length),
signature);
written = Base64Url.EncodeToUtf8(signature, completeJwt.AsSpan()
.Slice(headerBase64Length + 1 + payloadBase64Length + 1));
if (written != signatureBase64Length)
throw new Exception("expected signature length did not match bytes written");
return completeJwt;
}
private static void WriteJwtHeader(Utf8JsonWriter writer, IJwtSignatureProvider signatureProvider)
{
writer.WriteStartObject();
writer.WriteString("typ"u8, "JWT"u8);
signatureProvider.WriteJwtHeaderFields(writer);
writer.WriteEndObject();
}
}

View file

@ -0,0 +1,88 @@
using System.Security.Cryptography;
using System.Text.Json;
using IdentityShroud.Core.Model;
using IdentityShroud.Core.Services;
namespace IdentityShroud.Core;
public class SignatureProviderFactory(DekEncryptionService dekCryptor, IServiceProvider services)
{
public static void SelectAlgorithmAndKey(Realm realm, Client client, out JwtSigAlgName alg, out byte[] key)
{
throw new NotImplementedException();
}
public IJwtSignatureProvider Create(JwtSigAlgName algorithm, byte[] keyData)
{
//realm.DefaultSignatureAlgorithm
//realm.TokenSigningKeys
//IJwtSignatureProvider? sigProvider = services.GetKeyedService<IJwtSignatureProvider>();
throw new NotImplementedException();
}
}
public class RsaJwtSignatureProvider : IJwtSignatureProvider
{
private JwtSigAlgName _sigAlgName;
private RealmSigningKeyId _keyId;
private readonly RSA _rsa;
public RsaJwtSignatureProvider(DekEncryptionService dekCryptor,
RealmSigningKey privateKey,
JwtSigAlgName sigAlgName)
{
_sigAlgName = sigAlgName;
_keyId = privateKey.Id;
byte[] key = dekCryptor.Decrypt(privateKey.Key);
_rsa = RSA.Create();
_rsa.ImportPkcs8PrivateKey(key, out int _);
}
/*
+-------------------+---------------------------------+
| "alg" Param Value | Digital Signature Algorithm |
+-------------------+---------------------------------+
| RS256 | RSASSA-PKCS1-v1_5 using SHA-256 |
| RS384 | RSASSA-PKCS1-v1_5 using SHA-384 |
| RS512 | RSASSA-PKCS1-v1_5 using SHA-512 |
+-------------------+---------------------------------+
*/
public void WriteJwtHeaderFields(Utf8JsonWriter writer)
{
writer.WriteString("alg"u8, _sigAlgName.ToString());
writer.WriteString("kid"u8, _keyId.ToString());
}
public int GetSignatureLength()
{
return _rsa.KeySize / 8;
}
public void CalculateSignature(ReadOnlySpan<byte> jwt, Span<byte> sig)
{
_rsa.SignData(jwt, sig, GetHashAlgorithmName(), RSASignaturePadding.Pkcs1);
}
public void Dispose()
{
_rsa.Dispose();
}
private HashAlgorithmName GetHashAlgorithmName()
=> _sigAlgName.Name switch
{
"RS256" => HashAlgorithmName.SHA256,
"RS384" => HashAlgorithmName.SHA384,
"RS512" => HashAlgorithmName.SHA512,
_ => throw new ArgumentException("Invalid algorithm for RsaJwtSignatureProvider")
};
}