Session Like Singleton in Blazor Server Side - c#

One of the problems of Blazor Server side is its Singleton and Scoped services, Its singleton is shared between all users making it act like a cache, And scoped service is lost on refresh, I solved this problem to have a Singleton service that is not shared like the following:
First we require two services:
namespace BlazorStore9.Services
{
public class MZSingleton
{
public Dictionary<Guid, Cart> Carts { get; set; } = new Dictionary<Guid, Cart>();
}
public class MZScopedSessionID
{
public Guid SessionID { get; set; }
}
}
Then register them as Singleton and Scoped respectively, And inject them in the Layout pages and other pages or components that need it, The following is a Layout page, That stores and retrives sessionID using Local Storage:
#inject IJsInterop jsInterop
#inject MZSingleton mzSingleton
#inject MZScopedSessionID sessionID
#code
{
private BlazorStore9.Services.Cart cart = null;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
#region some imp codes
if (firstRender)
{
string SessionIDString = await jsInterop.GetAccessToken("SessionID");
if (string.IsNullOrEmpty(SessionIDString) || !Guid.TryParse(SessionIDString, out Guid theGuid))
{
Guid id = Guid.NewGuid();
await jsInterop.SaveAccessToken("SessionID", id.ToString());
sessionID.SessionID = id;
mzSingleton.Carts[sessionID.SessionID] = new Cart();
}
else
{
sessionID.SessionID = Guid.Parse(SessionIDString);
if (!mzSingleton.Carts.ContainsKey(sessionID.SessionID))
mzSingleton.Carts[sessionID.SessionID] = new Cart();
}
cart = mzSingleton.Carts[sessionID.SessionID];
}
}
Then in each page or component we have:
#inject MZSingleton mzSingleton
#inject MZScopedSessionID sessionID
#code
{
private Cart cart = null;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
cart = mzSingleton.Carts[sessionID.SessionID];
}
}
My question is, If this aproach is secure for using with Authentication purposes? Here I showed only a shopping cart that could not be secure, but what for Authentication purposes?

Related

IHostedService cannot access the session storage which already being defined in the page and passed to the service by parameter

I have 4 pages and a service, the purpose of each pages and a service:
First page: animals introduction > when user select animals then start the service
Second page: cities selection
Service (MyService): to get the data from the API and assign the result to the session storage which can be accessed later on from any pages and afterwards stop the service, otherwise loop through the service until it found the result or the service stopped manually
Third page: Summary of what user selected
Fourth page: to stop the service and to get the animals information from the session storage if found (foods liked or disliked by animals selected, animal's age, from where the animal coming from, etc), if not found then call the API directly from this page (but supposedly the data already being assigned to the session storage)
The reason on why I put as a service, is because I don't want user to wait and also user could select multiple animals from the first page and then pass the data to the service and the response from the service could take more than 1 second
Above scenarios already achieved, however when I wants to set the response from the API to the session storage, it didn't response to anything, then when comes to the fourth page, there is no data in session storage which when I manually query the DB, there is a response and also when I put the breakpoint at the line where it will assign to the session storage, it didn't get pass there and the last line after set to session storage never executed
Here is the code that I am using:
First Page
#inject
Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedSessionStorage SessionStorage
#inject MyService MyService
#code {
protected override void OnInitialized()
{
MyService.SessionStorage = SessionStorage;
}
// On user submit will execute below function
private void SetBackgroundService()
{
if (MyService.IsStartService)
return;
MyService.IsStartService = true;
if (!MyService.IsRunning)
MyService.StartAsync(new System.Threading.CancellationToken());
}
}
MyService
public class MyService : IHostedService, IDisposable
{
public bool IsRunning { get; set; }
public bool IsStartService { get; set; }
public Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage.ProtectedSessionStorage SessionStorage { get; set; }
public Task StartAsync(CancellationToken cancellationToken)
{
Task.Run(async () =>
{
if (!IsStartService)
return;
while (!cancellationToken.IsCancellationRequested)
{
if (!IsStartService)
return;
await Task.Delay(2000, cancellationToken);
await DoWorkAsync();
}
}, cancellationToken);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
IsStartService = IsRunning = false;
return Task.CompletedTask;
}
private async Task DoWorkAsync()
{
IsRunning = true;
var Animals = await <API Call>
if (Animals == null)
return;
await SessionStorage.SetAsync("Animals", JsonConvert.SerializeObject(Animals)); // this is where the debug stops
await StopAsync(new CancellationToken()); // this line never executed
}
public void Dispose()
{
}
}
Fourth page
#inject MyService MyService
protected override void OnInitialized()
{
if (MyService.IsRunning)
MyService.StopAsync(new System.Threading.CancellationToken());
}
Startup
public void ConfigureServices(IServiceCollection services)
{
// hide other codes for simplicity
services.AddHostedService<MyService>();
}
Any idea what I am doing wrong?
Thank you very much.

