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.
This commit is contained in:
parent
6055117929
commit
2c6d11577c
2 changed files with 18 additions and 2 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue