Skip to content

Access AuthenticationStateProvider in outgoing request middleware #52379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
1 task done
ggomarighetti opened this issue Nov 26, 2023 · 15 comments
Open
1 task done

Access AuthenticationStateProvider in outgoing request middleware #52379

ggomarighetti opened this issue Nov 26, 2023 · 15 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components bug This issue describes a behavior which is not expected - a bug. Pillar: Technical Debt
Milestone

Comments

@ggomarighetti
Copy link

ggomarighetti commented Nov 26, 2023

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

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

  • Access AuthenticationStateProvider in outgoing request middleware Click here
  • Access server-side Blazor services from a different DI scope Click here

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

@ghost ghost added the area-blazor Includes: Blazor, Razor Components label Nov 26, 2023
@ggomarighetti ggomarighetti changed the title Access ´AuthenticationStateProvider´ in outgoing request middleware Access AuthenticationStateProvider in outgoing request middleware Nov 26, 2023
@mkArtakMSFT mkArtakMSFT added the Docs This issue tracks updating documentation label Nov 27, 2023
@mkArtakMSFT mkArtakMSFT added this to the Planning: WebUI milestone Nov 27, 2023
@halter73
Copy link
Member

@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
Copy link

ghost commented Dec 18, 2023

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.

@mkArtakMSFT mkArtakMSFT added bug This issue describes a behavior which is not expected - a bug. and removed Docs This issue tracks updating documentation labels Dec 18, 2023
@mkArtakMSFT mkArtakMSFT added the Priority:1 Work that is critical for the release, but we could probably ship without label Jan 3, 2024
@MackinnonBuck
Copy link
Member

@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
Copy link
Author

@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
Copy link

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
Copy link
Author

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

@arkiaconsulting
Copy link

arkiaconsulting commented Jun 8, 2024

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
Copy link
Author

ggomarighetti commented Jun 8, 2024

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
Copy link

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.

@adampaquette
Copy link

Any updates on that?

@adampaquette
Copy link

adampaquette commented Jan 8, 2025

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

@arkiaconsulting Is it still working do you have an exemple code?

@arkiaconsulting
Copy link

@adampaquette I'm sorry but I didn't work with Blazor since many months

@danroth27 danroth27 removed the Priority:1 Work that is critical for the release, but we could probably ship without label Jan 13, 2025
@danroth27 danroth27 added the Priority:2 Work that is important, but not critical for the release label Jan 13, 2025
@fdonnet
Copy link

fdonnet commented Jan 16, 2025

Tested yesterday, and yes, you cannot access a scoped service in a DelegatingHandler.
My case was like for others, retrieve a token from cache, for a typed httpclient and pass the token with each call. (A DelegatingHandler seems appropriate).

I finished with a base httpclient
with this method

protected virtual async Task<HttpResponseMessage> SendAsync(Func<Task<HttpResponseMessage>> httpRequest)
       {
           return await httpRequest();
       }

overriden in child like this:

        protected override async Task<HttpResponseMessage> SendAsync(Func<Task<HttpResponseMessage>> httpRequest)
        {
            await SetSecruityHeaderAsync();
            return await base.SendAsync(httpRequest);
        }

        private async Task SetSecruityHeaderAsync()
        {
            var usertoken = await _userService.GetCurrentUserTokenAsync();

            if (string.IsNullOrEmpty(usertoken))
                throw new ApplicationException("Cannot retrieve info, refresh your app");

            Client.DefaultRequestHeaders.Clear();
            Client.DefaultRequestHeaders.Add("Authorization", $"Bearer {usertoken}");
        }

And here no problemn to access my scoped "UserService" ...
var usertoken = await _userService.GetCurrentUserTokenAsync();

Can confirm it doesn't work to do the same SetSecruityHeaderAsync() in a DelegatingHandler

@danroth27 danroth27 removed the Priority:2 Work that is important, but not critical for the release label Mar 19, 2025
@danroth27
Copy link
Member

Unfortuantely, we no longer expect that we'll get to this in .NET 10.

@MichaelHochriegl
Copy link

I have done this painful process already and found a couple of really hacky workarounds. Guess I really have to write this blog series after all, now that we will not get any improvement in .Net 10 in this area.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components bug This issue describes a behavior which is not expected - a bug. Pillar: Technical Debt
Projects
None yet
Development

No branches or pull requests

12 participants