Persist user session with cookie in Blazor WebAssembly

Viewed 181

How can I persist user session between user usages of the application (closed browser/tab without logout)? Can a Blazor WebAssembly app persist the user session in a cookie with some expiration date?

Right now I got this startup configuration, and everything related to authorization with IdentityServer works fine. The tokens in Identity Server have a 30 days expiration period.

// startup.cs
builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("Local", options.ProviderOptions);
    options.UserOptions.RoleClaim = "RoleName";
});
// appsettings.json
"Local": {
    "Authority": "https://idserver.url",
    "ClientId": "Client",
    "DefaultScopes": [
        "openid",
        "profile",
        "email",
        "roles",
        "offline_access"
    ],
    "ResponseType": "code",
    "PostLogoutRedirectUri": "https://localhost:5004/authentication/logout-callback",
    "RedirectUri": "https://localhost:5004/authentication/login-callback"
}

Is there a way to persist user session in cookies?

1 Answers

I will try my best to answer your question, because I had the same issue and figured it out.

So essentially in between sessions, the cookie is still there saved in the browser, and even though your AuthenticationStateProvider is not set to authenticated, if you try and execute a call against your API, the Cookie Handler will include the cookie in the request and it will authenticate.

So I was able to implement a solution based off an article that I found here: https://www.learmoreseekmore.com/2022/04/blazorwasm-cookie-series-part-1-blazor-webassembly-cookie-authentication.html

I assume that you have a delegation handler that attaches the cookie to outgoing HTTP requests like so:

public class CookieHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
        return await base.SendAsync(request, cancellationToken);
    }
}

This guy will continue to attach that cookie in between sessions.

The problem is that the AuthenticationStateProvider will not persist his state. So what I did was save a local variable into the local browser storage that allows me to remember if I am authenticated. When I check if I am logged in, I make the following checks:

  1. I check if the AutenticationStateProvider is authenticated.
  2. If not, I check if I have set a local variable in the local storage indicating if I am authenticated. If that local variable exists, then he will make an API call to my web service asking for my user information. If that call completes successfully, then I update my AuthenticationStateProvider

I have a dependency injection service I call ILoginService that has an implementation that looks a bit like so:

ILocalStorageService _storageService;
AuthenticationStateProvider _authStateProvider;

public UserModel User { get; private set; } = new UserModel();

public LoginService(ILocalStorageService storageService, IHttpClientFactory clientFactory, AuthenticationStateProvider authStateProvider) : base(clientFactory)
{
    _authStateProvider = authStateProvider;
    _storageService = storageService;
}

public async Task<bool> IsLoggedIn()
{
    var authState = await _authStateProvider.GetAuthenticationStateAsync();
    if (authState.User?.Identity?.IsAuthenticated == true)
    {
        return true;
    }

    var isauthenticated = await _storageService.GetItemAsync<string>("isauthenticated");
    if (!string.IsNullOrWhiteSpace(isauthenticated))
    {
        using (var client = _clientFactory.CreateClient("API"))
        {
            var response = await client.GetAsync("/login");
            if (response.IsSuccessStatusCode)
            {
                string jsonStr = await response.Content.ReadAsStringAsync();
                UserModel? user = JsonConvert.DeserializeObject<UserModel>(jsonStr);
                if (user == null)
                {
                    await _storageService.RemoveItemAsync("isauthenticated");
                    return false;
                }
                else
                {
                    (_authStateProvider as CustomAuthStateProvider)?.SetAuthInfo(user);
                    this.User = user;
                    return true;
                }
            }
        }
    }

    await _storageService.RemoveItemAsync("isauthenticated");
    return false;
}

public async Task<bool> Login(LoginCredentials credentials)
{
    try
    {
        if (credentials == null) throw new ArgumentNullException(nameof(credentials));

        using (var client = _clientFactory.CreateClient("API"))
        {
            StringContent body = new StringContent(JsonConvert.SerializeObject(credentials), System.Text.Encoding.UTF8, "application/json");
            var response = await client.PostAsync("/login", body);
            if (response.IsSuccessStatusCode)
            {
                string jsonStr = await response.Content.ReadAsStringAsync();
                UserModel? user = JsonConvert.DeserializeObject<UserModel>(jsonStr);
                if (user == null)
                {
                    await _storageService.RemoveItemAsync("isauthenticated");
                    return false;
                }
                else
                {
                    (_authStateProvider as CustomAuthStateProvider)?.SetAuthInfo(user);
                    this.User = user;
                    await _storageService.SetItemAsync<string>("isauthenticated", "true");
                    return true;
                }
            }
            else
            {
#if DEBUG
                string responseStr = await response.Content.ReadAsStringAsync();
#endif  
                await _storageService.RemoveItemAsync("isauthenticated");
                return false;
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
        await _storageService.RemoveItemAsync("isauthenticated");
        return false;
    }
}

public async Task<bool> Logout()
{
    try
    {
        using (var client = _clientFactory.CreateClient("API"))
        {
            var response = await client.DeleteAsync("/login");
            if (!response.IsSuccessStatusCode)
            {
#if DEBUG
                string responseStr = await response.Content.ReadAsStringAsync();
#endif
            }
        }

        (_authStateProvider as CustomAuthStateProvider)?.ClearAuthInfo();

        await _storageService.RemoveItemAsync("isauthenticated");
        return true;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
        return false;
    }
}

I hope this helps you fix your problem and anybody else that comes along.

Related