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

Posted

技术标签:

【中文标题】JwtSecurityTokenHandler 说更改 1 个字符后 JWT 的签名有效【英文标题】:JwtSecurityTokenHandler says signature of JWT valid after changing 1 char 【发布时间】:2014-12-13 10:56:19 【问题描述】:

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

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

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

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

我们正在使用 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;            
        
    

【问题讨论】:

【参考方案1】:

这似乎发生在 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。

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

【讨论】:

非常感谢!几乎可以肯定我的应用程序以某种方式将旧令牌存储在某处并自动恢复:)

以上是关于JwtSecurityTokenHandler 说更改 1 个字符后 JWT 的签名有效的主要内容,如果未能解决你的问题,请参考以下文章

JwtSecurityTokenHandler 返回小写声明类型

JWTSecurityTokenHandler.ValidateToken() 啥时候真正有效?

JwtSecurityTokenHandler().WriteToken(token) 在托管环境中抛出错误

JwtSecurityTokenHandler 和 TokenValidationParameters

JwtSecurityTokenHandler.ValidateToken 抛出 Lifetime 验证失败异常

JwtSecurityTokenHandler().ValidateToken() :: 签名验证失败...在此上下文中不支持 sha256