Home > OS >  ASP.NET Core - JWT authorization always throws 401
ASP.NET Core - JWT authorization always throws 401

Time:01-08

I know this is a common issue, yet as I searched the similiar topics I couldn't find any solution.

I've created a simple API with JWT authorization, however, after adding [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] tag to my Controller, every request (even with JWT Token added in Swagger) throws 401.

JWT is configured like this:

var jwtSettings = new JwtSettings();
        configuration.Bind(nameof(jwtSettings), jwtSettings);
        services.AddSingleton(jwtSettings);

        services.AddScoped<IIdentityService, IdentityService>();

        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(x =>
        {
            x.SaveToken = true;
            x.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Secret)),
                ValidateIssuer = false,
                ValidateAudience = false,
                RequireExpirationTime = false,
                ValidateLifetime = true
            };
        });

Startup class looks like this:

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }
        var swaggerOptions = new SwaggerOptions();
        Configuration.GetSection(nameof(SwaggerOptions)).Bind(swaggerOptions);

        app.UseSwagger(option =>
        {
            option.RouteTemplate = swaggerOptions.JsonRoute;
        });
        app.UseSwaggerUI(option =>
        {
            option.SwaggerEndpoint(swaggerOptions.UIEndpoint, swaggerOptions.Description);
        });
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        //this is correct order
        app.UseAuthentication();
        app.UseRouting();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });

And token generating method looks like this:

public async Task<AuthenticationResult> RegisterAsync(string email, string password)
    {
        var existingUser = await _userManager.FindByEmailAsync(email);
        if (existingUser != null)
        {
            return new AuthenticationResult
            {
                Errors = new[] { "User with this e-mail address already exists" }
            };
        }
        var newUser = new IdentityUser
        {
            Email = email,
            UserName = email
        };
        var createdUser = await _userManager.CreateAsync(newUser, password);
        if (!createdUser.Succeeded)
        {
            return new AuthenticationResult
            {
                Errors = createdUser.Errors.Select(x => x.Description)
            };
        }

        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_jwtSettings.Secret);
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub, newUser.Email),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.Email, newUser.Email),
                new Claim("id", newUser.Id)
            }),
            Expires = DateTime.UtcNow.AddHours(2),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return new AuthenticationResult
        {
            Success = true,
            Token = tokenHandler.WriteToken(token)
        };
    }

Now when my Controller will have the Authorize tag:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class PostsController : ControllerBase
{
    private readonly IPostService _postService;
    public PostsController(IPostService postService)
    {
        _postService = postService;
    }

    [HttpGet(ApiRoutes.Posts.GetAll)]
    public async Task<IActionResult> GetAll()
    {
        return Ok(await _postService.GetPostsAsync()); //returns all Posts from db
    }
}

It always throws 401. The token itself after decoding by jwt.io looks fine:

//header 
{
  "alg": "HS256"
}
//payload
{
  "sub": "[email protected]",
  "jti": "284db32d-6cc3-4532-96cf-55d0df9c3606",
  "email": "[email protected]",
  "id": "e76b112a-55bd-4c4b-832b-32ee7f6b1445",
  "nbf": 1641585546,
  "exp": 1641592746,
  "iat": 1641585546
}
//signature
HMACSHA256(
  base64UrlEncode(header)   "."  
  base64UrlEncode(payload),
  your-256-bit-secret
)

My appsettings:

  "JwtSettings": {
    "Secret": "sYwxnmRz6PpTnoQC7Fj3oQdqLcFtQEdI" //Im aware of not sharing this but this api is just for fun
  },
  "SwaggerOptions": {
    "JsonRoute": "swagger/{documentName}/swagger.json",
    "Description": "ShareThoughtAPI",
    "UIEndpoint": "v1/swagger.json"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

I think I've done everything by the book. I've been following this tutorial and did everything the same, yet the issue persists. I've ensured the correct order of Authorization, Routing and Authentication in Startup class and JWT config is basically copy-pasted. I've read the code 100 times and still can't find anything wrong with it. What am I missing?

CodePudding user response:

This works for me in .NET6. Couldn't make it work without Audience and Issuer attributes, though.

Token generation:

var plainTextSecurityKey = "This is secret";

var signingKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(
    Encoding.UTF8.GetBytes(plainTextSecurityKey));

var signingCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(signingKey,
    Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256Signature);

// -------------------------

var claimsIdentity = new ClaimsIdentity(new List<Claim>()
            {
                new Claim(ClaimTypes.Name, "[email protected]"),
                new Claim(ClaimTypes.Role, "Administrator"),
            }, "Custom");

var securityTokenDescriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor()
{
    Audience = "http://my.website.com",
    Issuer = "http://my.tokenissuer.com",

    Subject = claimsIdentity,
    SigningCredentials = signingCredentials
};

var tokenHandler = new JwtSecurityTokenHandler();
var plainToken = tokenHandler.CreateToken(securityTokenDescriptor);
var signedAndEncodedToken = tokenHandler.WriteToken(plainToken);

Console.WriteLine(signedAndEncodedToken);

Token validation:

builder.Services
.AddAuthentication(cfg =>
{
    cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
    var plainTextSecurityKey = "This is secret";

    var signingKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(
        Encoding.UTF8.GetBytes(plainTextSecurityKey));

    var signingCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(signingKey,
        Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256Signature);

    cfg.TokenValidationParameters = new TokenValidationParameters()
    {            
        ValidateAudience = false,
        ValidateIssuer   = false,
        IssuerSigningKey = signingKey            
    };

    // this is useful as you can see the actual issue
    cfg.Events = new JwtBearerEvents()
    {
        OnAuthenticationFailed = async context =>
        {
            var ex = context.Exception;
        }
    };
});

Note how the OnAuthenticationFailed is attached so that when there's a problem, you can see the exception. Beware, however, that the actual issue can be nested deep in InnerException chain of the exception.

CodePudding user response:

I've managed to find the answer to it. My mistake was nowhere close where I was looking for.

My previous Swagger configuration looked like this:

services.AddSwaggerGen(x =>
            {
                x.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "ShareThoughtAPI", Version = "v1 " });
                var security = new OpenApiSecurityRequirement();
                x.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
                {
                    Description = "JWT Authorization header using the bearer scheme",
                    Name = "Authorization",
                    In = ParameterLocation.Header,
                    Type = SecuritySchemeType.ApiKey
                });
                x.AddSecurityRequirement(security);
            }); 

However the proper way which now works should look like this:

services.AddSwaggerGen(x =>
            {
                x.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "ShareThoughtAPI", Version = "v1 " });
                x.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
                {
                    Description = "JWT Authorization header using the bearer scheme",
                    Name = "Authorization",
                    In = ParameterLocation.Header,
                    Type = SecuritySchemeType.ApiKey
                });
                x.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Id = "Bearer",
                                Type = ReferenceType.SecurityScheme
                            }
                        },
                        new List<string>()
                    }
                });
            });

Now sending the request from Postman or Fiddler works and config token in Swagger doesn't throw 401 all the time.

  •  Tags:  
  • Related