One thing that comes to mind when using access tokens to secure a web api is what do you do when the token expires?
Do you ask the user for credentials again? That’s not really a good option.
This blog post is about using refresh tokens to solve this problem. Specifically in ASP.NET Core Web Apis with JWT tokens.
First, is this really a big deal? Why don’t we just set a long expiration date in the access tokens? For example, a month or even a year?
Because if we do that and someone manages to get hold of that token they can use it for a month, or a year. Even if you change your password.
That’s because a server will trust a token if it’s signature is valid, and the only way to invalidate it is to change the key that was used to sign it, and that has the consequence of invalidating everyone else’s tokens.
Not really an option then. That leads us to the idea of using refresh tokens.
How does a refresh token work then?
Imagine that when you get an access token you also get another one-time-use token: the refresh token. The app stores the refresh token and leaves it alone.
Every time your app sends a request to the server it sends the access token in it (Authorization: Bearer TokenGoesHere
) so that the server knows who you are. There will come a time where the token will expire and the server will let you know of this somehow.
When this happens your app sends the expired token and the refresh token and gets back a new token and refresh token. Rinse, repeat.
If something fishy happens the refresh token can be revoked which means that when the app tries to use it to get a new access token, that request will be rejected and the user will have to enter credentials to be able to log in again.
To make this last point clear, imagine that the app stores the location (e.g. Dublin, Ireland) of the request when a refresh token is created. If user can access this information, and if there’s some login from a place that the user doesn’t recognize, the user can revoke the refresh token so that when the access token expires whoever is using it won’t be able to continue to use the app. This is why it’s probably a good idea to have the access tokens be short lived (i.e be valid for a couple of minutes).
To use refresh tokens we need to be able to do:
- Create access tokens (we will use JWT here)
- Generate, save, retrieve and revoke refresh tokens (server-side)
- Exchange an expired JWT token and refresh token for a new JWT token and refresh token (i.e. refresh a JWT token)
- Use ASP.NET authentication middleware to authenticate a user with JWT tokens
- Have a way to signal that the access token expired to the app (optional)
- When the token expires have the client transparently acquire a new token
If you need information on these topics individually, continue on. If you want to see all this working together, you can find a demo project here.
Create JWT Access Tokens
If you want a more thorough description of how to use JWT with ASP.NET Core I recommend Secure a Web Api in ASP.NET Core. Here’s a summary.
First you need to add the System.IdentityModel.Tokens.Jwt
package:
$ dotnet add package System.IdentityModel.Tokens.Jwt
To create a new JWT token:
private string GenerateToken(IEnumerable<Claim> claims)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("the server key used to sign the JWT token is here, use more than 16 chars"));
var jwt = new JwtSecurityToken(issuer: "Blinkingcaret",
audience: "Everyone",
claims: claims, //the user's claims, for example new Claim[] { new Claim(ClaimTypes.Name, "The username"), //...
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddMinutes(5),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(jwt); //the method is called WriteToken but returns a string
}
Here we are creating a new jwt token with an expiration date of 5 minutes signed using HmacSha256.
Generate, save, retrieve and revoke refresh tokens
The refresh tokens must be unique and it shouldn’t be possible (or it must be very hard) to guess them.
It might seem that a simple GUID satisfies this criteria. Unfortunately the process of generating GUIDs is not random. That means that given a few guids you can easily guess the next one.
Thankfully, there’s a secure random number generator in ASP.NET Core that we can use to generate a string that is unique and even given a few of them it’s very hard to predict how the next one will be:
using System.Security.Cryptography;
//...
public string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create()){
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
Here we are generating a 32 byte long random number and converting it to base64 so that we can use it as a string. There are no guidelines regarding the length other than it should lead to an unique and hard to guess token. I picked 32 but even 16 should be ok.
We will need to generate refresh tokens when we first generate a JWT token and when we “refresh” an expired token.
Every time we generate a new refresh token we should save in a way that it’s linked to the user for which the access token was issued.
The simplest version of this is just to have an extra column for the refresh token in the user’s table. This has the consequence of only allowing the user to be logged-in in one location (there’s only 1 refresh token valid per user at a time).
Alternatively, you can maintain several refresh tokens per user and save the geographical location, time, etc from the request that originated them so that you can provide the user with activity reports.
Also, it is probably a good idea to make the refresh tokens expire, for example after a few days (you’d have to save an expiration date together with the refresh token).
One thing you must not forget to do is to remove a refresh token when it is used in a refresh operation so that it cannot be used more than once.
Exchange an expired JWT and refresh token for a new JWT token and refresh token (i.e. refresh a JWT token)
To get a new access token from an expired one we need to be able to access the claims inside the token even though the token is expired.
When you use the ASP.NET Core authentication middleware for authenticating the user using JWT it will return a 401 response to an expired token.
We need to create a controller action that allows anonymous users and that takes the JWT and refresh tokens.
In that controller action we need to manually validate the expired access token (there’s an option to ignore the token lifetime) and extract all the information about the user contained in it.
We can then use the user information to retrieve the stored refresh token. We can then compare the stored refresh token with the one that was sent in the request.
If all is good we create new JWT and refresh tokens, save the new refresh token, discard the old and send the new JWT and refresh tokens to the client.
Here’s how you can retrieve the user information in the form of a ClaimsPrincipal
from the expired JWT token:
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false, //you might want to validate the audience and issuer depending on your use case
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("the server key used to sign the JWT token is here, use more than 16 chars")),
ValidateLifetime = false //here we are saying that we don't care about the token's expiration date
};
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
return principal;
}
The noteworthy parts in the above snippet are that we are using ValidateLifeTime = false
in the TokenValidationParameters
so that the expired token is considered valid. Also, we are checking that the algorithm used to sign the token is the one we expect (HmacSha256 in this example).
The reason for this is that in theory someone could create a JWT token and set the signing algorithm to “none”. The JWT token would be valid (even if unsigned). This way using a valid refresh token it would be possible to exchange a fake token for a real JWT token.
Now we just need the controller action (it should be a POST since it has side effects and also the tokens are too long for query string parameters):
[HttpPost]
public IActionResult Refresh(string token, string refreshToken)
{
var principal = GetPrincipalFromExpiredToken(token);
var username = principal.Identity.Name;
var savedRefreshToken = GetRefreshToken(username); //retrieve the refresh token from a data store
if (savedRefreshToken != refreshToken)
throw new SecurityTokenException("Invalid refresh token");
var newJwtToken = GenerateToken(principal.Claims);
var newRefreshToken = GenerateRefreshToken();
DeleteRefreshToken(username, refreshToken);
SaveRefreshToken(username, newRefreshToken);
return new ObjectResult(new {
token = newJwtToken,
refreshToken = newRefreshToken
});
}
There are a few assumptions in the snippet above. There’s the retrieving, saving and deleting that I’ve omitted, and also there’s the assumption that there’s only one refresh token per user which is the simplest scenario.
Use the ASP.NET Core authentication middleware to authenticate a user using a JWT token
We need to configure ASP.NET Core’s middleware pipeline so that if a request comes in with a valid Authorization: Bearer JWT_TOKEN
header the user is “signed in”.
If you want a more in-depth discussion about how to setup JWT in particular in ASP.NET Core have a look at Secure a Web Api in ASP.NET Core.
After version 2.0 of ASP.NET Core we add a single authentication middleware to the pipeline and we configure it in Startup.cs
‘ ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
//...
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "bearer";
options.DefaultChallengeScheme = "bearer";
}).AddJwtBearer("bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("the server key used to sign the JWT token is here, use more than 16 chars")),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero //the default for this setting is 5 minutes
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
}
Of note in the snippet above is the handling of the OnAuthenticationFailed
event. It will add a Token-Expired
header to the response when a request comes in with an expired token. The client can use this information to decide to use the refresh token. However, we can just have the client try to use the refresh token when it gets a 401 response. We’ll rely on the Token-Expired
header being in the response for the rest of this blog post.
Now we just need to add the Authentication
middleware to the pipeline:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
//...
The client
The goal here is to build an api client that can realize when a token has expired and take the appropriate actions to get a new token, and do all of this transparently.
When a request fails because of an expired access token, a new request should be sent to a refresh endpoint with the access and refresh tokens. After that request completes and the client gets the new tokens, the original request should be repeated.
The implementation for this will depend on which type of client you are using. Here we will describe a possible JavaScript client. We’ll be relying on the response to a request with an expired token having a header named "Token-Expired"
. We’ll be using fetch
to perform requests to the web api.
async function fetchWithCredentials(url, options) {
var jwtToken = getJwtToken();
options = options || {};
options.headers = options.headers || {};
options.headers['Authorization'] = 'Bearer ' + jwtToken;
var response = await fetch(url, options);
if (response.ok) { //all is good, return the response
return response;
}
if (response.status === 401 && response.headers.has('Token-Expired')) {
var refreshToken = getRefreshToken();
var refreshResponse = await refresh(jwtToken, refreshToken);
if (!refreshResponse.ok) {
return response; //failed to refresh so return original 401 response
}
var jsonRefreshResponse = await refreshResponse.json(); //read the json with the new tokens
saveJwtToken(jsonRefreshResponse.token);
saveRefreshToken(jsonRefreshResponse.refreshToken);
return await fetchWithCredentials(url, options); //repeat the original request
} else { //status is not 401 and/or there's no Token-Expired header
return response; //return the original 401 response
}
}
In the snippet above there’s getJwtToken
, getRefreshToken
, saveJwtToken
and saveRefreshToken
. In a browser these would use the browser’s localStorage
to save and retrieve the tokens, for example:
function getJwtToken() {
return localStorage.getItem('token');
}
function getRefreshToken() {
return localStorage.getItem('refreshToken');
}
function saveJwtToken(token) {
localStorage.setItem('token', token);
}
function saveRefreshToken(refreshToken) {
localStorage.setItem('refreshToken', refreshToken);
}
There also the refresh
function. This function performs a POST request to the api endpoint for refreshing tokens, for example if that endpoint is at /token/refresh
:
async function refresh(jwtToken, refreshToken) {
return fetch('token/refresh', {
method: 'POST',
body: `token=${encodeURIComponent(jwtToken)}&refreshToken=${encodeURIComponent(getRefreshToken())}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}
This is how using this client would look like if you were trying it out in the chrome developer tools console:
One thing to note here is the 401 that shows up in red in the console. That happens when the request fails because of the expired token.
If seeing the “error” (error in quotes because it’s a valid status code and appropriate in this case) is something you’d like to avoid, you can access the token’s expiration date in JavaScript and refresh it before it expires.
A JWT token has 3 parts separated by a a “.”. The second part contains the user’s claims, and in there there’s a claim named exp
which contains the unix time stamp of when the token expires. This is how you can get a JavaScript date object with the expiration date for a JWT token:
var claims = JSON.parse(atob(token.split('.')[1]));
var expirationDate = new Date(claims.exp*1000); //unix timestamp is in seconds, javascript in milliseconds
Checking the expiration date feels convoluted so I don’t recommend doing it (also dealing with timezones is probably a problem). I decided to mention this because it’s something that is interesting to know and that makes it very clear that what you put in a JWT token is not secret, it just can’t be tampered with without making the token invalid.
Hope you’ve found this post interesting. If so drop a line in the comments.