We are currently developing a Blazor
app which is secured using short lived (10 minute) Jwt with Refresh Tokens.
Currently we have the Jwt implemented and through the Blazor server side web api can login, generate the Jwt and generate the refresh token.
From the client side I have used the following link;
Authentication With client-side Blazor
and extended the ApiAuthenticationStateProvider.cs
as follows;
public class ApiAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
public ApiAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
{
_httpClient = httpClient;
_localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var savedToken = await _localStorage.GetItemAsync<string>("authToken");
var refreshToken = await _localStorage.GetItemAsync<string>("refreshToken");
if (string.IsNullOrWhiteSpace(savedToken) || string.IsNullOrWhiteSpace(refreshToken))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var userResponse = await _httpClient.GetAsync<UserModel>("api/accounts/user", savedToken);
if(userResponse.HasError)
{
var response = await _httpClient.PostAsync<LoginResponse>("api/login/refreshToken", new RefreshTokenModel { RefreshToken = refreshToken });
//check result now
if (!response.HasError)
{
await _localStorage.SetItemAsync("authToken", response.Result.AccessToken);
await _localStorage.SetItemAsync("refreshToken", response.Result.RefreshToken);
userResponse = await _httpClient.GetAsync<UserModel>("api/accounts/user", response.Result.AccessToken);
}
}
var identity = !userResponse.HasError ? new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, userResponse.Result.Email) }, "apiauth") : new ClaimsIdentity();
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public void MarkUserAsAuthenticated(string email)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, email) }, "apiauth"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
public void MarkUserAsLoggedOut()
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}
}
So if the Jwt fails the first time we try to renew with the refresh token.
The code above is working, however the first issue i found is, if I then navigate to the /fetchData
test end point (which is protected with the [Authorize]
attribute). The page initially runs fine and sends the Jwt in the header. However, if i then f5
and refresh the page I get a 401 unauthorized on the /fecthData
endpoint, i.e. on the code;
@code {
WeatherForecast[] forecasts;
protected override async Task OnInitAsync()
{
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
}
}
Now if to get around this I can manually add the Jwt form localStorage to the header (in my case I use an extension method);
public static async Task<ServiceResponse<T>> GetAsync<T>(
this HttpClient httpClient, string url, string token)
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
var response = await httpClient.GetAsync(url);
return await BuildResponse<T>(response);
}
However, the second issue I have here is that if the Jwt expires during this call I would need to call to use the refresh token to get a new Jwt.
Is there a way I can do this do this with middleware to avoid having to check for a 401 on each call and then renewing the token this way?