Home > Software design >  ASP.NET Core 5 JWT Authentication fails with response code 401
ASP.NET Core 5 JWT Authentication fails with response code 401

Time:10-20

I'm trying to implement JWT based authentication in my ASP.NET Core 5 Web API. However, I always end up with the response code 401 when using my APIs marked with the [Authorize] attribute.

Here's what I have so far. First, my AccountController issues a JWT if the user provides a valid username and password:

[Authorize]
[ApiController]
[Route("api/"   Constants.ApiVersion   "/Accounts")]
public class AccountController : ControllerBase
{
  private readonly UserManager<AppUser>     _userManager;
  private readonly IPasswordHasher<AppUser> _passwordHasher;


  public AccountController(UserManager<AppUser> userManager, IPasswordHasher<AppUser> passwordHasher)
  {
    _userManager    = userManager;
    _passwordHasher = passwordHasher;
  }


  [AllowAnonymous]
  [HttpPost]
  [Route("Token")]
  public async Task<IActionResult> Login([FromForm]LoginBindingModel model)
  {
    if(model == null)
    {
      return BadRequest();
    }

    if(!ModelState.IsValid)
    {
      return BadRequest(ModelState);
    }

    AppUser user = await _userManager.FindByNameAsync(model.UserName);

    if(user == null || !await _userManager.CheckPasswordAsync(user, model.Password))
    {
      return Unauthorized();
    }

    SymmetricSecurityKey    encryptionKey   = new(Encoding.UTF8.GetBytes("TODO_Find_better_key_and_store_as_secret"));
    JwtSecurityTokenHandler jwtTokenHandler = new();
    SecurityTokenDescriptor tokenDescriptor = new()
    {
      Subject            = new ClaimsIdentity(new[] { new Claim("UserName", user.UserName) }),
      Expires            = DateTime.UtcNow.AddDays(7),
      SigningCredentials = new SigningCredentials(encryptionKey, SecurityAlgorithms.HmacSha256Signature)
    };

    SecurityToken jwtToken = jwtTokenHandler.CreateToken(tokenDescriptor);
    string        token    = jwtTokenHandler.WriteToken(jwtToken);

    return Ok(token);
  }


  [HttpPost]
  [Route("ChangePassword")]
  public async Task<ActionResult> ChangePassword([FromBody]ChangePasswordBindingModel model)
  {
    if(model == null)
    {
      return BadRequest();
    }

    if(!ModelState.IsValid)
    {
      return BadRequest(ModelState);
    }

    AppUser user = await _userManager.GetUserAsync(User);

    if(user == null)
    {
      return new StatusCodeResult(StatusCodes.Status403Forbidden);
    }

    IdentityResult result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);

    return GetHttpResponse(result);
  }


  ...
}

This code seems to work as it should. It returns a token that is successfully parsed by jwt.io and contains the username I put into it.

Next, the Startup class looks as follows:

public class Startup
{
  public Startup(IConfiguration configuration)
  {
    Configuration = configuration;
  }


  public IConfiguration Configuration
  {
    get;
  }


  public void ConfigureServices(IServiceCollection services)
  {
    services.Configure<ApplicationSettings>(Configuration.GetSection(nameof(ApplicationSettings)));
    services.AddIdentityCore<AppUser>(options =>
    {
      Configuration.GetSection(nameof(IdentityOptions)).Bind(options);
    });
    services.AddScoped<IPasswordHasher<AppUser>, Identity.PasswordHasher<AppUser>>();
    services.AddTransient<IUserStore<AppUser>, UserStore>();
    services.AddAuthentication(options =>
    {
      options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
      options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(options =>
    {
      options.SaveToken = true;
      options.TokenValidationParameters = new TokenValidationParameters
      {
        ValidateIssuer           = true,
        ValidIssuer              = "whatever",
        ValidateAudience         = true,
        ValidAudience            = "whatever",
        ValidateIssuerSigningKey = true,
        IssuerSigningKey         = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("TODO_Find_better_key_and_store_as_secret"))
      };
    });
    services.AddMvc();
    services.AddControllers();
  }


  // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  {
    if(env.IsDevelopment())
    {
      app.UseDeveloperExceptionPage();
    }

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

I'm sending an HTTP POST request to the Token route which returns me the JWT. After that I'm sending an HTTP POST request with the necessary JSON data in the request body and Authorization: Bearer <the JWT> in the header to the ChangePassword route.

However, that always returns me response code 401 without any additional information or exception.

I'm unaware what the magic in Startup.ConfigureServices is actually supposed to do behind the scenes. Anyway, it obviously doesn't work. Does anyone know what is going on and what to do to make it work?

CodePudding user response:

However, that always returns me response code 401 without any additional information or exception.

That is because you set ValidateIssuer and ValidateAudience true but there is no Issuer and Audience in the generated token.

One way is that you can set Issuer and Audience in code:

SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor()
{
    Issuer= "whatever",
    Audience= "whatever",
    Subject = new ClaimsIdentity(new[] { new Claim("UserName", user.Name) }),
    Expires = DateTime.UtcNow.AddDays(7),
    SigningCredentials = new SigningCredentials(encryptionKey, SecurityAlgorithms.HmacSha256Signature)
};

Another way is that you can set ValidateIssuer and ValidateAudience false:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false,  //change here..
        ValidIssuer = "whatever",
        ValidateAudience = false,  //change here..
        ValidAudience = "whatever",
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("TODO_Find_better_key_and_store_as_secret"))
    };
});
  • Related