【问题标题】:How do I verify an asymmetrically signed JWT in dotnet core?如何在 dotnet core 中验证非对称签名的 JWT?
【发布时间】:2019-11-03 21:08:23
【问题描述】:

我找到了 .NET FW 中的非对称签名示例和 .NET Core 中的对称签名示例,但我无法弄清楚如何在 .NET Core 中非对称地验证 JWT。给定 JWK 集的 URL 或给定公钥,如何在 .NET Core 中验证令牌?

【问题讨论】:

    标签: asp.net-core .net-core jwt rsa


    【解决方案1】:

    非对称签名和对称签名之间的唯一区别是签名密钥。只需为令牌验证参数构造一个新的非对称安全密钥即可。

    假设您想使用 RSA 算法。让我们使用 powershell 导出一对 RSA 密钥,如下所示:

    $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider -ArgumentList 2048
    
    $rsa.ToXmlString($true) | Out-File key.private.xml
    $rsa.ToXmlString($false) | Out-File key.public.xml
    

    现在我们将使用这两个密钥来签署令牌。

    一个小补丁

    由于.NET Core 支持rsa.FromXmlString() api,所以我只是复制@myloveCc's code 在C# 中构造一个RsaParameters(这项工作通过以下ParseXmlString() 方法完成):

    public static class KeyHelper 
    {
        public static RSAParameters ParseXmlString( string xml){
            RSAParameters parameters = new RSAParameters();
    
            System.Xml.XmlDocument xmlDoc = new System.Xml.XmlDocument();
            xmlDoc.LoadXml(xml);
    
            if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue"))
            {
                foreach (System.Xml.XmlNode node in xmlDoc.DocumentElement.ChildNodes)
                {
                    switch (node.Name)
                    {
                        case "Modulus": parameters.Modulus = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "Exponent": parameters.Exponent = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "P": parameters.P = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "Q": parameters.Q = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "DP": parameters.DP = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "DQ": parameters.DQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "InverseQ": parameters.InverseQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                        case "D": parameters.D = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break;
                    }
                }
            }
            else
            {
                throw new Exception("Invalid XML RSA key.");
            }
            return parameters;
        }
    
    
        public static RsaSecurityKey BuildRsaSigningKey(string xml){ 
            var parameters = ParseXmlString(xml);
            var rsaProvider = new RSACryptoServiceProvider(2048);
            rsaProvider.ImportParameters(parameters);
            var key = new RsaSecurityKey(rsaProvider);   
            return key;
        }  
    }
    

    这里我添加了一个BuildRsaSigningKey() 辅助方法来生成一个SecurityKey

    令牌生成

    这是一个使用 RSA 生成令牌的演示:

    
    public string GenerateToken(DateTime expiry)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var Identity = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name,          "..."),
            // ... other claims
       });
    
        var xml = "<RSAKeyValue> load...from..local...files...</RSAKeyValue>";
        SecurityKey key =  KeyHelper.BuildRsaSigningKey(xml); 
    
        var Token = new JwtSecurityToken
        (
            issuer: "test",
            audience: "test-app",
            claims: Identity.Claims,
            notBefore: DateTime.UtcNow,
            expires: expiry,
            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest)
        );
        var TokenString = tokenHandler.WriteToken(Token);
        return TokenString;
    }
    

    令牌验证

    要自动验证它,配置 JWT Bearer 身份验证如下:

    Services.AddAuthentication(A =>
    {
        A.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        A.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(O =>
    {
        var xml = "<RSAKeyValue> load...from..local...files...</RSAKeyValue>";
        var key = KeyHelper.BuildRsaSigningKey(xml);
    
        O.RequireHttpsMetadata = false;
        O.SaveToken = true;
        O.IncludeErrorDetails = true;
        O.TokenValidationParameters = new TokenValidationParameters
        {
            IssuerSigningKey = key,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,   
            // ... other settings
        };
    });
    

    如果您想手动验证:

    public IActionResult ValidateTokenManually(string jwt)
    {
        var xml = "<RSAKeyValue>... the keys ...</RSAKeyValue>";
        SecurityKey key = KeyHelper.BuildRsaSigningKey(xml);    
    
        var validationParameters = new TokenValidationParameters
        {
            IssuerSigningKey = key,
            RequireSignedTokens = true,
            RequireExpirationTime = true,
            ValidateLifetime = true,
            // ... other settings
        };
    
        var tokenHandler = new JwtSecurityTokenHandler();
        var principal = tokenHandler.ValidateToken(jwt, validationParameters, out var rawValidatedToken);
        var securityToken = (JwtSecurityToken)rawValidatedToken;
        return Ok(principal);
    }
    

    【讨论】:

    • 我只需要公钥来验证令牌,对吧?
    • @Genfood 是的,这是真的 :)
    【解决方案2】:

    我最终实现了OpenID Connect Discovery 规范,它允许您以标准格式发布令牌端点和密钥集端点。然后我可以使用AddJwtBearer() AuthenticationBuilder 扩展方法来自动缓存密钥集,验证令牌,并填充ClaimsPrincipal

    要编写您自己的实现 OpenID Connect 发现协议的令牌服务,您需要:

    • 实现一个路由 /keys,它服务于从您的 pfx 证书派生的 Microsoft.IdentityModel.Tokens.JsonWebKeySet 对象。

      JsonWebKeySet GetJwksFromCertificates(IEnumerable<X509Certificate2> certificates)
      {
          var jwks = new JsonWebKeySet();
      
          foreach (var certificate in certificates)
          {
              var rsaParameters = ((RSA)certificate.PublicKey.Key).ExportParameters(false);
      
              var jwk = new JsonWebKey
              {
                  // https://tools.ietf.org/html/rfc7517#section-4
                  Kty = certificate.PublicKey.Key.KeyExchangeAlgorithm,
                  Use = "sig",
                  Kid = certificate.Thumbprint,
                  X5t = certificate.Thumbprint,
      
                  // https://tools.ietf.org/html/rfc7517#appendix-B
                  N = Convert.ToBase64String(rsaParameters.Modulus),
                  E = Convert.ToBase64String(rsaParameters.Exponent),
              };
      
              jwks.Keys.Add(jwk);
          }
      
           return jwks;
      }
      
    • 实现一个返回501 Not Implemented的路由/not-yet-implemented
    • 实现一个路由/.well-known/openid-configurationMicrosoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration 对象提供服务。
      OpenIdConnectConfiguration GetOpenIdConnectConfiguration(string issuer) {
          var configuration = new OpenIdConnectConfiguration
          {
              Issuer = issuer,
              TokenEndpoint = issuer + "/token",
              AuthorizationEndpoint = issuer + "/not-yet-implemented",
              JwksUri = issuer + "/keys",
          };
          configuration.GrantTypesSupported.Add(grantType);
          return configuration;
      }
      
    • 实现一个路由/token,它使用您的应用程序特定逻辑来验证用户并生成ClaimsIdentity,然后使用JwtSecurityTokenHandler 创建一个System.IdentityModel.Tokens.Jwt.JwtSecurityToken

      JwtSecurityToken CreateJwt(
          string issuer,
          TimeSpan lifetime,
          ClaimsIdentity claimsIdentity,
          X509Certificate2 signingCertificate)
      {
          var tokenDescriptor = new SecurityTokenDescriptor
          {
              Issuer = issuer,
              Expires = DateTime.UtcNow.Add(lifetime),
              NotBefore = DateTime.UtcNow,
              Subject = claimsIdentity,
              SigningCredentials = new X509SigningCredentials(signingCertificate),
          };
      
          return new JwtSecurityTokenHandler().CreateJwtSecurityToken(tokenDescriptor);
      }
      

    我还鼓励您为您的 /token 路由实施 OAuth client_credentials 授权流程。

    更新

    我发表了一篇完整的文章:non-paywalled link

    【讨论】:

    • 您能否分享您的方法的示例代码 - 这将非常有帮助。谢谢!!
    • 刚刚添加了路由列表以及如何实现它们
    猜你喜欢
    • 2019-05-21
    • 2021-11-06
    • 2014-05-29
    • 2018-12-13
    • 2020-12-08
    • 2016-07-10
    • 2020-05-15
    • 2016-08-28
    相关资源
    最近更新 更多