I had previously asked a question that was answered properly, but the problem is that when my custom AuthenticationStateProvider
is registered as a scoped
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
I get the following error:
System.InvalidOperationException: GetAuthenticationStateAsync was called before SetAuthenticationState
But, when it is registered as a singleton, it works correctly, However, the single instance creates for the lifetime of the application domain by AddSingelton
, and so this is not good.(Why? Because of :))
What should I do to register my custom AuthenticationStateProvider
as a scoped, but its value is not null?
Edit:
According to @MrC aka Shaun Curtis
Comment:
It's my CustomAuthenticationStateProvider
:
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceScopeFactory _scopeFactory;
public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory)
: base(loggerFactory) =>
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user from a new scope to ensure it fetches fresh data
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();
return await ValidateUserAsync(userManager, authenticationState?.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
{
if (principal is null)
{
return false;
}
var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
if (!int.TryParse(userIdString, out var userId))
{
return false;
}
var user = await userManager.FindUserAsync(userId);
return user is not null;
}
}
And it's a program configuration and service registration:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
#region Authentication
//Authentication
services.AddDbContextFactory<ApplicationDbContext>(options =>
{
options.UseSqlServer(
Configuration.GetConnectionString("LocalDBConnection"),
serverDbContextOptionsBuilder =>
{
var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
serverDbContextOptionsBuilder.CommandTimeout(minutes);
serverDbContextOptionsBuilder.EnableRetryOnFailure();
})
.AddInterceptors(new CorrectCommandInterceptor()); ;
});
//add policy
services.AddAuthorization(options =>
{
options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
});
// Needed for cookie auth.
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.SlidingExpiration = false;
options.LoginPath = "/";
options.LogoutPath = "/login";
//options.AccessDeniedPath = new PathString("/Home/Forbidden/");
options.Cookie.Name = ".my.app1.cookie";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
return cookieValidatorService.ValidateAsync(context);
}
};
});
#endregion
//AutoMapper
services.AddAutoMapper(typeof(MappingProfile).Assembly);
//CustomAuthenticationStateProvider
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
.
.
}
CodePudding user response:
Your problem is probably here:
var scope = _scopeFactory.CreateScope();
/...
var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();
You create a new IOC container and request the instance of IUsersService
from that container.
If IUsersService
is Scoped, it creates a new instance.
IUsersService
requires various other services which the new container must provide.
public UsersService(IUnitOfWork uow, ISecurityService securityService, ApplicationDbContext dbContext, IMapper mapper)
Here's the definition of those services in Startup:
services.AddScoped<IUnitOfWork, ApplicationDbContext>();
services.AddScoped<IUsersService, UsersService>();
services.AddScoped<IRolesService, RolesService>();
services.AddScoped<ISecurityService, SecurityService>();
services.AddScoped<ICookieValidatorService, CookieValidatorService>();
services.AddScoped<IDbInitializerService, DbInitializerService>();
IUnitOfWork
and ISecurityService
are both Scoped, so it creates new instances of these in the the new Container. You almost certainly don't want that: you want to use the ones from the Hub SPA session container.
You have a bit of a tangled web so without a full view of everything I can't be sure how to restructure things to make it work.
One thing you can try is to just get a standalone instance of IUsersService
from the IOC container using ActivatorUtilities
. This instance gets instantiated with all the Scoped services from the main container. Make sure you Dispose
it if it implements IDisposable
.
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceProvider _serviceProvider;
public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
: base(loggerFactory) =>
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(scopeFactory));
protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get an instance of IUsersService from the IOC Container Service to ensure it fetches fresh data
IUsersService userManager = null;
try
{
userManager = ActivatorUtilities.CreateInstance<IUsersService>(_serviceProvider);
return await ValidateUserAsync(userManager, authenticationState?.User);
}
finally
{
userManager?.Dispose();
}
}
private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
{
if (principal is null)
{
return false;
}
var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
if (!int.TryParse(userIdString, out var userId))
{
return false;
}
var user = await userManager.FindUserAsync(userId);
return user is not null;
}
}
For reference this is my test code using the standard ServerAuthenticationStateProvider
in a Blazor Server Windows Auth project.
public class MyAuthenticationProvider : ServerAuthenticationStateProvider
{
IServiceProvider _serviceProvider;
public MyAuthenticationProvider(IServiceProvider serviceProvider, MyService myService)
{
_serviceProvider = serviceProvider;
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
MyService? service = null;
try
{
service = ActivatorUtilities.CreateInstance<MyService>(_serviceProvider);
// Do something with service
}
finally
{
service?.Dispose();
}
return base.GetAuthenticationStateAsync();
}
}
CodePudding user response:
Don't worry about the AddSingelton
in the Blazor apps. Scoped dependencies act the same as Singleton registered dependencies in Blazor apps (^).
- Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
- The Blazor Server hosting model supports the Scoped lifetime across HTTP requests (Just for the Razor Pages or MVC portion of the app) but not across SignalR connection/circuit messages among components that are loaded on the client.
That's why there's a scope.ServiceProvider.GetRequiredService
here to ensure the retrived user is fetched from a new scope and has a fresh data.
Actually this solution is taken from the Microsoft's sample.