简体   繁体   中英

Implementing short lived Jwt with Refresh Token with Blazor

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, ie 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?

So often, we are thinking on Blazor as an MVC but it is not. It's more like a desktop app running inside browser. I use JWT and renewing tokens in this way: after login, I have an infinite loop that is pinging backend and keeping the session and renewing the tokens. Simplifying:

class JWTAuthenticationStateProvider : AuthenticationStateProvider
{
    private bool IsLogedIn = false;
    private CustomCredentials credentials = null;
    // private ClaimsPrincipal currentClaimsPrincipal = null; (optinally)
    public Task Login( string user, string password )
    {
         credentials = go_backend_login_service( user, password );
         // do stuff with credentials and claims
         // I raise event here to notify login
         keepSession( );
    }
    public Task Logout(  )
    {
         go_bakcend_logout_service( credentials );
         // do stuff with claims
         IsLogedIn = false;
         // I raise event here to notify logout
    }
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        // make a response from credentials or currentClaimsPrincipal
    }
    private async void KeepSession()
    {
        while(IsLogedIn)
        {
            credentials = go_backend_renewingJWT_service( credentials );
            // do stuff with new credentials: check are ok, update IsLogedIn, ...
            // I raise event here if server says logout
            await Task.Delay(1000);  // sleep for a while.
        }
    }
}

Remember to register component by DI:

public void ConfigureServices(IServiceCollection services)
{
    // ... other services added here ...

    // One JWTAuthenticationStateProvider for each connection on server side.
    // A singleton for clientside.
    services.AddScoped<AuthenticationStateProvider, 
                       JWTAuthenticationStateProvider>();
}

This is just one idea, you should to think about it and adapt it to your own solution.

More about Authentication and Authorization on github SteveSandersonMS/blazor-auth.md

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM