使用AddJwtBearer()时,ASP.NET Core 3.1 JWT签名无效。

13
问题:AddJwtBearer()失败,但手动验证令牌有效。
我正在尝试使用非对称RSA算法生成和验证JWT。
我可以使用这个演示代码轻松生成JWT。
[HttpPost("[action]")]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<IActionResult> JwtBearerToken() {
    AppUser user = await userManager.GetUserAsync(User);

    using RSA rsa = RSA.Create(1024 * 2);
    rsa.ImportRSAPrivateKey(Convert.FromBase64String(configuration["jwt:privateKey"]), out int _);
    var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);

    var jwt = new JwtSecurityToken(
        audience: "identityapp",
        issuer: "identityapp",
        claims: new List<Claim>() {new Claim(ClaimTypes.NameIdentifier, user.UserName)},
        notBefore: DateTime.Now,
        expires: DateTime.Now.AddHours(3),
        signingCredentials: signingCredentials
    );

    string token = new JwtSecurityTokenHandler().WriteToken(jwt);

    return RedirectToAction(nameof(Index), new {jwt = token});
}



我也可以使用以下演示代码验证令牌及其签名。

[HttpPost("[action]")]
[ValidateAntiForgeryToken]
public IActionResult JwtBearerTokenVerify(string token) {
    using RSA rsa = RSA.Create();
    rsa.ImportRSAPrivateKey(Convert.FromBase64String(configuration["jwt:privateKey"]), out int _);

    var handler = new JwtSecurityTokenHandler();
    ClaimsPrincipal principal = handler.ValidateToken(token, new TokenValidationParameters() {
        IssuerSigningKey = new RsaSecurityKey(rsa),
        ValidAudience = "identityapp",
        ValidIssuer = "identityapp",
        RequireExpirationTime = true,
        RequireAudience = true,
        ValidateIssuer = true,
        ValidateLifetime = true,
        ValidateAudience = true,
    }, out SecurityToken securityToken);

    return RedirectToAction(nameof(Index));
}



但是,当访问一个由以下代码保护的端点时,验证失败(401):
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

HTTP头部返回的错误信息:Bearer error="invalid_token", error_description="The signature is invalid"



我的JWT bearer身份验证配置在这里

.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => {
    using var rsa = RSA.Create();
    rsa.ImportRSAPrivateKey(Convert.FromBase64String(Configuration["jwt:privateKey"]), out int _);
                    
    options.IncludeErrorDetails = true;
    options.TokenValidationParameters = new TokenValidationParameters() {
        IssuerSigningKey = new RsaSecurityKey(rsa),
        ValidAudience = "identityapp",
        ValidIssuer = "identityapp",
        RequireExpirationTime = true,
        RequireAudience = true,
        ValidateIssuer = true,
        ValidateLifetime = true,
        ValidateAudience = true,                 
    };
});

我可以轻松使用对称密钥和HmacSha256让它起作用 - 但这不是我想要的。


更新

我已经将异常写入响应中,这就是我得到的结果:

