IdentityShroud/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs
2026-02-14 14:38:30 +01:00

83 lines
No EOL
6.4 KiB
C#

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using IdentityShroud.Core.Messages;
using Microsoft.AspNetCore.WebUtilities;
namespace IdentityShroud.Core.Tests;
public class JwtSignatureGeneratorTests
{
[Fact]
public void VerifySignatureValid()
{
using var rsa = RSA.Create(2048);
string header = WebEncoders.Base64UrlEncode("fake header"u8.ToArray());
string payload = WebEncoders.Base64UrlEncode("fake payload"u8.ToArray());
var jwtString = JwtSignatureGenerator.GenerateCompleteJwt(header, payload, rsa);
Assert.True(ValidateJwtSignature(jwtString, rsa));
}
/// <summary>
/// This test is to prove our signature verification code is correct. The inputs are
/// all from a production keycloak instance.
/// </summary>
[Fact]
public void ValidateKeycloakSignature()
{
string keycloakGeneratedJwt =
"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJybVZ3TU5rM0o1WHlmMWhyS3NVbEVYN1BNUm42dlZKY0h3U3FYMUVQRnFJIn0.eyJleHAiOjE3NzEwNTQxMDksImlhdCI6MTc3MTA1MzgwOSwiYXV0aF90aW1lIjoxNzcxMDUzODA4LCJqdGkiOiI5MTEzZjEwNC03YzllLTQzNzItYmU4Yy03NDMwMmI1ZTU1NGUiLCJpc3MiOiJodHRwczovL2lhbS5rYXNzYWNsb3VkLm5sL2F1dGgvcmVhbG1zL21wbHVza2Fzc2EiLCJhdWQiOlsia2Fzc2EtbWFuYWdlbWVudC1zZXJ2aWNlIiwiYXBhY2hlMi1pbnRyYW5ldC1hdXRoIiwiYWNjb3VudCJdLCJzdWIiOiIwOTNjY2YxNS1jNGE5LTRhYjQtOTcxZi1kNWEwMjIzNmQ4NWEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJkZWFsZXJfc3VwcG9ydCIsInNpZCI6IjRiYjI0OGQ1LWVkNzktNGU0Yy05NWNjLTAwYzgzMzliZmZiMyIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cHM6Ly9tcGx1c2thc3NhLm9ubGluZSIsImh0dHBzOi8vd3d3Lm1wbHVza2Fzc2Euc3VwcG9ydCIsImh0dHBzOi8vbXBsdXNrYXNzYS5zdXBwb3J0IiwiaHR0cDovL2xvY2FsaG9zdDo0MDkwIiwiaHR0cHM6Ly93d3cubXBsdXNrYXNzYS5vbmxpbmUiLCJodHRwOi8vbG9jYWxob3N0IiwiLyoiLCJodHRwOi8vbG9jYWxob3N0OjQyMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtbXBsdXNrYXNzYSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJkZWFsZXItbWVkZXdlcmtlci1yb2xlIiwibXBsdXNrYXNzYS1tZWRld2Vya2VyLXJvbGUiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhcGFjaGUyLWludHJhbmV0LWF1dGgiOnsicm9sZXMiOlsiaW50cmFuZXQiLCJyZWxlYXNlbm90ZXNfd3JpdGUiXX0sImthc3NhLW1hbmFnZW1lbnQtc2VydmljZSI6eyJyb2xlcyI6WyJwb3NhY2NvdW50X3Bhc3N3b3JkcmVzZXQiLCJkcmFmdF9saWNlbnNlX3dyaXRlIiwibGljZW5zZV9yZWFkIiwia25vd2xlZGdlSXRlbV9yZWFkIiwibWFpbGluZ19yZWFkIiwibXBsdXNhcGlfcmVhZCIsImRhdGFiYXNlX3VzZXJfd3JpdGUiLCJlbnZpcm9ubWVudF93cml0ZSIsImdrc19hdXRoY29kZV9yZWFkIiwiZW1wbG95ZWVfcmVhZCIsImRhdGFiYXNlX3VzZXJfcmVhZCIsImFwaWFjY291bnRfcGFzc3dvcmRyZXNldCIsIm1wbHVzYXBpX3dyaXRlIiwiZW52aXJvbm1lbnRfcmVhZCIsImtub3dsZWRnZUl0ZW1fd3JpdGUiLCJkYXRhYmFzZV91c2VyX3Bhc3N3b3JkX3JlYWQiLCJsaWNlbnNlX3dyaXRlIiwiY3VzdG9tZXJfd3JpdGUiLCJkZWFsZXJfcmVhZCIsImVtcGxveWVlX3dyaXRlIiwiZGF0YWJhc2VfY29uZmlndXJhdGlvbl93cml0ZSIsInJlbGF0aW9uc19yZWFkIiwiZGF0YWJhc2VfdXNlcl9wYXNzd29yZF9tcGx1c19lbmNyeXB0ZWRfcmVhZCIsImRyYWZ0X2xpY2Vuc2VfcmVhZCIsImRhdGFiYXNlX2NvbmZpZ3VyYXRpb25fcmVhZCJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQga21zIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZGVhbGVySWQiOjEsIm5hbWUiOiJFZWxrZSBLbGVpbiIsInByZWZlcnJlZF91c2VybmFtZSI6ImVlbGtlQGJvbHQubmwiLCJsb2NhbGUiOiJlbiIsImdpdmVuX25hbWUiOiJFZWxrZSIsImZhbWlseV9uYW1lIjoiS2xlaW4iLCJlbWFpbCI6ImVlbGtlQGJvbHQubmwiLCJlbXBsb3llZU51bWJlciI6NTR9.SHjVTWsFwiaKTxBX-0GZM1pK8rOodkYnEu_QJ4dlPpozai9j3RRJK3DswsuEbJC8PdQXI4-AI0-5JGBQi2gDXdFSVHhAblnmjva0sWCaY7lG2ASa65UKM_4RzH-6nvQ9EiZXdANzsWkLG350l-dLiqdt--Lpjpw2huK_GKAx20SKfauKBmm990rHzrl0Uii3wQ3fPHlAJ_8-WSnSBquOH8xsYJHa1LOsc2WqbEDnMA4hRnGvCoubwhkOANfWSx0OCwSIKBddrcts64ZAxFhmilZXGzWMqDkblY2fDU8_jrlysgYsymQlOVwwg7V5Ps-DJkGXWvmpncKfyYd3Vuwusg";
string keycloakKeySet = """
{
"keys": [
{
"kid": "rmVwMNk3J5Xyf1hrKsUlEX7PMRn6vVJcHwSqX1EPFqI",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "pYbLAeOLDEwzL4tEwuE2LfisOBXoQqWA9RdP3ph6muwF1ErfhiBSIB2JETKf7F1OsiF1_qnuh4uDfn0TO8bK3lSfHTlIHWShwaJ_UegS9ylobfIYXJsz0xmJK5ToFaSYa72D_Dyln7ROxudu8-zc70sz7bUKQ0_ktWRsiu76vY6Kr9-18PgaooPmb2QP8lS8IZEv-gW5SLqoMc1DfD8lsih1sdnQ8W65cBsNnenkWc97AF9cMR6rdD2tZfLAxEHKYaohAL9EsQsLic3P2f2UaqRTAOvgqyYE5hyJROt7Pyeyi8YSy7zXD12h2mc0mrSoA-u7s_GrOLcLoLLgEnRRVw",
"e": "AQAB",
"x5c": [
"MIICozCCAYsCBgFwfLC07DANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDAptcGx1c2thc3NhMB4XDTIwMDIyNTE0MTAyMFoXDTMwMDIyNTE0MTIwMFowFTETMBEGA1UEAwwKbXBsdXNrYXNzYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKWGywHjiwxMMy+LRMLhNi34rDgV6EKlgPUXT96YeprsBdRK34YgUiAdiREyn+xdTrIhdf6p7oeLg359EzvGyt5Unx05SB1kocGif1HoEvcpaG3yGFybM9MZiSuU6BWkmGu9g/w8pZ+0TsbnbvPs3O9LM+21CkNP5LVkbIru+r2Oiq/ftfD4GqKD5m9kD/JUvCGRL/oFuUi6qDHNQ3w/JbIodbHZ0PFuuXAbDZ3p5FnPewBfXDEeq3Q9rWXywMRBymGqIQC/RLELC4nNz9n9lGqkUwDr4KsmBOYciUTrez8nsovGEsu81w9dodpnNJq0qAPru7Pxqzi3C6Cy4BJ0UVcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYcJVYv8HzZQIMrqhIyu7EVihPAx0w9NaZ1xzB9qCHrwie6ZLQdnMm8l0IdehyYuY+0HK7FjC8dAcT4nklOQDg4iCp7ZrM7vFNP+60pR0i7aIbf0cFXy9VTOPvUsXmu+p1LqRQLRJD0BjO29gupTe68KyTtyuX5A7JfmCq84j5i45Md8A9MAMZWnXMSHiaZLtlOS/4t4cdc371uq9fH7SKusnvUY0d14+BzcrHi/eurhKUUjJZ1xclUE2trOXFrE78fSMUmeGbIlpAV0MtrJW7OmXmaHH8Q1wTm78RH/dn4EmWSoFcigdVcDge941/MT2soDSIGLUnYiYrW8d6HE2Lg=="
],
"x5t": "rj9_q26MIdowvyJJbyHySeUl1y8",
"x5t#S256": "KNyQ8ngE925F__ZPJm-wCNUnGBJQGJbZGGjlCvmwBkM"
}
]
}
""";
JsonWebKeySet keySet = JsonSerializer.Deserialize<JsonWebKeySet>(keycloakKeySet)!;
using RSA publicKey = LoadFromJwk(keySet.Keys[0]);
Assert.True(ValidateJwtSignature(keycloakGeneratedJwt, publicKey));
}
private bool ValidateJwtSignature(string jwtString, RSA publicKey)
{
int lastDotIndex = jwtString.LastIndexOf('.');
return publicKey.VerifyData(
Encoding.UTF8.GetBytes(jwtString, 0, lastDotIndex),
WebEncoders.Base64UrlDecode(jwtString, lastDotIndex + 1, jwtString.Length - (lastDotIndex + 1)),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
}
private static RSA LoadFromJwk(JsonWebKey jwk)
{
var rsa = RSA.Create();
var parameters = new RSAParameters
{
Modulus = WebEncoders.Base64UrlDecode(jwk.Modulus),
Exponent = WebEncoders.Base64UrlDecode(jwk.Exponent)
};
rsa.ImportParameters(parameters);
return rsa;
}
}