Search code examples
c#asp.net-corejwtasp.net-core-mvcbearer-token

Issue with Using JWT for Authentication in ASP.NET Core MVC


I'm encountering an issue while implementing authentication using JSON Web Token (JWT) in ASP.NET Core MVC. I have an ASP.NET Core API with a login endpoint that returns a JWT in response to user login. Here's the code for my login endpoint in the API:

[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginUserDTO userDTO)
{
    // ... (omission for brevity)
    return Accepted(new TokenRequest { Token = await _authManager.CreateToken(), RefreshToken = await _authManager.CreateRefreshToken() });
}    

public async Task<string> CreateToken()
{
    var signingCredentials = GetSigningCredentials();
    var claims = await GetClaims();
    var token = GenerateTokenOptions(signingCredentials, claims);

    return new JwtSecurityTokenHandler().WriteToken(token);
}

private JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, List<Claim> claims)
{
    var jwtSettings = _configuration.GetSection("Jwt");
    var expiration = DateTime.Now.AddMinutes(Convert.ToDouble(
        jwtSettings.GetSection("lifetime").Value));

    var token = new JwtSecurityToken(
        issuer: jwtSettings.GetSection("Issuer").Value,
        claims: claims,
        expires: expiration,
        signingCredentials: signingCredentials
        );

    return token;
}

private async Task<List<Claim>> GetClaims()
{
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, _user.UserName)
    };

    var roles = await _userManager.GetRolesAsync(_user);

    foreach (var role in roles)
    {
        claims.Add(new Claim(ClaimTypes.Role, role));
    }

    return claims;
}

private SigningCredentials GetSigningCredentials()
{
    var key = _configuration.GetSection("Jwt:KEY").Value;
    var secret = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));

    return new SigningCredentials(secret, SecurityAlgorithms.HmacSha256);
}

Now, in my MVC project, I have a Login method that calls the API for login and securely saves the token:

public async Task<IActionResult> Login(LoginViewModel model)
{
    // ... (omission for brevity)

    var token = await GetAccessToken(model); // call tha API to get the token

    if (!string.IsNullOrEmpty(token))
    {
        SaveToken(token);
        return RedirectToAction("Index", "Home");
    }
    else
    {
        ModelState.AddModelError("", "Invalid username or password");
    }
    return View(model);
} 
private void SaveTokenSecurely(string token)
{
    // Puoi salvare il token in un cookie sicuro, ad esempio:
    var cookieOptions = new CookieOptions
    {
        HttpOnly = true,
        Secure = true,
        SameSite = SameSiteMode.Strict,
        Expires = DateTime.UtcNow.AddHours(1)
    };

    Response.Cookies.Append("AccessToken", token, cookieOptions);
}

Here's how I've configured authentication in the MVC project:

builder.Services.AddAuthentication(auth =>
{
    auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    auth.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
})
 .AddJwtBearer(options =>
 {
     var jwtSettings = builder.Configuration.GetSection("Jwt");
     var key = jwtSettings.GetSection("Key").Value;
     var issuer = jwtSettings.GetSection("Issuer").Value;

     options.TokenValidationParameters = new TokenValidationParameters
     {
         ValidateIssuer = true,
         ValidateAudience = false,
         ValidateLifetime = true,
         ValidateIssuerSigningKey = true,
         ValidIssuer = issuer,
         IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)),

     };
 });

The appsettings.json file is the same for the API and MVC project:

  "Jwt": {
    "Issuer": "MY.PROJECT.Api",
    "lifetime": 15,
    "Key": "SUPERR STRONG KEY"
  }

The problem is that despite successful login and obtaining the token, when I try to access my Privacy page with the [Authorize(Roles = "Administrator")] attribute, I still get a 401 error. I've verified that the "Administrator" role is present in the payload of my token.

I checked the token in jwt.io and here is the result. If the secret base64 encoded is not checked I get Invalid Signature.

Also I tried to call Privacy() from Postman setting the Authorization > Bearer Token > and then the token, this works as expected.

I'm aware that there are several questions on Stack Overflow regarding JWT token unauthorized errors in ASP.NET Core MVC, and I've already looked into some of them:

Does anyone have an idea of what the issue might be or how I can resolve it? Thanks in advance for the help!

Temporary solution

I solve it adding this:

builder.Services.AddAuthentication(auth =>
{
    auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    auth.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
})
 .AddJwtBearer(options =>
 {
     var jwtSettings = builder.Configuration.GetSection("Jwt");
     var key = jwtSettings.GetSection("Key").Value;
     var issuer = jwtSettings.GetSection("Issuer").Value;

     options.TokenValidationParameters = new TokenValidationParameters
     {
         ValidateIssuer = true,
         ValidateAudience = false,
         ValidateLifetime = true,
         ValidateIssuerSigningKey = true,
         ValidIssuer = issuer,
         IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)),
     };
     options.Events = new JwtBearerEvents
     {
         OnMessageReceived = context =>
         {
             var token = context.Request.Cookies["AccessToken"];
             context.Token = token;
             return Task.CompletedTask;
         }
     };
 });

although this is not a very good solution. Could you please provide a complete answer regarding my problem?


Solution

  • I think @DerDingens (in comment) is right, to sum up:

    1. This will never work if HTTP requests does not contain header:
    'Authorization: Bearer <JWT_TOKEN>'
    

    Because this is how JWT works:
    'JWT auth' -> means 'Authorization: Bearer ...' header must be present
    if You use cookie, then:
    'Cookie' Auth -> Cookie must be present

    1. In Your specific case (store JWT in cookie): I think that I saw several times on the internet similar problem, you can solve this with code below. Its very similar to Your solution.

    Solution: Add middleware that sets header 'Authorization: Bearer' before UseAuth.. middleware, for example:

    // Add this middleware before authorization
    // obviously 'AccessToken' is Your cookie name
    app.Use(async (context, next) =>
    {
        var token = context.Request.Cookies["AccessToken"];
    
        if (!string.IsNullOrEmpty(token) &&
            !context.Request.Headers.ContainsKey("Authorization"))
        {
            context.Request.Headers.Add("Authorization", "Bearer " + token);
        }
    
        await next();
    });