I implemented token refresh in a .NET 7.0 sample recently. There has always been an option to refresh tokens and rewrite cookies, in many MS OIDC stacks, including older ones: Owin, .NET Core etc. It seems poorly documented though, and I had to dig around in the aspnet source code to figure out the cookie rewrite step. So I thought I'd add to this thread in case useful to future readers.
REFRESH TOKEN GRANT
First send a standards based refresh token grant
request:
private async Task<JsonNode> RefreshTokens(HttpContext context)
{
var tokenEndpoint = "https://login.example.com/oauth/v2/token";
var clientId = "myclientid";
var clientSecret = "myclientsecret";
var refreshToken = await context.GetTokenAsync("refresh_token");
var requestData = new[]
{
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("refresh_token", refreshToken),
};
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("accept", "application/json");
var response = await client.PostAsync(tokenEndpoint, new FormUrlEncodedContent(requestData));
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonNode.Parse(json).AsObject();
}
}
REWRITE COOKIES
Then rewrite cookies, which is done by 'signing in' with a new set of tokens. A better method name might have been something like 'update authentication state'. If you then look at the HTTP response you will see an updated set-cookie
header, with the new tokens.
Note that in a refresh token grant response, you may or may not receive a new refresh token and new ID token. If not, then supply the existing values.
private async Task RewriteCookies(JsonNode tokens, HttpContext context)
{
var accessToken = tokens["access_token"]?.ToString();
var refreshToken = tokens["refresh_token"]?.ToString();
var idToken = tokens["id_token"]?.ToString();
var newTokens = new List<AuthenticationToken>();
newTokens.Add(new AuthenticationToken{ Name = "access_token", Value = accessToken });
if (string.IsNullOrWhiteSpace(refreshToken))
{
refreshToken = await context.GetTokenAsync("refresh_token");
}
newTokens.Add(new AuthenticationToken{ Name = "refresh_token", Value = refreshToken });
if (string.IsNullOrWhiteSpace(idToken))
{
idToken = await context.GetTokenAsync("id_token");
}
newTokens.Add(new AuthenticationToken{ Name = "id_token", Value = idToken });
var properties = context.Features.Get<IAuthenticateResultFeature>().AuthenticateResult.Properties;
properties.StoreTokens(newTokens);
await context.SignInAsync(context.User, properties);
}
SUMMARY
Being able to refresh access tokens when you receive a 401 response from an API is an essential capability in any web app. Use short lived access tokens and then code similar to the above, to renew them with good usability.
Note that relying on an expiry time is not fully reliable. API token validation can fail due to infrastructure events in some cases. APIs then return 401 for access tokens that are not expired. The web app should handle this via a refresh, followed by a retry of the API request.