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

twi*_*kes 6 rsa jwt .net-core asp.net-core

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

itm*_*nus 10

非对称签名和对称签名之间的唯一区别是签名密钥。只需构造一个新的 ASymmetric Security Key 来令牌验证参数就可以了。

假设您想使用 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
Run Code Online (Sandbox Code Playgroud)

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

一点修补

由于rsa.FromXmlString()api是.NET Core支持的,所以我直接复制@myloveCc的代码RsaParameters在C#中构造a (这个工作是通过以下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;
    }  
}
Run Code Online (Sandbox Code Playgroud)

这里我添加了一个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;
}
Run Code Online (Sandbox Code Playgroud)

令牌验证

要自动验证它,请配置 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
    };
});
Run Code Online (Sandbox Code Playgroud)

如果您想手动验证它:

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);
}
Run Code Online (Sandbox Code Playgroud)

  • @Genfood 是的,确实如此:) (2认同)

twi*_*kes 4

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

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

  • 实现一个为从您的 pfx 证书派生的对象/keys提供服务的路由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;
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • /not-yet-implemented实现返回 的路线501 Not Implemented
  • /.well-known/openid-configuration实现服务于对象的路由Microsoft.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;
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 实现一个路由/token,使用特定于应用程序的逻辑来验证用户身份并生成ClaimsIdentity,然后System.IdentityModel.Tokens.Jwt.JwtSecurityToken使用JwtSecurityTokenHandler.

    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);
    }
    
    Run Code Online (Sandbox Code Playgroud)

我还鼓励您为您的路线实施 OAuthclient_credentials授权流程/token

更新

我发表了完整的文章:非付费链接