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
{
///
/// Generates a JWT signature using RS256 algorithm
///
/// Base64Url encoded header
/// Base64Url encoded payload
/// RSA private key (PEM format or RSA parameters)
/// Base64Url encoded signature
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 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 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();
}
}