3

I have recently been writing application for blazor server (dotnet 6) and am struggling with user authentication.

I currently have it written as follows:

  • I have an access token and a refresh token, the access token is valid for 1 minute and the refresh token is valid for 14 days.
  • When the access token expires, the application checks if the refresh token is valid in the database and if it is, it refreshes it and generates new tokens.
  • Both tokens are stored in localstorage and here my question is, am I doing right?

In blazor I have seen most implementations using localstorage, but heard various voices (to keep them in secure cookies or memory (?)). What is the best way to do this so that the tokens are not vulnerable to csrf, xss etc attacks? Should I keep both tokens in one place or somehow separate them?

I know that in blazor I can use the built-in authorization based on HttpContext and cookies. I need tokens in the database to be able to manage user sessions.

I wrote a CustomAuthenticationStateProvider class that deals with user authentication. Everything works fine, but I don't know if it is well done from the security side.

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        private readonly Blazored.LocalStorage.ILocalStorageService _localStorage;
        private readonly Application.Interfaces.ITokenService _tokenService;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());

        public CustomAuthenticationStateProvider(Blazored.LocalStorage.ILocalStorageService localStorage, Application.Interfaces.ITokenService tokenService, IHttpContextAccessor httpContextAccessor)
        {
            _localStorage = localStorage;
            _tokenService = tokenService;
            _httpContextAccessor = httpContextAccessor;
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            try
            {
                var userSession = await _localStorage.GetItemAsync<UserSession>("UserSession");
                if (userSession == null)
                    return await Task.FromResult(new AuthenticationState(_anonymous));

                if (!_tokenService.ValidateToken(userSession.AuthToken))
                {
                    if (!_tokenService.ValidateToken(userSession.RefreshToken))
                    {
                        await this.UpdateAuthenticationState(null);
                        return await Task.FromResult(new AuthenticationState(_anonymous));
                    }
                    else
                    {
                        var refreshTokenValidInDb = await _tokenService.CheckIfRefreshTokenIsValid(userSession.RefreshToken);
                        if (refreshTokenValidInDb)
                        {
                            if (_httpContextAccessor.HttpContext == null)
                            {
                                return await Task.FromResult(new AuthenticationState(_anonymous));
                            }

                            var userAgent = this.GetUserAgent(_httpContextAccessor.HttpContext);
                            var ipAddress = this.GetIpAddress(_httpContextAccessor.HttpContext);
                            var (authToken, refreshToken) = await _tokenService.RefreshAuthTokens(userSession.RefreshToken, userAgent, ipAddress);

                            userSession.AuthToken = authToken;
                            userSession.RefreshToken = refreshToken;

                            await _localStorage.SetItemAsync("UserSession", userSession);
                        }
                        else
                        {
                            await this.UpdateAuthenticationState(null);
                            return await Task.FromResult(new AuthenticationState(_anonymous));
                        }
                    }
                }

                var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
                {
                    new Claim(ClaimTypes.NameIdentifier, userSession.Id.ToString()),
                    new Claim(ClaimTypes.Name, userSession.Name),
                    new Claim("token", userSession.AuthToken),
                    new Claim("refreshToken", userSession.RefreshToken),
                    new Claim("useragent", userSession.UserAgent),
                    new Claim("ipv4", userSession.IPv4)
                }, "Auth"));


                return await Task.FromResult(new AuthenticationState(claimsPrincipal));
            }
            catch
            {
                return await Task.FromResult(new AuthenticationState(_anonymous));
            }
        }

        public async Task UpdateAuthenticationState(UserSession? userSession)
        {
            ClaimsPrincipal claimsPrincipal;

            if (userSession != null)
            {
                await _localStorage.SetItemAsync("UserSession", userSession);

                claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
                {
                    new Claim(ClaimTypes.NameIdentifier, userSession.Id.ToString()),
                    new Claim(ClaimTypes.Name, userSession.Name),
                    new Claim("token", userSession.AuthToken),
                    new Claim("refreshToken", userSession.RefreshToken),
                    new Claim("useragent", userSession.UserAgent),
                    new Claim("ipv4", userSession.IPv4)
                }));
            }
            else
            {
                await _localStorage.RemoveItemAsync("UserSession");
                claimsPrincipal = _anonymous;
            }

            NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
        }

        /// <summary>
        /// Get ip address from HttpContext
        /// </summary>
        /// <param name="httpContext">HttpContext</param>
        /// <returns>Client ip address</returns>
        private string GetIpAddress(HttpContext httpContext)
        {
            var ipNullable = httpContext.Connection.RemoteIpAddress;
            return (ipNullable != null) ? ipNullable.ToString() : "";
        }

        /// <summary>
        /// Get UserAgent from HttpContext
        /// </summary>
        /// <param name="httpContext">HttpContext</param>
        /// <returns>UserAgent</returns>
        private string GetUserAgent(HttpContext httpContext)
        {
            return httpContext.Request.Headers["User-Agent"];
        }

    }
Pietrek
  • 51
  • 5
  • It all depends on your desired level of security and authentication requirements. More security, more hoops and relogging in for your users. You're saving security information to LocalStorage on the browser. How secure is any data in the Web Browser? I'm ex. High Security Environment so consider the browser and all data in it an open book. – MrC aka Shaun Curtis Nov 12 '22 at 12:44
  • But I have to keep tokens somewhere to know if and what user is logged in. Another option is SessionStorage and deleting the data when the user leaves the tab, the only problem is as if the user would like to remember the login. – Pietrek Nov 12 '22 at 13:01
  • Welcome to the security conundrum! Everything but making the user log in again when the current session expires, or they close it, is a compromise. On one side of the scale is ease of use by the user. Log in once, browser saves your passwords in a "Secure" store and you never need to log in again. On the other end of the scale is log in on every session with two or three factor authentication. If you log out or break the session, back thro the two/three factor authentication. We can't advise you here where you need to be on the scale. – MrC aka Shaun Curtis Nov 12 '22 at 13:20

1 Answers1

2

Finally I did it like this:

  • the access token is generated with a validity of 10 minutes and stored in ProtectedLocalStorage
  • each time you perform an action on the website, the token refreshes
  • the token is also stored in the database and is refreshed there as well
  • if the token is invalid or not in the database, the user is not logged in
Pietrek
  • 51
  • 5