Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core 3.1 JWT signature invalid when using AddJwtBearer()

Problem: AddJwtBearer() is failing, but verifying the token manually works.

I'm trying to generate and verify a JWT with an asymmetric RSA algo.

I can generate the JWT just fine using this demo code

[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});
}



I'm also able to verify the token and it's signature using the demo code below

[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));
}



But, verification fails (401) when hitting an endpoint protected with
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

Error message from HTTP header: Bearer error="invalid_token", error_description="The signature is invalid"



My JWT bearer auth configuration is here

.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,                 
    };
});

I can easily make it work using a symmetric key and HmacSha256 - but that's not what I'm looking for.


UPDATE

I've written the exception to the response, and this is what I get:

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":"[email protected]","nbf":1582878368,"exp":1582889168,"iss":"identityapp","aud":"identityapp"}'.

UPDATE - Working solution

So, I guess I figured it out from the exception message. The RSA security key was being prematurely disposed.

I extracted the key creation from the AddJwtBearer(), and used dependency injection instead.

This seems to work just fine. But I'm unsure if this is good practice.

// 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,
    };

});
like image 866
Millard Avatar asked Feb 27 '20 13:02

Millard


1 Answers

While your solution apparently works, it has two issues, for which I'll provide solutions.

The first issue is that the RSA you create implements IDisposable, but the disposing is not handled properly within the life cycle (here: transient) since the RSA is not the immediate result of a factory. This results in a resource leak where undisposed RSA instances might accumulate throughout the running time of you host (and even beyond the "official" shutdown).

The second issue is that your use of BuildServiceProvider creates a whole new service provider additionally to the one implicitly used by the rest of the code. In other words, this creates a new dependency injection container in parallel to the "canonical" one.

The solution goes as follows. (Note I can't test your scenario perfectly, but I have something similar in my own application.) I'll start with the key part in the middle:

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);
});

Note how the RSA gets its own factory. So it is disposed at the right time. The security key too has its own factory, which looks up the RSA when needed.

Somewhere above the code I just showed, you would do something like this:

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();

Note how the TokenValidationParameters were moved inside a Configure method! The signingKey there will be the SecurityKey you registered on the dependency injection container! Thus we get rid of BuildServiceProvider.

Caution: Microsoft's IdentityModel seems to have a bug where using an RSA, disposing it, and then using another RSA fails under certain circumstances for the second RSA instance. This is for example the underlying issue behind this SO question. You might run into that issue independently of my solution. But you may sidestep that issue by adding your RSA (not necessarily the security key) with AddSingleton rather than AddTransient.

like image 87
Carsten Führmann Avatar answered Sep 19 '22 07:09

Carsten Führmann