Closed RufusJWB closed 3 years ago
Hi @RufusJWB - I don't fully understand what is the goal of adding the X5c property on JwtHearer (#1543).
To validate a signature, you'll need to set IssuerSigningKey on TokenValidationParameters.
IDX10501 signals that the library couldn't match a signing key with a 'kid' claim, and that's expected as you haven't set an IssuerSigningKey.
Hi @GeoK ! Yes, setting the IssuerSigningKey is exactly what I want to do, but to do this, I need (at least to my understanding) have access to the X5c property of the received JWT. If you decode the jwt I have pasted above, you will see, that it is signed with the private key that matches the certificate in the X5c field.
The code could look like this in the end:
string jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjM2ODZEMEQ2NzgyQzUyQ0U1NjI5RThENkI4MjFCMkU2RENGMjE4RkYiLCJ4NXQiOiJOb2JRMW5nc1VzNVdLZWpXdUNHeTV0enlHUDgiLCJ0eXAiOiJKV1QiLCJ4NWMiOlsiTUlJSGZEQ0NCV1NnQXdJQkFnSVFiMEd0ZG5YdXRZWXM2VSt1Yy9TbEtqQU5CZ2txaGtpRzl3MEJBUXNGQURDQm56RUxNQWtHQTFVRUJoTUNSRVV4RHpBTkJnTlZCQWdNQmtKaGVXVnliakVSTUE4R0ExVUVCd3dJVFhWbGJtTm9aVzR4RURBT0JnTlZCQW9NQjFOcFpXMWxibk14RVRBUEJnTlZCQVVUQ0ZwYVdscGFXa0l5TVIwd0d3WURWUVFMREJSVGFXVnRaVzV6SUZSeWRYTjBJRU5sYm5SbGNqRW9NQ1lHQTFVRUF3d2ZVMmxsYldWdWN5QkpjM04xYVc1bklFTkJJRVZGSUVGMWRHZ2dNakF5TURBZUZ3MHlNREV3TURjd09EUXdORFphRncweU16RXdNRGN3T0RRd05EWmFNR0V4RVRBUEJnTlZCQVVUQ0Zvd01ESk5OelpCTVE0d0RBWURWUVFxRXdWU2RXWjFjekVSTUE4R0ExVUVCQk1JUW5WelkyaGhjblF4RURBT0JnTlZCQW9UQjFOcFpXMWxibk14RnpBVkJnTlZCQU1URGtKMWMyTm9ZWEowSUZKMVpuVnpNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXVlV0MzR0IwdHRPaFNtZlBGQWJ5cVc0YjRjWG1nUUJBOGJzalU2bDNvT0JqczV6M1pYWFlMM3FsdTA5Mm8rcERKdEFMVGpYNmlaQ3dwWlJuQURYU1pVeWpWNm1XZEF6aWxqdG1FVXlZZG56NnhzQ1lpR2NHQzdaMTdNMmQ2OXFBdEl2d2hGeWE4alVZbDB5QTJGK1ppb1ViaStQUmhtdFBSZmg1bi80RXp4b1J5bG0xVlJzNkdQcTFRMjNBZTdPbGhBOGxxVFg2YkJZdVYwQ0E4Q2tTWjB3MzBaSXJGUTJ6UVZHSEJwTm8rcjNiSlRWdFNRZDduaUNTNkN5bFVEWWI4NGZ4emFPMmZoczJXMU5TcEI5SzVpR1dFaWpNQ0I2Ti9kTTMvMFVTbTJMSmt2cExKK2VQUlkwQ1I2TVptZmdxQ3l2QVFVWFRxdmFOZ2krOXNSUlNmUUlEQVFBQm80SUM3ekNDQXVzd0tRWURWUjBsQkNJd0lBWUlLd1lCQlFVSEF3SUdDQ3NHQVFVRkJ3TUVCZ29yQmdFRUFZSTNGQUlDTUI4R0ExVWRJd1FZTUJhQUZOYnYrNmZuS3JIQVhVeG9oY0l0ajFabWZvTHVNSUgzQmdnckJnRUZCUWNCQVFTQjZqQ0I1ekF5QmdnckJnRUZCUWN3QW9ZbWFIUjBjRG92TDJGb0xuTnBaVzFsYm5NdVkyOXRMM0JyYVQ5YVdscGFXbHBDTWk1amNuUXdRUVlJS3dZQkJRVUhNQUtHTld4a1lYQTZMeTloYkM1emFXVnRaVzV6TG01bGRDOURUajFhV2xwYVdscENNaXhNUFZCTFNUOWpRVU5sY25ScFptbGpZWFJsTUVrR0NDc0dBUVVGQnpBQ2hqMXNaR0Z3T2k4dllXd3VjMmxsYldWdWN5NWpiMjB2UTA0OVdscGFXbHBhUWpJc2J6MVVjblZ6ZEdObGJuUmxjajlqUVVObGNuUnBabWxqWVhSbE1DTUdDQ3NHQVFVRkJ6QUJoaGRvZEhSd09pOHZiMk56Y0M1emFXVnRaVzV6TG1OdmJUQkdCZ05WSFNBRVB6QTlNRHNHRFNzR0FRUUJvV2tIQWdJREFRRXdLakFvQmdnckJnRUZCUWNDQVJZY2FIUjBjSE02THk5M2QzY3VjMmxsYldWdWN5NWpiMjB2Y0d0cEx6Q0J5Z1lEVlIwZkJJSENNSUcvTUlHOG9JRzVvSUcyaGlab2RIUndPaTh2WTJndWMybGxiV1Z1Y3k1amIyMHZjR3RwUDFwYVdscGFXa0l5TG1OeWJJWkJiR1JoY0RvdkwyTnNMbk5wWlcxbGJuTXVibVYwTDBOT1BWcGFXbHBhV2tJeUxFdzlVRXRKUDJObGNuUnBabWxqWVhSbFVtVjJiMk5oZEdsdmJreHBjM1NHU1d4a1lYQTZMeTlqYkM1emFXVnRaVzV6TG1OdmJTOURUajFhV2xwYVdscENNaXh2UFZSeWRYTjBZMlZ1ZEdWeVAyTmxjblJwWm1sallYUmxVbVYyYjJOaGRHbHZia3hwYzNRd0hRWURWUjBPQkJZRUZFaEVhQmM0S1gzRG94aVB1dUd3aFF2RFhldVdNQTRHQTFVZER3RUIvd1FFQXdJSGdEQlJCZ05WSFJFRVNqQklvQ29HQ2lzR0FRUUJnamNVQWdPZ0hBd2FjblZtZFhNdVluVnpZMmhoY25SQWMybGxiV1Z1Y3k1amIyMkJHbkoxWm5WekxtSjFjMk5vWVhKMFFITnBaVzFsYm5NdVkyOXRNQXdHQTFVZEV3RUIvd1FDTUFBd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dJQkFDdEk4UDVvQTR1UG9JWVpLV0tYNUJvSlZsaTArOFhuckM5L0xIb29NYlI1WVJyekhCdC8wbFc0d2p4OHJjY2thTGN5ZzZyVXJIYXpra3k2dWRCaDVBTlBmSE9HQ0M4b3N3REV6UFQveUtZazBqcUx2eStmckVNNFlrZk5aSTUzYXVXYTJXNlYxNUxIYkVyVzJCOU9SS2lOdEsrYmtIa09zR0xuWlFFaXFGQ3RZL3I1cy9XNVdSaGlNb1ppUTZsMUh0ZXNVSWZIR1piNFhXVGlwSUd3RnltU2RrM2xpQWorekJqWmoydm15MURZVHVuQytSUmxHZFpnemkrRC9LK1NoQ240YjZWR2ZPR3AxMFVLNUtTbm8zL3lhSTY2aXNBNXZtbFlJRk8xWkYvMGhqRnl6NUdVeW9NdDFJRk9qcFA3ZS9kaEtiUElsWHA1aDJ3c3hrNjJKRlBRN3hpNHBIYlpvUlBMbVE1RkVoZVI4UXpCZHJuMTJuWnp2bHk0dXhiaWlISStVMFlaUnEzVm1YRnorYzJIcHM3SEVtR3ArUmhaRXVDcit1NklobDMySTljTlBMMVNKZTh3aW04b2p6bmFaRmZoenlYbDl0cnppSHJ6NUdOdFdEZzRNWkhlL0pTN0tRNC95eHVqL2w4cDBwTHdXaU9Ycnl0WEZsSGx1b0tka2J2cXlCRGdvb25iSjBpSzhVMDE1WnR2MVYwc2pBbDYzYnFZMDZuaHYvZUxWemorOWE1V1FHNHE4aTk2RkNGNGJqOUFtK0ZzRDU5MnUwM3M2VVdUa01kbWMrRmJ0V0xXdDlrL25hWjk0ZFRWRklQaDBFTG5XeWNTYWlySnA0LzNvRkpPNjJkWkRpc1ZCbENiQmJUZ1NPS1VaeVdJMVJ6NlRTVXp3UFVHS0REVCJdfQ.eyJjb250ZW50IjoiVGVzdCBkYXRhIiwibmJmIjoxNjAyNzUwMjI1LCJleHAiOjE2MDI3NTA1MjUsImlhdCI6MTYwMjc1MDIyNSwiaXNzIjoiQ049QnVzY2hhcnQgUnVmdXMsIE89U2llbWVucywgU049QnVzY2hhcnQsIEc9UnVmdXMsIFNFUklBTE5VTUJFUj1aMDAyTTc2QSJ9.Ax1E_6ZqgdstOmYBxO2bSITpppW_K8JN4f_kAcUPBc77ucaZ4419bzOrx9eF9ib22nTa2JDRivGa_sUfs3W4OjjoEiXatzzTXWZYFcpAz60CYRuvgpu1rKOl4T5aiz5JAyVOoFWopDGjKPYIM2Hkg51JgDKcYDcX-JNBoH8ZEyXSkzhHwjwnDMZf6RT1RLCIpb4urcrfXl7nyJiwyLRzwAaO9gSdJiTPRGm5qrcmTy93eBABb0zjp0HbTbfqlMcHQozPB7ARAkn5d60BqRj2CrOMGak4yBzliYUVJqmkYZAtZ2NK-3tMZXY_Z8fRDiOJS4ys1Up3Zum8f-X2ZSeJog";
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken outerJWT = handler.ReadJwtToken(jwt);
JwtHeader header = outerJWT.Header;
//string x5c = header.X5c; // <-- This is not working!!!
string x5c = "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";
var certificate = new X509Certificate2(Convert.FromBase64String(x5c));
TokenValidationParameters validationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
ValidateLifetime = false,
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKeyResolver = (t, st, i, p) => new[] {
new X509SecurityKey(certificate)
},
};
SecurityToken validatedSecurityToken = null;
var cp = handler.ValidateToken(jwt, validationParameters, out validatedSecurityToken);
@RufusJWB it is important to ensure a token has been created by a trusted authority. How will the code above ensure the token has been issued by a trusted authority?
Our model for validation is to obtain keys and the issuer value from a trusted authority and then ensure the token is signed by that authority. We should also check the audience and expiration.
@RufusJWB it is important to ensure a token has been created by a trusted authority. How will the code above ensure the token has been issued by a trusted authority?
@brentschmaltz the code will validate the signer certificate which is extracted from the x5c field and check, if it is issued by a CA that chains up to a trusted root CA. This can be done by out-of-the-box mechanism of any PKI infrastructure.
Our model for validation is to obtain keys and the issuer value from a trusted authority and then ensure the token is signed by that authority. We should also check the audience and expiration.
Of course, audience and expiration will be checked in production. I only disabled it for the sake of clarity in the code example.
@brentschmaltz any thoughts on this?
Well what you can do @RufusJWB is set TokenValidationParameters.IssuerSigningKeyResolver
to a delegate which will extract the key from the x5c
header while validating the chain by your PKI (do checkout new .NET 5 APIs 😉).
Obtaining the x5c
header and properly validating its contents is a little quirky though. From the delegate you have access to a SecurityToken
instance which you can safely cast to JsonWebToken
then call TryGetHeaderValue<string[]>(JwtHeaderParameterNames.X5c, out var result)
on it. The last part is the quirky one, you cannot really validate if the JWT does contains invalid x5c
header the method returns false
if someone passes a integer or null
there. Could something be done about that @brentschmaltz (maybe this is out of scope of this issue)?
@shadow-cs thank you for your feedback. I'll try this. Did you see my PR #1543 ? This one line of code would make the x5c header directly available without casting.
I had a really similar issue and it was impossible to find any documentation regarding how the JWT signature is being verified. With defined kid the standard supports 3 ways: x5c, jwk, x5u. As far as I managed to test, the validation supports x5u like way to call "jwt's issuer + .well-known/jwks.json" URL to get the public key. However it is not 100% clear if x5c, jwk and x5u are not supported and they cannot take priority over the built-in validation. E.g.: if the x5u is supported then the http request to fetch the public key could be tampered to a fake endpoint via providing a value to x5u and the whole signature validation can be broken. Similarly with jwk and x5c, what will happen if someone provides a value in these fields? Would they be ignored by default? Will this behavior stay like this? FYI @GeoK
@gergoszekeresnuance x5c, x5u, where the key is inside the JWT, is not supported directly. Our validation logic expects that TokenValidationParameters either has the signing keys OR a delegate is used.
Microsoft's WCF runtime would validate SAML token signatures that contained x5c, but we made a design decision to require all accepted keys to be available.
@RufusJWB in order to get the x5c from a JWT created by Microsoft Entra (Active Directory), you need to:
For simplicity you can deserialize your response into a simpler object since you only need the kid and x5x. Something like: { "keys": [ { "kid": "kid_val", "x5c": ["x5c_val"] } ] }
I will leave this article for reference on how to do it: https://www.voitanos.io/blog/validating-entra-id-generated-oauth-tokens/
I have the following JWT which was signed with an X509 certificate. The signing certificates itself is embedded in the
x5c
field of the JWT header.I'm trying to validate this JWT with the following code:
This results in an exception:
As an alternative approach, I did try to extract the certificate from the x5c field in the header
but this is not working either, since there is no
X5c
field in theJwtHeader
class.