10

我们正在尝试验证由 OpenID 连接提供程序 (OP) 提供给 .NET 客户端应用程序的 ID 令牌 (IDT)。IDT 是您所期望的。那里没有什么不寻常的事情发生。

为了验证 IDT 的签名,我们可以通过调用公共端点从 OP 获取指数和模数。这些可用于创建与 OP 用于签署 IDT 的私钥相对应的公钥。有了这些,我们创建了一个 RSACryptoServiceProvider 对象来进行签名验证。为了解决这个问题,我们将加密服务提供者作为令牌验证参数传递给 JwtSecurityTokenHandler。

这工作正常。我们以为我们已经完成并准备好迎接周末了。但是,我们发现我们可以更改签名中的最后一个字符,JwtSecurityTokenHandler 仍然会告诉我们 JWT 是有效的。我们找不到对此的解释,并想知道是否:

  1. 我们创建签名密钥的方式存在问题,导致它无法正确验证 JWT。
  2. JwtSecurityTokenHandler 中有一个错误。
  3. 我们不完全理解规范,允许进行这种小改动,因为 JWT 签名部分的最后一个字符实际上与验证无关。
  4. 别的东西

我们正在使用 System.IdentityModel.Tokens.Jwt.dll v4.0.30319 中的 System.IdentityModel.Tokens.JwtSecurityTokenHandler。

下面是我们代码的一个非常简单的示例。

程序.cs

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();
        }
    }
}

TokenValidator.cs

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;            
        }
    }
}
4

1 回答 1

16

这似乎发生在 Base64Url 编码签名的解码中。我不能告诉你确切的原因,但试试这个:

转到: http: //kjur.github.io/jsjws/tool_b64udec.html

在上面的帖子中解码您在 JWT 中的签名:

Z3P4Rt_w7d0oP8x6zfaot8PIxpEJHUw43Z_4VkOzv59nRz1dWopGUXw51DJd5cLjeM_zc14durs5NhJE27WmcKaEuE8-WZ0ubxM_bzykZfmAPa1WVk9KctPKiUH7QZg4OCLaqIX6usi5kkuICiPVdoJPkHmojMkm5nCqeBIbYteasysMTQGq93VtoBGUQomF89ZaFMBlUy0ofH7SEKJEW_4vgy7Umu0h7kNKkh6Aw4x9Bw1AkG1D6H_scsuH2uSxQ7QV-3G60DcjLZ31_R1ZxaUg2WS2ajemb6swKM4LIOR9_mK6ScUVVBxBL4Oh9g6EA93lMg_1GRZi780v_3TR8Q

这将产生这个十六进制输出:

6773f846dc3b774a0ff31eb37daa2df0f231a44247530e376785643b3bf9f67473d5d5a8a46517c39d4325de5c2e378ccdcd7876eaece4d849136ed699c29a12e13c599d2e6f131bcf29197e600f6b559593d29cb4f2a2507ed0660e0e08b6aa217eaeb22e6492e20288f55da093e41e6a233249b99c2a9e0486d8b5e6accac313406abddd5b68046510a2617cf59685301954cb4a1f1fb484289116e2f832ed49aed21ee434a921e80c38c7d070d40906d43e87b1cb2e1f6b92c50ed05771bad037232d9df5475671694836592d9a8de99beacc0a3382c8391f662ba49c515541c412f83a1f60e8403dde5320d464598bbf34bf74d1f1

更改 Base64Url 编码签名的最后一个字符实际上并不总是会更改十六进制的签名值。这是因为字符串中只有最后一个 Base64 字符(Q = 16 = 010000)的前两位是有效的。最后四位被丢弃,因为它们没有形成一个完整的字节。因此,您实际上可以使用所有这些字符 QRSTUVQXYZabcdef(二进制 010000 - 011111),它们最终都会产生相同的十六进制值 f1,因为所有这些字符的前两位都是 01。

总而言之,您实际上并没有篡改签名,而只是篡改了它的编码。您仍在使用有效密钥进行验证。

于 2014-10-28T21:37:46.437 回答