JWT Token authentication, expired tokens still working, .net core Web Api
AuthenticationTokenJwt.Net CoreAuthentication Problem Overview
I'm building a .net core web api.
Preface - I've implemented token authentication as per https://stormpath.com/blog/token-authentication-asp-net-core and https://dev.to/samueleresca/developing-token-authentication-using-aspnet-core. I've also read a few issues on github and here on SO.
This also came in handy https://goblincoding.com/2016/07/24/asp-net-core-policy-based-authorisation-using-json-web-tokens/.
After implementing it all I'm feeling like I'm missing something.
I've created a simple Angular application that sits in a web client. When I authenticate, client is sent a token. I'm storing that in session for now (still in dev so will address security concerns around where to store it later).
Not really sure this (https://stackoverflow.com/questions/26739167/jwt-json-web-token-automatic-prolongation-of-expiration) is useful as I haven't implemented refresh tokens as far as I can see.
I noticed that when I call logout, and then log back in again, the client is sent a new token - as expected. However, if the token expiry time is passed (I set it to 1 minute for testing) and then the page is refreshed, the token seems to remain the same in my app. i.e. it's as if the token never expires?!
I would have expected the client to be returned a 401 Unauthorised error and I can then handle forcing the user to re-authenticate.
Is this not how this should work? Is there some auto-refresh token magic going on in the background that is default (I haven't set up any notion of refresh tokens in the tutorials explicitly)? Or am I missing something about the concept of token auth?
Also - if this is a perpetually refreshing token, should I be concerned about security if the token was ever compromised?
Thanks for your help
Authentication Solutions
Solution 1 - Authentication
I believe this has to do with ClockSkew in JwtBearerOptions.
Change to TimeSpan.Zero as I fhink the default is set to 5 minutes (not 100% sure though).
I have posted some sample code below that is to be placed in Startup.cs => Configure.
app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
AuthenticationScheme = "Jwt",
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = Configuration["Tokens:Audience"],
ValidIssuer = Configuration["Tokens:Issuer"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"])),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}
});
Solution 2 - Authentication
If your expiry time is well over the default (5 mins) or over a set a time like I had and it still considers expired token as valid, and setting the ClockSkew
to TimeSpan.Zero
has no effect, make sure you have the property
ValidateLifetime
set to true
as I had mine set to false
causing the problem, which totally make sense, but it was an easy oversight.
services.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtToken:Issuer"],
ValidAudience = Configuration["JwtToken:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration["JwtToken:SecretKey"]))
};
});
Solution 3 - Authentication
There is an additional delay of 5 minutes in the library itself.
If you are setting 1 minute as indicated for expiration, the total will be 6 minutes. If you set 1 hour the total will be 1 hour and 5 minutes.
Solution 4 - Authentication
In my case, I added a new SecurityTokenDescriptor which contains properties that take the current date and time and expires based on our requirements. Below is a sample login controller with a post request, which in turn returns the user details with a token.
public async Task<ActionResult<UserWithToken>> Login([FromBody] User user)
{
user = await _context.Users
.Include(u => u.Reservations)
.Where(u => u.Email == user.Email
&& u.Password == user.Password)
.FirstOrDefaultAsync();
if (user == null)
{
return NotFound();
}
UserWithToken userWithToken = new UserWithToken(user);
if (userWithToken == null)
{
return NotFound();
}
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_jwtsettings.SecretKey);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Email)
}),
Expires = DateTime.UtcNow.AddMinutes(10),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
userWithToken.Token = tokenHandler.WriteToken(token);
return userWithToken;
}
Here token will expires in 10 minutes.