我们正在尝试验证OpenID Connect Provider(OP)向.NET客户端应用程序提供的ID令牌(IDT)。 IDT就是你所期望的。没有什么不寻常的事情发生在那里。
为了验证IDT的签名,我们可以通过调用公共端点从OP获得指数和模数。这些可用于创建与OP用于签署IDT的私有公钥对应的公钥。通过这些,我们创建了一个RSACryptoServiceProvider对象来进行签名验证。为此,我们将加密服务提供程序作为令牌验证参数传递给JwtSecurityTokenHandler。
这很好用。我们以为我们已经完成并为周末做好了准备。但是,我们发现我们可以更改签名中的最后一个字符,JwtSecurityTokenHandler仍会告诉我们JWT是有效的。我们无法找到解释,并且想知道是否:
我们正在使用System.IdentityModel.Tokens.Jwt.dll v4.0.30319中的System.IdentityModel.Tokens.JwtSecurityTokenHandler。
下面是一个非常简单的代码示例。
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;
}
}
}
答案 0 :(得分:10)
这似乎发生在Base64Url编码签名的解码中。我无法确切地告诉你原因,但试试这个:
转到:http://kjur.github.io/jsjws/tool_b64udec.html
在上面的帖子中解码您在JWT中的签名:
Z3P4Rt_w7d0oP8x6zfaot8PIxpEJHUw43Z_4VkOzv59nRz1dWopGUXw51DJd5cLjeM_zc14durs5NhJE27WmcKaEuE8-WZ0ubxM_bzykZfmAPa1WVk9KctPKiUH7QZg4OCLaqIX6usi5kkuICiPVdoJPkHmojMkm5nCqeBIbYteasysMTQGq93VtoBGUQomF89ZaFMBlUy0ofH7SEKJEW_4vgy7Umu0h7kNKkh6Aw4x9Bw1AkG1D6H_scsuH2uSxQ7QV-3G60DcjLZ31_R1ZxaUg2WS2ajemb6swKM4LIOR9_mK6ScUVVBxBL4Oh9g6EA93lMg_1GRZi780v_3TR8Q
这将产生此HEX输出:
6773f846dc3b774a0ff31eb37daa2df0f231a44247530e376785643b3bf9f67473d5d5a8a46517c39d4325de5c2e378ccdcd7876eaece4d849136ed699c29a12e13c599d2e6f131bcf29197e600f6b559593d29cb4f2a2507ed0660e0e08b6aa217eaeb22e6492e20288f55da093e41e6a233249b99c2a9e0486d8b5e6accac313406abddd5b68046510a2617cf59685301954cb4a1f1fb484289116e2f832ed49aed21ee434a921e80c38c7d070d40906d43e87b1cb2e1f6b92c50ed05771bad037232d9df5475671694836592d9a8de99beacc0a3382c8391f662ba49c515541c412f83a1f60e8403dde5320d464598bbf34bf74d1f1
更改Base64Url编码签名的最后一个字符实际上并不总是以十六进制更改签名值。这是因为字符串中只有最后一个Base64字符(Q = 16 = 010000)的前两位是重要的。最后四位被抛出,因为它们不形成完整的字节。所以,你实际上可以使用所有这些字符QRSTUVQXYZabcdef(二进制010000 - 011111),它们都将产生相同的十六进制值f1,因为所有这些字符的两个第一位都是01。
总而言之,您实际上并没有篡改签名,只是对其进行编码。您仍在使用有效密钥进行验证。