JwtSecurityTokenHandler表示更改1个字符后JWT的签名有效

时间:2014-10-17 19:28:31

标签: c# security oauth-2.0 jwt openid-connect

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

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

这很好用。我们以为我们已经完成并为周末做好了准备。但是,我们发现我们可以更改签名中的最后一个字符,JwtSecurityTokenHandler仍会告诉我们JWT是有效的。我们无法找到解释,并且想知道是否:

  1. 我们创建签名密钥的方式存在问题,导致其无法正确验证JWT。
  2. JwtSecurityTokenHandler中存在错误。
  3. 我们不完全理解规范,允许这种小改动,因为JWT签名部分的最后一个字符实际上与验证不相关。
  4. 其他东西
  5. 我们正在使用System.IdentityModel.Tokens.Jwt.dll v4.0.30319中的System.IdentityModel.Tokens.JwtSecurityTokenHandler。

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

    的Program.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;            
            }
        }
    }
    

1 个答案:

答案 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。

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