dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.23k stars 9.95k forks source link

Access `AuthenticationStateProvider` in outgoing request middleware #52379

Open ggomarighetti opened 10 months ago

ggomarighetti commented 10 months ago

Is there an existing issue for this?

Describe the bug

I try to access AuthenticationStateProvider from a DelegatingHandler which intercepts requests from an HttpClient, following the official documentation guidelines:

But I get errors when implementing it in a completely new Blazor project (Server Side) in .NET 8 SDK.

Expected Behavior

I am looking to access the AuthenticationStateProvider from the DelegateHandler.

Steps To Reproduce

Program.cs

using Microsoft.AspNetCore.Components.Server.Circuits;
using Minerva.Components;
using Minerva.Services.Security;
using Minerva.Services.Util;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Configuration.AddEnvironmentVariables();
var configuration = builder.Configuration;

// test
builder.Services.AddScoped<SecurityServiceAccessor>();
builder.Services.AddScoped<CircuitHandler, SecurityCircuitHandler>();

services.AddTransient<SecurityHandler>();
services.AddHttpClient("Backend.Secured").AddHttpMessageHandler<SecurityHandler>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

SecurityServiceAccessor.cs

namespace Minerva.Services.Util;

public class SecurityServiceAccessor
{
    private readonly AsyncLocal<IServiceProvider?> _services = new();

    public IServiceProvider? Services
    {
        get => _services.Value;
        set => _services.Value = value;
    }
}

SecurityCircuitHandler.cs

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

namespace Minerva.Services.Util;

public class SecurityCircuitHandler : CircuitHandler
{
    private readonly IServiceProvider _serviceProvider;
    private readonly SecurityServiceAccessor _serviceAccessor;

    public SecurityCircuitHandler(IServiceProvider serviceProvider, SecurityServiceAccessor serviceAccessor)
    {
        _serviceProvider = serviceProvider;
        _serviceAccessor = serviceAccessor;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        Console.WriteLine("Circuit Opened  -  " + DateTimeOffset.UtcNow);

        _serviceAccessor.Services = _serviceProvider;
        Console.WriteLine("Service Provider Assigned  -  null: " +
                          (_serviceAccessor.Services.GetService<AuthenticationStateProvider>() == null));

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        Console.WriteLine("Connection Up  -  " + DateTimeOffset.UtcNow);

        _serviceAccessor.Services = _serviceProvider;
        Console.WriteLine("Service Provider Assigned  -  null: " +
                          (_serviceAccessor.Services.GetService<AuthenticationStateProvider>() == null));

        return base.OnConnectionUpAsync(circuit, cancellationToken);
    }

    public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
        Func<CircuitInboundActivityContext, Task> next)
    {
        Console.WriteLine("Circuit Activity  -  " + DateTimeOffset.UtcNow);

        _serviceAccessor.Services = _serviceProvider;
        Console.WriteLine("Service Provider Assigned  -  null: " +
                          (_serviceAccessor.Services.GetService<AuthenticationStateProvider>() == null));

        return base.CreateInboundActivityHandler(next);
    }

    public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        Console.WriteLine("Connection Down  -  " + DateTimeOffset.UtcNow);

        _serviceAccessor.Services = default;

        return base.OnConnectionDownAsync(circuit, cancellationToken);
    }

    public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        Console.WriteLine("Circuit Closed  -  " + DateTimeOffset.UtcNow);

        _serviceAccessor.Services = default;

        return base.OnCircuitClosedAsync(circuit, cancellationToken);
    }
}

SecurityHandler.cs

using Microsoft.AspNetCore.Components.Authorization;
using Minerva.Services.Util;

namespace Minerva.Services.Security;

public class SecurityHandler : DelegatingHandler
{
    private readonly SecurityServiceAccessor _serviceAccessor;

    public SecurityHandler(SecurityServiceAccessor serviceAccessor)
    {
        _serviceAccessor = serviceAccessor;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        Console.WriteLine("Handling...");

        Console.WriteLine("Service Provider Reading  -  null: " +
                          (_serviceAccessor.Services.GetService<AuthenticationStateProvider>() == null));

        Console.WriteLine("Sending async...");
        return await base.SendAsync(request, cancellationToken);
    }
}

Home.razor

@page "/"
@attribute [Authorize]
@rendermode InteractiveServer
@inject IHttpClientFactory ClientFactory

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<button @onclick="Callback"></button>

@code{

    private async Task Callback()
    {
        var httpClient = ClientFactory.CreateClient("Backend.Secured");
        var result = await httpClient.GetAsync("https://catfact.ninja/breeds");
        Console.WriteLine(result.StatusCode);
    }

}

Exceptions (if any)

warn: Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer[100]
      Unhandled exception rendering component: Value cannot be null. (Parameter 'provider')
      System.ArgumentNullException: Value cannot be null. (Parameter 'provider')
         at System.ThrowHelper.Throw(String paramName)
         at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
         at Minerva.Services.Security.SecurityHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) in C:\Users\ggomarighetti\source\repos\minerva-front\Minerva\Services\Security\SecurityHandler.cs:l
ine 29
         at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.<SendCoreAsync>g__Core|5_0(HttpRequestMessage request, Boolean useAsync, CancellationToken cancellationToken)
         at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, Cance