My AuthenticationService is returning null

I have a web client app (Blazor) that is successfully fetching a JWT token after authentication from another project I have created (an API), once the authentication is successfull I am deserialising the response into a User object and storing that in my authentication service so I can use that for further requests and to see if the user is logged in, there seems to be no rhyme or reason as to why this works or does not work, but it seems like the AuthenticationService is either losing my User model.
So I have the following setup (program.cs):
builder.Services
.AddScoped<IAuthenticationService, AuthenticationService>()
.AddScoped<IUserService, UserService>()
.AddScoped<IHttpService, HttpService>()
.AddScoped<ILocalStorageService, LocalStorageService>();
var host = builder.Build();
var authenticationService = host.Services.GetRequiredService<IAuthenticationService>();
await host.RunAsync();
So as far as I'm aware, this should initialise the AuthenticationService once and it should keep that around.
My Authentication service looks like this (AuthenticationService.cs):
using FarmManagerWebClient.Models;
using Flurl;
using Flurl.Http;
using Microsoft.AspNetCore.Components;
namespace FarmManagerWebClient.Services
{
public interface IAuthenticationService
{
User User { get; }
Task Initialize();
Task Login(string username, string password);
Task Logout();
}
public class AuthenticationService : IAuthenticationService
{
private IHttpService _httpService;
private NavigationManager _navigationManager;
private ILocalStorageService _localStorageService;
private IConfiguration _configuration;
private string _apiRootUrl;
public User User { get; private set; }
public AuthenticationService(
IHttpService httpService,
NavigationManager navigationManager,
ILocalStorageService localStorageService,
IConfiguration configuration
)
{
_httpService = httpService;
_navigationManager = navigationManager;
_localStorageService = localStorageService;
_configuration = configuration;
_apiRootUrl = _configuration.GetSection("Settings")["ApiRootUrl"];
}
public async Task Initialize()
{
User = await _localStorageService.GetItem<User>("user");
}
public async Task Login(string username, string password)
{
User = await _apiRootUrl
.AppendPathSegment("/api/v1/users/authenticate")
.PostJsonAsync(new
{
username = username,
password = password
})
.ReceiveJson<User>();
await _localStorageService.SetItem("user", User);
}
public async Task Logout()
{
User = null;
await _localStorageService.RemoveItem("user");
_navigationManager.NavigateTo("/user/login");
}
}
}
Once the login has completed, I (as far as I know) should be able to use this following piece of code to check if the User is authenticated:
#if (AuthenticationService.User != null) { Show/Hide various things }
This seems to work maybe once, but then after a few times of clicking around the site, Authentication.Service.User starts returning null - despite the data still existing in LocalStorage in the browser.
So what am I doing wrong?

Is it possible in Blazor Server to inject scoped service in middleware and inject that same instance in component?

