Home > Net >  Why is my custom `AuthentictionStateProvider` not null in AddSingleton but null in AddScoped
Why is my custom `AuthentictionStateProvider` not null in AddSingleton but null in AddScoped

Time:11-03

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.

  • Related