llationToken originalCancellationToken)
         at Minerva.Components.Pages.Home.Callback() in C:\Users\ggomarighetti\source\repos\minerva-front\Minerva\Components\Pages\Home.razor:line 20
         at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
         at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]
      Unhandled exception in circuit 'rKmKcYZ93K8cqm9heF7tQg9y-yxOHUqOi_ZKjRX8FC8'.
      System.ArgumentNullException: Value cannot be null. (Parameter 'provider')
         at System.ThrowHelper.Throw(String paramName)
er.cs:line 56
         at Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost.OnConnectionDownAsync(CancellationToken cancellationToken)

.NET Version

8.0.100

Anything else?

SDK DE .NET: Version: 8.0.100
Commit: 57efcf1350 Workload version: 8.0.100-manifests.6a1e483a

Entorno de tiempo de ejecución:
OS Name: Windows
OS Version: 10.0.19045
OS Platform: Windows
RID: win-x64
Base Path: C:\Program Files\dotnet\sdk\8.0.100\

Cargas de trabajo de .NET instaladas:
Workload version: 8.0.100-manifests.6a1e483a [ios]
Origen de la instalación: VS 17.8.34316.72
Versión del manifiesto: 17.0.8478/8.0.100
Ruta de acceso del manifiesto: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.ios\17.0.8478\WorkloadManifest.json Tipo de instalación: Msi

[maui-windows]
Origen de la instalación: VS 17.8.34316.72 Versión del manifiesto: 8.0.3/8.0.100 Ruta de acceso del manifiesto: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.maui\8.0.3\WorkloadManifest.json Tipo de instalación: Msi

[android] Origen de la instalación: VS 17.8.34316.72 Versión del manifiesto: 34.0.43/8.0.100 Ruta de acceso del manifiesto: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.android\34.0.43\WorkloadManifest.json Tipo de instalación: Msi

[maccatalyst] Origen de la instalación: VS 17.8.34316.72 Versión del manifiesto: 17.0.8478/8.0.100 Ruta de acceso del manifiesto: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.maccatalyst\17.0.8478\WorkloadManifest.json Tipo de instalación: Msi

Host: Version: 8.0.0 Architecture: x64 Commit: 5535e31a71

.NET SDKs installed: 6.0.403 [C:\Program Files\dotnet\sdk] 8.0.100 [C:\Program Files\dotnet\sdk]

.NET runtimes installed: Microsoft.AspNetCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.25 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.14 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 8.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.25 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 8.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.25 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 7.0.14 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 8.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found: x86 [C:\Program Files (x86)\dotnet] registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables: Not set

global.json file: C:\Users\ggomarighetti\source\repos\minerva-front\global.json

Learn more: https://aka.ms/dotnet/info

Download .NET: https://aka.ms/dotnet/download

halter73 commented 9 months ago

@MackinnonBuck Could this be because CreateInboundActivityHandler is not being called when it should be? @rsandbach pointed out that #51934 and #52390 could be related.

ghost commented 9 months ago

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

MackinnonBuck commented 8 months ago

@ggomarighetti, the problem may have been caused by #51934, which is getting addressed in the February servicing release for .NET 8. Would you be able to try the new release in February and see if it resolves the issue?

ggomarighetti commented 8 months ago

@MackinnonBuck no problem, as soon as the new version is available, I will run the test again and report the news in the issue. Thank you very much!

rockfordlhotka commented 8 months ago

Similarly, if this change allows arbitrary assemblies to once again access the user identity in Blazor I'll let you know.

I don't know how we're supposed to build real apps without access to the current user's roles, claims, etc.

ggomarighetti commented 5 months ago

@MackinnonBuck I have tested with the new versions and the issue has not been solved.

arkiaconsulting commented 3 months ago

By looking at this, I managed to get the right DI scope from within my DelegatingHandler

I'm basically putting the access token in a scoped service from within the CircuitHandler.OnConnectionUpAsync method, then I use the CircuitServicesAccessor injected in the DelegatingHandler in order to fetch the right DI scope.

See also this for an example with AuthenticationStateProvider

ggomarighetti commented 3 months ago

Currently I no longer use Blazor for development.

For those who read this message later, the only way to apply authentication and authorization correctly for an application with Blazor is to use a BFF pattern.

Investigate the libraries or tutorials that exist at the time you read this, but, in principle, it is based on developing the Blazor application in client mode (Web Assembly) and securing everything with an ASP.NET REST API on top.

This way you can connect the API to your authentication provider (in my case it was with Auth0) and be able to refresh the access token (in my case it was short lifetime) in an effective and efficient way.

Honorable mention to @rockfordlhotka's comment, I don't know how they expect real applications to be developed with a framework in this state.

I have currently migrated the project to TypeScript with NextJS. If necessary I can keep the issue open until the problem is solved, another way to do it is found or a library is developed that can solve it.

Regards!

seaz5150 commented 1 month ago

The only thing that has worked for me is using the pre-8.0 way described here.

Having to make every component that performs API calls inherit from the custom component base obviously isn't great and even then it sometimes just breaks for reasons I don't understand (even though I am able to work around it).

The new approach added to 8.0 simply does not work, services are always null. Also, CreateInboundActivityHandler never gets called.