From 2c6d11577c89e5bfd6839e22bffc55c4bc243b81 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Thu, 9 Jul 2026 00:22:02 +0100 Subject: [PATCH] Yavsc.Org: set KeyId on signing credentials IdentityServer8 was emitting JWTs without a 'kid' header and serving the JWKS without per-key identifiers, because LoadSigningCredentialsInner constructed RsaSecurityKey / ECDsaSecurityKey objects without an explicit KeyId. Resource servers (Yavsc.Blogs, Yavsc.Api) cannot match a token to a key in the JWKS without one, so every signature validation failed with 'The signature key was not found' (Microsoft.IdentityModel IDX10500). Root cause: SigningCredentials were built directly from the BC-parsed key parameters, bypassing the X509Certificate2 path IdentityServer normally derives the kid from. The fix derives a stable KeyId from the certificate's SHA-256 thumbprint (truncated to 16 hex chars) and sets it on both SecurityKey variants before constructing SigningCredentials. The thumbprint-based kid is stable across process restarts as long as the cert doesn't change, and changes naturally on LetsEncrypt renewal (~90 days), which is the right behaviour: old tokens age out, resource servers refresh their JWKS cache to discover the new kid. Production rollout: redeploy Yavsc.Org and re-login (or let the refresh-token path rotate) so newly issued tokens carry the kid. Pre-restart tokens will continue to be rejected with IDX10500 until they expire or are refreshed. --- contrib/bruno/opencollection.yml | 1 + src/Yavsc.Org/Extensions/HostingExtensions.cs | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/contrib/bruno/opencollection.yml b/contrib/bruno/opencollection.yml index 1d584584..f072bf40 100644 --- a/contrib/bruno/opencollection.yml +++ b/contrib/bruno/opencollection.yml @@ -20,6 +20,7 @@ request: flow: authorization_code authorizationUrl: "{{Authority}}/connect/authorize" accessTokenUrl: "{{Authority}}/connect/token" + refreshTokenUrl: https://yavsc.pschneider.fr/connect/token callbackUrl: "{{Authority}}" credentials: clientId: postit diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index b29f37ce..b813f681 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -478,6 +478,21 @@ public static class HostingExtensions // Validate the cert is readable (used downstream for token // audience/subject validation; signing itself uses the key). + // Derive a stable KeyId from the certificate's SHA-256 + // thumbprint. Without an explicit KeyId, IdentityServer emits + // JWTs without a 'kid' header and the JWKS without per-key + // identifiers, which breaks signature validation on resource + // servers (they cannot match a token to a key in the JWKS, + // they fail with IDX10500 "The signature key was not found"). + // Truncating to 16 hex chars is enough to be globally unique + // within a deployment and keeps the JWT header compact. The + // thumbprint changes on cert renewal, which is the desired + // behaviour: old tokens age out, resource servers refresh + // their JWKS cache for the new kid. + var certForKid = new X509Certificate2(certPath); + var certHash = certForKid.GetCertHash(); + var kid = Convert.ToHexString(certHash)[..Math.Min(16, certHash.Length * 2)]; + string keyPem = File.ReadAllText(keyPath); // BouncyCastle's PemReader accepts every flavour of unencrypted @@ -513,7 +528,7 @@ public static class HostingExtensions #pragma warning disable CA1416 // Valider la compatibilité de la plateforme var rsaDotNet = DotNetUtilities.ToRSA(rsa); #pragma warning restore CA1416 // Valider la compatibilité de la plateforme - var key = new RsaSecurityKey(rsaDotNet); + var key = new RsaSecurityKey(rsaDotNet) { KeyId = kid }; return new SigningCredentials(key, SecurityAlgorithms.RsaSha256); } case ECPrivateKeyParameters ec: @@ -525,7 +540,7 @@ public static class HostingExtensions }; var ecdsa = ECDsa.Create(); ecdsa.ImportParameters(ecParams); - var key = new ECDsaSecurityKey(ecdsa); + var key = new ECDsaSecurityKey(ecdsa) { KeyId = kid }; return new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256); } default: