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!
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?
I think @DerDingens (in comment) is right, to sum up:
'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
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();
});