IDX10503: Signature validation failed. Keys tried: 'Microsoft.IdentityModel.Tokens.RsaSecurityKey, KeyId: '', InternalId: '79b1afb2-0c85-43a1-bb81-e2accf9dff38'. , KeyId: 
'.
Exceptions caught:
 'System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'RSA'.
   at System.Security.Cryptography.RSAImplementation.RSACng.ThrowIfDisposed()
   at System.Security.Cryptography.RSAImplementation.RSACng.GetDuplicatedKeyHandle()
   at System.Security.Cryptography.RSAImplementation.RSACng.VerifyHash(ReadOnlySpan`1 hash, ReadOnlySpan`1 signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
   at System.Security.Cryptography.RSAImplementation.RSACng.VerifyHash(Byte[] hash, Byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
   at Microsoft.IdentityModel.Tokens.AsymmetricAdapter.VerifyWithRsa(Byte[] bytes, Byte[] signature)
   at Microsoft.IdentityModel.Tokens.AsymmetricAdapter.Verify(Byte[] bytes, Byte[] signature)
   at Microsoft.IdentityModel.Tokens.AsymmetricSignatureProvider.Verify(Byte[] input, Byte[] signature)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(Byte[] encodedBytes, Byte[] signature, SecurityKey key, String algorithm, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters)
'.
token: '{"alg":"RS256","typ":"JWT"}.{"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier":"mail@mail.com","nbf":1582878368,"exp":1582889168,"iss":"identityapp","aud":"identityapp"}'.

更新 - 解决方案

所以,我猜我从异常消息中找到了解决方法。RSA安全密钥被过早地销毁了。

我将密钥创建从AddJwtBearer()中提取出来,并改用依赖注入。

这似乎完全可以工作。但我不确定这是否是好的做法。

// Somewhere futher up in the ConfigureServices(IServiceCollection services) method
services.AddTransient<RsaSecurityKey>(provider => {
    RSA rsa = RSA.Create();
    rsa.ImportRSAPrivateKey(
        source: Convert.FromBase64String(Configuration["jwt:privateKey"]),
        bytesRead: out int _);
                
        return new RsaSecurityKey(rsa);
});


// Chaining onto services.AddAuthentication()
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => {
    SecurityKey rsa = services.BuildServiceProvider().GetRequiredService<RsaSecurityKey>();
                    
        options.IncludeErrorDetails = true;
        options.TokenValidationParameters = new TokenValidationParameters() {
        IssuerSigningKey = rsa,
        ValidAudience = "identityapp",
        ValidIssuer = "identityapp",
        RequireExpirationTime = true,
        RequireAudience = true,
        ValidateIssuer = true,
        ValidateLifetime = true,
        ValidateAudience = true,
    };

});
1个回答

6

虽然你提出的解决方案似乎可以工作,但是它存在两个问题,我将提供解决方案。

第一个问题是你创建的 RSA 实现了 IDisposable 接口,但是在生命周期(这里是短暂的)内未正确处理资源释放,因为 RSA 不是工厂的直接结果。这会导致资源泄漏,未释放的 RSA 实例可能会在主机运行期间(甚至超出“官方”关闭时间)累积。

第二个问题是你使用 BuildServiceProvider 创建了一个全新的服务提供程序,除了代码其余部分隐式使用的那一个。换句话说,这会在“规范”的依赖注入容器并行创建一个新的。

解决方案如下。(请注意,我无法完美测试你的场景,但我有类似的应用程序。)我将从中间开始关键部分:

services
.AddTransient(provider => RSA.Create())
.AddTransient<SecurityKey>(provider =>
{
    RSA rsa = provider.GetRequiredService<RSA>();
    rsa.ImportRSAPrivateKey(source: Convert.FromBase64String(Configuration["jwt:privateKey"]), bytesRead: out int _);
    return new RsaSecurityKey(rsa);
});

注意 RSA 有自己的工厂,这样可以在正确的时候进行处理。安全密钥也有其自己的工厂,在需要时查找RSA。在我展示的代码之上的某个地方,您应该做类似以下操作:
services
.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
.Configure<SecurityKey>((options, signingKey) =>
{
    options.IncludeErrorDetails = true;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        IssuerSigningKey = signingKey,
        ValidAudience = "identityapp",
        ValidIssuer = "identityapp",
        RequireExpirationTime = true,
        RequireAudience = true,
        ValidateIssuer = true,
        ValidateLifetime = true,
        ValidateAudience = true,
    };
});

services
    .AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
    .AddJwtBearer();

注意,TokenValidationParameters 被移动到了一个 Configure 方法中!在那里的 signingKey 将是你在依赖注入容器中注册的 SecurityKey!因此,我们摆脱了 BuildServiceProvider
注意:Microsoft 的 IdentityModel 似乎存在一个 bug,在某些情况下使用一个 RSA,将其丢弃,然后再使用另一个 RSA 会失败。这是例如在这个 SO 问题背后的潜在问题。你可能会独立于我的解决方案而遇到这个问题。但你可以通过使用 AddSingleton 而不是 AddTransient 来添加你的 RSA(不一定是安全密钥)来避免这个问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接