Reputation: 369
I have a Blazor app, server, global, .NET 9. I created my
ApiAuthenticationStateProvider : AuthenticationStateProvider
in which I have GetAuthenticationStateAsync
, Login
, Logout
.
How can I store user information to retrieve it in GetAuthenticationStateAsync
? Theoretically the safest option is a cookie, but when I want to save the created ClaimsPrincipal
using HttpContext.SignInAsync
I get an error:
headers cannot be modified if the response has already been sent to the client.
So I try to use ProtectedLocalStorage
, but already at the start in GetAuthenticationStateAsync
with
await _protectedLocalStorage.GetAsync<string?>("token");
I get the error
System.InvalidOperationException: 'JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.'
In Microsoft's documentation, the only example is returning a user from the backing field, which is weak because all you have to do is refresh the page and you lose information about the logged-in user.
I have no more ideas on how to properly implement GetAuthenticationStateAsync()
and where to store information about the logged-in user and their session, which would be good if it lasted max. 15 minutes and was extended if the user was active.
My test code:
public sealed class ApiAuthenticationStateProvider
: AuthenticationStateProvider
{
private readonly ILogger<ApiAuthenticationStateProvider> _logger;
private readonly ProtectedLocalStorage _protectedLocalStorage;
private readonly IJWTService _jWTService;
private ClaimsPrincipal _getAnnonymousUser => new ClaimsPrincipal(new ClaimsIdentity());
public ApiAuthenticationStateProvider(ILogger<ApiAuthenticationStateProvider> logger, ProtectedLocalStorage protectedLocalStorage, IJWTService jWTService)
{
_logger = logger;
_protectedLocalStorage = protectedLocalStorage;
_jWTService = jWTService;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
ProtectedBrowserStorageResult<string?> result = await _protectedLocalStorage.GetAsync<string?>(Definitions.LocalStorage.JWT_TOKEN);
string? authStateString = result.Value;
if (string.IsNullOrEmpty(authStateString))
{
return new AuthenticationState(_getAnnonymousUser);
}
ClaimsPrincipal? claimsPrincipal = _jWTService.GetClaimsPrincipalFromToken(authStateString);
if (claimsPrincipal is not null)
{
return new AuthenticationState(claimsPrincipal);
}
else
{
return new AuthenticationState(_getAnnonymousUser);
}
}
public async Task<bool> Login(int idOperator, string login, string actor, List<string>? permissions = null)
{
try
{
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, idOperator.ToString()),
new Claim(ClaimTypes.Name, login),
new Claim(ClaimTypes.Actor, actor),
};
if (permissions.IsAny())
{
claims.AddRange(permissions.Select(permission => new Claim(Definitions.Claim.PERMISSION, permission)));
}
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
string jwtToken = _jWTService.GenerateJwt(claimsPrincipal, expirationTimeInMin: 15);
await _protectedLocalStorage.SetAsync(Definitions.LocalStorage.JWT_TOKEN, jwtToken);
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during login.");
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_getAnnonymousUser)));
return false;
}
}
public async Task Logout()
{
await _protectedLocalStorage.DeleteAsync(Definitions.LocalStorage.JWT_TOKEN);
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_getAnnonymousUser)));
}
}
Upvotes: 0
Views: 41
Reputation: 8811
HttpContext available in SSR while browser storage available in interacitve. In .NET9. You could store it in cookie and use different way to access depending on the renderinfo which was newly introduced.
Put following cookie operate script in App.razor
<script>
function WriteCookie(name, value, days) {
var expires;
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toGMTString();
}
else {
expires = "";
}
document.cookie = name + "=" + value + expires + "; path=/";
}
function ReadCookie(name) {
console.log("test");
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
</script>
Then use jsinterop to access browser cookie when interactive.
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IJSRuntime js;
private readonly ILogger<CustomAuthenticationStateProvider> _logger;
private RendererInfo _rendererinfo;
public void SetRenderInfo(RendererInfo rendererinfo)
{
_rendererinfo = rendererinfo;
}
public CustomAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor, IJSRuntime js, ILogger<CustomAuthenticationStateProvider> logger)
{
this._httpContextAccessor = httpContextAccessor;
this.js = js;
this._logger = logger;
}
public async override Task<AuthenticationState> GetAuthenticationStateAsync()
{
string token = "";
if (_rendererinfo.IsInteractive)
{
token = await js.InvokeAsync<string>("ReadCookie", "token");
}
else
{
token = _httpContextAccessor.HttpContext.Request.Cookies["token"];
}
if (string.IsNullOrEmpty(token))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var tokenHandler = new JwtSecurityTokenHandler();
var identity = new ClaimsIdentity(tokenHandler.ReadJwtToken(token).Claims, "jwt");
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(identity)));
}
public async Task UpdateAsync(string token)
{
if (_rendererinfo.IsInteractive)
{
await js.InvokeVoidAsync("WriteCookie", "token", token, DateTime.Now.AddMinutes(1));
}
else
{
_httpContextAccessor.HttpContext.Response.Cookies.Append("token", token);
}
var identity = new ClaimsIdentity();
if (!string.IsNullOrEmpty(token))
{
var tokenHandler = new JwtSecurityTokenHandler();
identity = new ClaimsIdentity(tokenHandler.ReadJwtToken(token).Claims, "jwt");
}
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(new ClaimsPrincipal(identity))));
}
public async Task<bool> Login(int idOperator, string login, string actor, List<string>? permissions = null)
{
try
{
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, idOperator.ToString()),
new Claim(ClaimTypes.Name, login),
new Claim(ClaimTypes.Actor, actor),
};
if (permissions.IsAny())
{
claims.AddRange(permissions.Select(permission => new Claim(Definitions.Claim.PERMISSION, permission)));
}
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
string jwtToken = _jWTService.GenerateJwt(claimsPrincipal, expirationTimeInMin: 15);
await UpdateAsync(jwtToken);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during login.");
return false;
}
}
public async Task Logout()
{
await UpdateAsync("");
}
}
Set the renderinfo(availabe in component) before use provder.
@inject AuthenticationStateProvider stateprovider
@code{
...
var customprovider=((CustomAuthenticationStateProvider)stateprovider);
customprovider.SetRenderInfo(RendererInfo);
var user = await customprovider.GetAuthenticationStateAsync();
}
Upvotes: 0