The application is Blazor Server and the question is very similar to Scope in Middleware and Blazor Component but it has not been active for long and I've clarified a few parts.
I've written a middleware that reads a cookie from the current request. A scoped service has been injected (singleton is not suitable since it's per user) via InvokeAsync and it gets updated with a value from the cookie. The same service is injected in a page component but unfortunately it's not the same instance of the service.
I've tried both render-mode="Server" and render-mode="ServerPrerendered". They both behave differently as you would expect but nevertheless it is not the same instance as the one created in the middleware. In render-mode Server the service is injected once as you expected and in render-mode ServerPrerendered the service is injected twice, once for the prerendered page and once for the interactive page. My goal is to have the same scoped service injected for the request in the middleware to also be injected in the page component. Is this possible?
Code for adding middleware (a bit simplified but still same problem). I've added some filtering since I'm only interested in the page request:
app.UseWhen(
context =>
{
return (context.Request.Path.StartsWithSegments("/_content") ||
context.Request.Path.StartsWithSegments("/_framework") ||
context.Request.Path.StartsWithSegments("/_blazor") ||
context.Request.Path.StartsWithSegments("/images") ||
context.Request.Path.StartsWithSegments("/favicon.ico") ||
context.Request.Path.StartsWithSegments("/css")) == false;
}
, builder => builder.UseSettingsMiddleware());
Adding the scoped service:
public void ConfigureServices(IServiceCollection services)
{
/* all other services added before this */
services.AddScoped<IThemeService, ThemeService>();
}
The middleware:
public class ThemeMiddleware
{
private readonly RequestDelegate _next;
private string _id;
public ThemeMiddleware(RequestDelegate next)
{
_next = next;
_id = Guid.NewGuid().ToString()[^4..];
}
public async Task InvokeAsync(HttpContext httpContext, IThemeService themeService)
{
var request = httpContext.Request;
string path = request.Path;
string theme = request.Cookies["App.Theme"];
Debug.WriteLine($"Middleware [{_id}]: Service [{themeService.GetId()}] | Request Path={path} | Theme={theme}");
if(string.IsNullOrEmpty(theme) == false)
{
themeService.SetTheme(theme);
}
await _next(httpContext);
}
}
The service:
public class ThemeService : IThemeService, IDisposable
{
string _theme = "default";
string _id;
string dateTimeFormat = "ss.fffffff";
public ThemeService()
{
_id = Guid.NewGuid().ToString()[^4..];
}
public void Dispose() { }
public string GetId() { return _id; }
public string GetTheme()
{
Debug.WriteLine($"ThemeService [{_id}]: GetTheme={DateTime.Now.ToString(dateTimeFormat)}");
return _theme;
}
public void SetTheme(string theme)
{
Debug.WriteLine($"ThemeService [{_id}]: SetTheme={DateTime.Now.ToString(dateTimeFormat)}");
_theme = theme;
}
}
The component (basically same code also exists in MainLayout.razor):
#page "/"
#inject IThemeService ThemeService
#code {
protected override async Task OnInitializedAsync()
{
System.Diagnostics.Debug.WriteLine($"Index.razor: Service [{ThemeService.GetId()}]");
}
}
Output
render-mode=Server
Middleware [399d]: Service [1f37] | Request Path=/ | Theme=dark
ThemeService [1f37]: SetTheme=00.5996142
MainLayout.razor: Service [4e96]
ThemeService [4e96]: GetTheme=01.0375910
Index.razor: Service [4e96]
render-mode=ServerPrerendered
Middleware [982d]: Service [5fa8] | Request Path=/ | Theme=dark
ThemeService [5fa8]: SetTheme=03.2477461
MainLayout.razor: Service [5fa8]
ThemeService [5fa8]: GetTheme=03.3576799
Index.razor: Service [5fa8]
MainLayout.razor: Service [d27c]
ThemeService [d27c]: GetTheme=03.9510551
Index.razor: Service [d27c]
The service id is actually the same in the prerendered request but not in the interactive one which is the one that counts. Any ideas on how to move forward?
I have seen a similar problem and was able to solve it as follows:
Prepare a wrapper class similar to the one below:
public static class Wrapper<T>
{
private static AsyncLocal<T> _value = new AsyncLocal<T>();
public static T CurrentValue
{
get
{
return _value.Value;
}
set
{
_value.Value = value;
}
}
}
prepare a middleware:
public class TestMiddleware<T>: IMiddleware
{
public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
Wrapper<T>.CurrentValue = /* set references */;
await next(context);
}
}
You may now access the Wrapper<T>.CurrentValue from a Blazor page or a scoped / transient service which was instantiated in the current Blazor circuit.
The root cause is, if I remember correctly by looking in the source code, that the Blazor DI scope is a new instance and not the middleware DI scope.
I opted for another solution to the initial problem, i.e. read a cookie-value and have it available in an injected service on the component-level. I would have preferred to handle this as a middleware, but unfortunately never found a way to do this except when using a singleton service and that is not an option.
Startup.cs:
services.AddScoped<IThemeService, ThemeService>();
ThemeService.cs:
public class ThemeService : IThemeService, IDisposable
{
string _id;
string _theme = "default";
string _themeCookieName;
public ThemeService(IHttpContextAccessor contextAccessor, IOptions<MyConfiguration> myConfig)
{
_id = Guid.NewGuid().ToString()[^4..];
_themeCookieName = myConfig.Value.ThemeCookieName;
RetrieveTheme(contextAccessor);
}
void RetrieveTheme(IHttpContextAccessor contextAccessor)
{
var request = contextAccessor.HttpContext.Request;
string theme = request.Cookies[_themeCookieName];
if (string.IsNullOrEmpty(theme) == false)
{
_theme = theme;
}
}
public void Dispose() { }
public string GetId() { return _id; }
public string GetTheme() { return _theme; }
public void SetTheme(string theme) { _theme = theme; }
}
MainLayout.razor:
#inject IThemeService ThemeService
/* markup */
#code {
bool _darkTheme = false;
MudTheme _theme = new DefaultTheme();
protected override async Task OnInitializedAsync()
{
var currentTheme = ThemeService.GetTheme();
_darkTheme = currentTheme == "dark";
_theme = _darkTheme ? new DarkTheme() : new DefaultTheme();
}
}

