Reputation: 2281
We are trying to validate the ID Token (IDT) presented to a .NET client application by an OpenID Connect Provider (OP). The IDT is what you would expect. Nothing unusual going on there.
To verify the signature of the IDT, we can get the exponent and modulus from the OP by calling a public endpoint. These can be used to create a public key that corresponds to the private one used by the OP to sign the IDT. With these, we create a RSACryptoServiceProvider object to do the signature verification. To help with this, we are are passing the crypto service provider as a token validation parameter to a JwtSecurityTokenHandler.
This works fine. We thought we were done and ready for the weekend. However, we found that we can change the last character in the signature and the JwtSecurityTokenHandler will still tell us the JWT is valid. We cannot find an explanation for this and are wondering if:
We are using System.IdentityModel.Tokens.JwtSecurityTokenHandler from System.IdentityModel.Tokens.Jwt.dll v4.0.30319.
A very simple sample of our code is below.
using System;
using System.Configuration;
using System.IdentityModel.Tokens;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJzdWIiOiJ1c2VyMSIsImF1ZCI6ImNsaWVudDEiLCJqdGkiOiJKcUFDVVFiTlRQR201U0ZJRXY3MWR0IiwiaXNzIjoiaHR0cHM6XC9cL2xvY2FsaG9zdDo5MDMxIiwiaWF0IjoxNDEzNTcwNjEyLCJleHAiOjE0MTM1NzA5MTJ9.Z3P4Rt_w7d0oP8x6zfaot8PIxpEJHUw43Z_4VkOzv59nRz1dWopGUXw51DJd5cLjeM_zc14durs5NhJE27WmcKaEuE8-WZ0ubxM_bzykZfmAPa1WVk9KctPKiUH7QZg4OCLaqIX6usi5kkuICiPVdoJPkHmojMkm5nCqeBIbYteasysMTQGq93VtoBGUQomF89ZaFMBlUy0ofH7SEKJEW_4vgy7Umu0h7kNKkh6Aw4x9Bw1AkG1D6H_scsuH2uSxQ7QV-3G60DcjLZ31_R1ZxaUg2WS2ajemb6swKM4LIOR9_mK6ScUVVBxBL4Oh9g6EA93lMg_1GRZi780v_3TR8Q";
var tokenValidator = new TokenValidator(new CacheProvider(), new DebugOpenIdConnectProviderClient(),
ConfigurationManager.AppSettings["AUDIENCE"], ConfigurationManager.AppSettings["ISSUER"]);
SecurityToken securityToken;
var principal = tokenValidator.Validate(token, out securityToken);
if (principal != null)
{
Console.Out.WriteLine("Security token is valid");
}
foreach (var claim in principal.Claims)
{
Console.Out.WriteLine("{0} = {1}", claim.Type, claim.Value);
}
Console.ReadLine();
}
}
}
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.Security.Claims;
using System.Security.Cryptography;
using Newtonsoft.Json;
namespace ConsoleApplication1
{
public class TokenValidator
{
private readonly CacheProvider cacheProvider;
private readonly IOpenIdConnectProviderClient openIdConnectProviderClient;
private readonly string audience;
private readonly string issuer;
public TokenValidator(CacheProvider cacheProvider, IOpenIdConnectProviderClient openIdConnectProviderClient, string audience, string issuer)
{
this.cacheProvider = cacheProvider;
this.openIdConnectProviderClient = openIdConnectProviderClient;
this.audience = audience;
this.issuer = issuer;
}
public ClaimsPrincipal Validate(string tokenString, out SecurityToken securityToken)
{
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var jwt = jwtSecurityTokenHandler.ReadToken(tokenString) as JwtSecurityToken;
var publicKey = GetPublicKey(jwt.Header.SigningKeyIdentifier[0].Id);
var rsaPublicKey = CreatePublicKey(publicKey.n, publicKey.e);
return jwtSecurityTokenHandler.ValidateToken(tokenString, new TokenValidationParameters()
{
IssuerSigningToken = new RsaSecurityToken(rsaPublicKey, publicKey.kid),
IssuerSigningKeyResolver = (token, securityToken2, keyIdentifier, validationParameters) => {
return new RsaSecurityKey(rsaPublicKey);
},
#if DEBUG
ClockSkew = new TimeSpan(0, 30, 0),
#endif
ValidIssuer = issuer,
ValidAudience = audience,
}, out securityToken);
}
public static RSACryptoServiceProvider CreatePublicKey(string modulus, string exponent)
{
var cryptoProvider = new RSACryptoServiceProvider();
cryptoProvider.ImportParameters(new RSAParameters()
{
Exponent = Base64UrlEncoder.DecodeBytes(exponent),
Modulus = Base64UrlEncoder.DecodeBytes(modulus),
});
return cryptoProvider;
}
private PublicKeyData GetPublicKey(string kid)
{
var keys = cacheProvider["PUBLIC_KEYS"] as Dictionary<string, PublicKeyData>;
if (keys == null)
{
keys = GetPublicKeysFromPingFederate();
cacheProvider["PUBLIC_KEYS"] = keys;
}
var currentKey = keys[kid];
if (currentKey != null)
{
return currentKey;
}
throw new Exception("Could not find public key for kid: " + kid);
}
private Dictionary<string, PublicKeyData> GetPublicKeysFromPingFederate()
{
var keyString = openIdConnectProviderClient.Execute();
var keys = JsonConvert.DeserializeObject<PublicKeysJsonResult>(keyString);
var result = new Dictionary<string, PublicKeyData>();
foreach (var key in keys.Keys)
{
result[key.kid] = key;
}
return result;
}
}
}
Upvotes: 10
Views: 6816
Reputation: 373
This seems to be happening in the decoding of the Base64Url encoded signature. I can't tell you exactly why, but try this out:
Go to: http://kjur.github.io/jsjws/tool_b64udec.html
Decode your signature in the JWT in your post above:
Z3P4Rt_w7d0oP8x6zfaot8PIxpEJHUw43Z_4VkOzv59nRz1dWopGUXw51DJd5cLjeM_zc14durs5NhJE27WmcKaEuE8-WZ0ubxM_bzykZfmAPa1WVk9KctPKiUH7QZg4OCLaqIX6usi5kkuICiPVdoJPkHmojMkm5nCqeBIbYteasysMTQGq93VtoBGUQomF89ZaFMBlUy0ofH7SEKJEW_4vgy7Umu0h7kNKkh6Aw4x9Bw1AkG1D6H_scsuH2uSxQ7QV-3G60DcjLZ31_R1ZxaUg2WS2ajemb6swKM4LIOR9_mK6ScUVVBxBL4Oh9g6EA93lMg_1GRZi780v_3TR8Q
This will yield this HEX output:
6773f846dc3b774a0ff31eb37daa2df0f231a44247530e376785643b3bf9f67473d5d5a8a46517c39d4325de5c2e378ccdcd7876eaece4d849136ed699c29a12e13c599d2e6f131bcf29197e600f6b559593d29cb4f2a2507ed0660e0e08b6aa217eaeb22e6492e20288f55da093e41e6a233249b99c2a9e0486d8b5e6accac313406abddd5b68046510a2617cf59685301954cb4a1f1fb484289116e2f832ed49aed21ee434a921e80c38c7d070d40906d43e87b1cb2e1f6b92c50ed05771bad037232d9df5475671694836592d9a8de99beacc0a3382c8391f662ba49c515541c412f83a1f60e8403dde5320d464598bbf34bf74d1f1
Changing the last character of the Base64Url Encoded signature will actually not always change the signature value in hex. That is because only the first two bits of the last Base64 character (Q = 16 = 010000) in the string are significant. The last four bits are thrown out since they do not form a complete byte. So, you can actually use all these characters QRSTUVQXYZabcdef (binary 010000 - 011111), they will all produce the same hex value f1 in the end since the two first bits for all those characters are 01.
To conclude, you have not actually tampered with the signature, merely with the encoding of it. You are still validating using the valid key.
Upvotes: 18