'Injecting AuthenticationStateProvider into an API Controller in a Blazor Server app

In a Blazor Server app, I have an api controller class that handles file uploads. It works fine (uploads succeed) until I try to inject a custom data service that relies on AuthenticationStateProvider.

I can declare a dummy service, register it with:

    builder.Services.AddScoped<MyService>();

and inject it into the api controller instead of the real data service no problem.

The custom data service's constructor looks like:

public CustomDataService(IConfiguration Configuration, AuthenticationStateProvider Asp)
{
    configuration = Configuration;
    asp = Asp;

    // Get user name and AspNetUserId for use in CRUD functions.
    var authState = asp.GetAuthenticationStateAsync().Result;
    .
    .
    .
}

This works fine when injected into Razor pages, but when injected into the api controller, the row in the data service constructor that calls:

    asp.GetAuthenticationStateAsync().Result;

is the last line to execute before a 500 error goes to the client.

I thought that maybe I needed to register the custom data service with a different resolver, but it's not the custom data service that fails to resolve. It's AuthenticationStateProvider that fails to resolve.

How can I get a valid resolution of AuthenticationStateProvider in this situation?



Solution 1:[1]

I found a workaround/solution.

I changed my services to use HttpContext instead of AuthenticationStateProvider as a way to get at the current UserName and AspNetUserId and this method appears compatible with both Blazor components and API Controllers.

New service class constructor:

    public CustomDataService(IConfiguration Configuration, IHttpContextAccessor HttpContextAccessor)
    {
        configuration = Configuration;

        _httpContextAccessor = HttpContextAccessor;

        userName = HttpContextAccessor.HttpContext.User.Identity.Name;
        aspNetUserId = HttpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
        .
        .
        .
    }

Solution 2:[2]

I also encountered this issue.

Here is my workaround in order to get the AuthenticationState value in a kind of "regular" fashion. You will need to provide the HttpContext object to the alternate method. This is a modified implementation of the default RevalidatingIdentityAuthenticationStateProvider with generic type :

public class RevalidatingIdentityAuthenticationStateProvider<TUser>
    : RevalidatingServerAuthenticationStateProvider where TUser : class
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly IdentityOptions _options;

    public RevalidatingIdentityAuthenticationStateProvider(
        ILoggerFactory loggerFactory,
        IServiceScopeFactory scopeFactory,
        IOptions<IdentityOptions> optionsAccessor)
        : base(loggerFactory)
    {
        _scopeFactory = scopeFactory;
        _options = optionsAccessor.Value;
    }

    protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

    private ClaimsPrincipal GetAnonymousClaim()
    {
        var anonymous = new ClaimsIdentity();
        return new ClaimsPrincipal(anonymous);
    }

    /// <summary>
    /// A HttpContext compatible method to call GetAuthenticationStateAsync
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task<AuthenticationState> GetAuthenticationStateAsync(HttpContext context)
    {
        //If anonymous
        if (context.User == null || context.User.Identity == null || context.User.Identity.IsAuthenticated == false)
        {
            return new AuthenticationState(GetAnonymousClaim());
        }

        var currentAuthState = new AuthenticationState(context.User);

        var cancellationToken = new CancellationToken();
        if (await ValidateAuthenticationStateAsync(currentAuthState, cancellationToken))
        {
            //If the current authstate is validated
            return currentAuthState;
        }

        //If any other case (invalid)
        return new AuthenticationState(GetAnonymousClaim());
    }

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
        // Get the user manager from a new scope to ensure it fetches fresh data
        var scope = _scopeFactory.CreateScope();
        try
        {
            var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
            return await ValidateSecurityStampAsync(userManager, authenticationState.User);
        }
        finally
        {
            if (scope is IAsyncDisposable asyncDisposable)
            {
                await asyncDisposable.DisposeAsync();
            }
            else
            {
                scope.Dispose();
            }
        }
    }

    private async Task<bool> ValidateSecurityStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
    {
        var user = await userManager.GetUserAsync(principal);
        if (user == null)
        {
            return false;
        }
        else if (!userManager.SupportsUserSecurityStamp)
        {
            return true;
        }
        else
        {
            var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType);
            var userStamp = await userManager.GetSecurityStampAsync(user);
            return principalStamp == userStamp;
        }
    }
}

Then in your controller you will just have to call:

var authState = await _authStateProvider.GetAuthenticationStateAsync(HttpContext);

Of course get your own AuthenticationStateProvider implementation through DI.

This can also be enhanced by managing your own logic through the GetAuthenticationStateAsync, as this method is overridable. The only downside, is that you would be have to manage yourself when you are in a regular (expected) Blazor logic and when you are in a controller and having to retrieve the HttpContext from somewhere else.

Of course there is the HttpContextAccessor option, but better to call your own implementation with the parameter, only when needed.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jeremy Caney
Solution 2 Guillaume ZAHRA