How do I access browser local storage from .cs files in blazor?

First of all, I can access localstorage data in .razor pages. I mean I cannot access localstorage data in .cs files. How can I access?
_Imports.razor:
#using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
#inject ProtectedLocalStorage protectedLocalStorage
anyone .razor file:
await protectedLocalStorage.SetAsync(key, JsonSerializer.Serialize(instance));
Above code works for me but I want to call protectedLocalStorage from .cs files additionally.
P.S sorry for grammar mistakes
Edit:
I am using IHttpClientFactory in startup.cs and I want to add token as a header before api request.
startup.cs
services.AddHttpClient("api", hc =>
{
hc.BaseAddress = new Uri("http://localhost:5000/");
string tokenVal = tokenService.GetToken();
if (!String.IsNullOrEmpty(tokenVal))
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenVal);
});
I want to take token value from local storage from this .cs file
public class TokenService : ITokenService
{
private IHttpContextAccessor httpContextAccessor;
public TokenService(IHttpContextAccessor HttpContextAccessor, IProtected) => httpContextAccessor = HttpContextAccessor;
public string GetToken()
{
return "";
}
}
How do I access browser local storage from .cs files in blazor?
ASP.NET supports injection in most constructors. Expanding OP's example:
// Startup.cs -> ConfigureServices(IServiceCollection services)
// Probably not necessary in your case but, to be thorough:
services.AddScoped<ProtectedLocalStorage>();
// SomeFile.cs
public class TokenService : ITokenService
{
// Ignore for the moment that these are being used in the same context
private IHttpContextAccessor httpContextAccessor;
private readonly ProtectedBrowserStorage _storage;
// Injection can happen here in ASP.NET
public TokenService(
IHttpContextAccessor HttpContextAccessor,
ProtectedBrowserStorage storage)
{
httpContextAccessor = HttpContextAccessor;
// injection works but the PBS service might not: see below
_storage = storage;
}
//..
}
However, I don't recommend this for ProtectedBrowserStorage, since it uses IJSRuntime under the hood. If you try to use this in a non-javascript aware context (e.g. during Startup.Configure where the client is still awaiting a response and there is no way to execute javascript), you will run into errors. In Blazor, ProtectedBrowserStorage should only be called - directly or indirectly - from a Blazor component; to keep it simple, wrap it in a class you only use with components, or keep it in the component itself.
Thus, if you are trying to do this:
I am using IHttpClientFactory in startup.cs and I want to add token as a header before api request.
ProtectedBrowserStorage is not the tool for you. Use cookies or another web server technology.
How I solved in the end:
I have created custom authentication class inherited AuthenticationStateProvider. Then I designed all of check processes to be solved on ProtectedLocalStorage.
AuthenticationService
public class AuthenticationService : AuthenticationStateProvider
{
private const string USER_SESSION_OBJECT_KEY = "user_session_obj";
private const string ACCESS_TOKEN = "accesstoken";
private const string USER_PERMISSIONS = "userpermissions";
private readonly ProtectedLocalStorage _protectedLocalStorage;
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthenticationService(ProtectedLocalStorage protectedSessionStore, IHttpContextAccessor httpContextAccessor)
{
_protectedLocalStorage = protectedSessionStore;
_httpContextAccessor = httpContextAccessor;
}
public string IpAddress => _httpContextAccessor?.HttpContext?.Connection?.RemoteIpAddress?.ToString() ?? string.Empty;
private User User { get; set; }
private List<UserPermission> UserPermissionList { get; set; }
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
User userSession = await GetUserSession();
List<UserPermission> userPermissions = await GetUserPermission();
if (userSession != null)
return await GenerateAuthenticationState(userSession, userPermissions);
return await GenerateEmptyAuthenticationState();
}
catch
{
await LogoutAsync();
return null;
}
}
public async Task LoginAsync(User user,List<UserPermission> userPermissions)
{
await SetUserSession(user);
await SetUserPermissionSession(userPermissions);
NotifyAuthenticationStateChanged(GenerateAuthenticationState(user, userPermissions));
}
public async Task LogoutAsync()
{
//await SetUserSession(null);
RefreshUserSession(null);
await _protectedLocalStorage.DeleteAsync(USER_SESSION_OBJECT_KEY);
await _protectedLocalStorage.DeleteAsync(ACCESS_TOKEN);
await _protectedLocalStorage.DeleteAsync(USER_PERMISSIONS);
NotifyAuthenticationStateChanged(GenerateEmptyAuthenticationState());
}
public async Task<User> GetUserSession()
{
if (User != null)
return User;
//TODO burda localUserJson get yaparken hata alıyor. try catch işi çözmezse buraya tekrardan bakılacak.
try
{
var localUserJson = await _protectedLocalStorage.GetAsync<string>(USER_SESSION_OBJECT_KEY);
if (string.IsNullOrEmpty(localUserJson.Value))
return null;
return RefreshUserSession(JsonConvert.DeserializeObject<User>(localUserJson.Value));
}
catch
{
await LogoutAsync();
return null;
}
}
public async Task<List<UserPermission>> GetUserPermission()
{
if (UserPermissionList != null)
return UserPermissionList;
try
{
var localUserPermissionJson = await _protectedLocalStorage.GetAsync<string>(USER_PERMISSIONS);
if (string.IsNullOrEmpty(localUserPermissionJson.Value))
return null;
return RefreshUserPermissionSession(JsonConvert.DeserializeObject<List<UserPermission>>(localUserPermissionJson.Value));
}
catch
{
await LogoutAsync();
return null;
}
}
private async Task SetUserSession(User user)
{
RefreshUserSession(user);
await _protectedLocalStorage.SetAsync(USER_SESSION_OBJECT_KEY, JsonConvert.SerializeObject(user));
}
private async Task SetUserPermissionSession(List<UserPermission> userPermissions)
{
RefreshUserPermissionSession(userPermissions);
await _protectedLocalStorage.SetAsync(USER_PERMISSIONS, JsonConvert.SerializeObject(userPermissions));
}
private User RefreshUserSession(User user) => User = user;
private List<UserPermission> RefreshUserPermissionSession(List<UserPermission> userPermission) => UserPermissionList = userPermission;
private Task<AuthenticationState> GenerateAuthenticationState(User user, List<UserPermission> userPermission)
{
ClaimsIdentity claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, user.Id.ToString()),
new Claim(ClaimTypes.Role, userPermission.ToString()),
}, "auth");
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
return Task.FromResult(new AuthenticationState(claimsPrincipal));
}
private Task<AuthenticationState> GenerateEmptyAuthenticationState() => Task.FromResult(new AuthenticationState(new ClaimsPrincipal()));
}
Then I registered this class in startup.cs
Startup
services.AddScoped<AuthenticationStateProvider, AuthenticationService>();
During changing page, authentication system interrupt showing page to check if it is authenticate or not thanks to below code.
_Imports
#attribute [Authorize]
*You can set localstorage at login page. You can create your way to check thanks to this way.

