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"];
}
}
CodePudding user response:
For server-side Blazor, it is very unlikely that you should implement a custom AuthenticationStateProvider. The built-in implementation already integrates with ASP.NET Core's built-in authentication mechanisms. If you implement a custom one, you may introduce security vulnerabilities.
Steve Sanderson, creator of Blazor
Everything works fine
Perhaps, locally... I guess you eventually want this site to reside on a server and runs remotely, right?
Note that you can't access the HttpContext object by injecting the IHttpContextFactory, as the HttpContext is not available most of the time: Blazor Server Apps do not use the HTTP protocol, but the SignalR protocol. In short, you can't access the HttpContext service, except when your app is accessed initially. See this...
But I have to keep tokens somewhere to know if and what user is logged in.
Do what Identity Server or others do... They use the LocalStorage and the SessionStorage. Is it completely secured ? Probably not. But there are ways to mitigate those security vulnerabilties...
Lots of information is missing about your app, However, I'd advise you to create a custom AuthenticationStateProvider that sets the jwt token you app gets in the local or session storage, and read it from those locations when needed.
CodePudding user response:
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