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"];
}
}