如何在 dotnet core 中验证非对称签名的 JWT?

Posted

技术标签:

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

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

【问题讨论】:

【参考方案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。

【讨论】:

您能否分享您的方法的示例代码 - 这将非常有帮助。谢谢!! 刚刚添加了路由列表以及如何实现它们

以上是关于如何在 dotnet core 中验证非对称签名的 JWT?的主要内容,如果未能解决你的问题,请参考以下文章

在 .Net Core 上使用非对称密钥

openssl生成签名与验证签名

对称加密 和 非对称加密

openssl rsautl(签名/验证签名/加解密文件)和openssl pkeyutl(文件的非对称加密)

密码学基础:非对称加密(RSA算法原理)

DotNet加密方式解析-- 好文收藏