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

How do I verify an asymmetrically signed JWT in dotnet core?

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

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

假设您想使用 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);
}

我最终实现了 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;
    }
    
  • 实现 /not-yet-implemented returns 501 Not Implemented 的路由。
  • 实施服务于 Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration 对象的路由 /.well-known/openid-configuration
    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,然后使用 [= 创建 System.IdentityModel.Tokens.Jwt.JwtSecurityToken 25=].

    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