Blazor ServerApp. Windows Authentication in Class?

I'm using the latest VS2019Pro with Core 3.1.
It seems like Blazor ServerApp has real-time code running within the #code{} tags in the .razor pages. So instead of using APIs to provide the data, I was thinking it would make a lot of sense to just create classes and methods to return the data.
The only issue I am facing is being able to use User.Identity.Name in a Class. Usually this is provided in the .razor pages and in Controllers without issue, but how can I (if it's even possible) use the same User.Identity.Name property within classes?
You can use the standard .net core auth: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-3.1
Basically you define a:
[CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; }
Then you can await it:
private async Task LogUsername()
{
var authState = await authenticationStateTask;
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
_authMessage = $"{user.Identity.Name} is authenticated.";
}
else
{
_authMessage = "The user is NOT authenticated.";
}
}
Edit -----
Something like this??
Base Component:
[CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; }
public String Username {get;set;}
protected override async Task OnInitializedAsync(){
Username = (await authenticationStateTask).User.Identity.Name;
}
Then in your other components:
#inherits BaseCompomnent
#Username
IMyRoleManager _myRoleManager;
private AuthenticationStateProvider _authenticationStateProvider;
public MyRepository(IMyRoleManager myRoleManager, AuthenticationStateProvider authentication)
{
_myRoleManager = myRoleManager;
_authenticationStateProvider = authentication;
}
public async Task<bool> AddRoleToUser(string role)
{
var id = await GetUserIdFromAuthenticationStateProviderAsync();
var result = await _myRoleManager.SetRoleForUser(role, id);
return result;
}
And in the startup file the correct entries have to be there for services.xxx (identity services)

Categories

Resources