dotnet / AspNetCore.Docs

Documentation for ASP.NET Core
https://docs.microsoft.com/aspnet/core
Creative Commons Attribution 4.0 International
12.56k stars 25.31k forks source link

Update section on passing tokens in Blazor Web Apps #31691

Open guardrex opened 7 months ago

guardrex commented 7 months ago

Description

Per our offline discussion, either (or both) @halter73 and @JeremyLikness are to review the Pass tokens to a server-side Blazor app section to either establish the content directly (a PR) or provide me enough detail (e.g., Stephen's remarks fleshed out further) in an issue comment to set up the section for BWAs.

Stephen, Jeremy ... We can remove one of you if only one review of the section is reasonable. I assigned both to merely to help keep this on the radar until we get it checked out.

Here's the LIVE section link that merely tells readers that this content is due to be updated ...

https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/additional-scenarios?view=aspnetcore-8.0#pass-tokens-to-a-server-side-blazor-app

What we have for Blazor Server is in the 7.0 version of the article ...

https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/additional-scenarios?view=aspnetcore-7.0#pass-tokens-to-a-server-side-blazor-app

Page URL

https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/additional-scenarios?view=aspnetcore-8.0

Content source URL

https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/server/additional-scenarios.md

Document ID

c98be365-408d-7ee6-cb74-14c44d01b0b8

Article author

@guardrex

timohermans commented 7 months ago

I just found this issue, so I'll just leave my two cents here.

I tried applying this to my own blazor server 8 app. When adding a scoped TokenProvider service, the set value will not be handed over to the circuit. The value will initially be set during the prerender, but on the second oninitializedasync null.

What did work was marking the service as singleton, though I'm not sure if this is the way the documentation intended it to be done.

GStoynev commented 7 months ago

https://github.com/dotnet/AspNetCore.Docs/issues/31759 got closed, but let me share what worked for me, if helpful to patch the documentation or to others searching for solution to the issue:

Capturing a cookie inline like this and then passing it as a Parameter to the Routes component where it's stored in the scoped provider seems to do the trick:

<body>
@{
    var cookie = HttpContext?.Request.Cookies["UI"];
    Logger.LogInformation("Cookie captured: {UICookie}", cookie);
}

<Routes Cookie="@cookie" @rendermode="InteractiveServer"/>
<script src="_framework/blazor.web.js"></script>

</body>
GStoynev commented 7 months ago

Another suggestion for that section of the documentation:

Mixing OpenIdConnect in the example creates the impression that there might be some magic happening there, that alleviates the issue with the pre-rendering and the duplicated TokenProvider.

A simple example with a cookie would have been simpler and to the point.

timohermans commented 7 months ago

@GStoynev wow that is a very easy solution 🤔.

One other thing I found out yesterday is that the httpcontext is always available within a HttpClient implementation, wherever you inject it inside the circuit. Which is also what the documentation does, I believe.

guardrex commented 7 months ago

The product unit (PU) is working through their backlog to reach this issue and take a look at the section. I hope we'll have this sorted out no later than the end of next week (2/23).

guardrex commented 6 months ago

Still trying to get 👁️👁️ on this. I'll try to reach out to Stephen and Jeremy again on Monday.

guardrex commented 6 months ago

~I was just made aware over on a PU issue of the following approach in an Javier sample app ...~

~https://github.com/javiercn/BlazorWebNonceService/~

~I'll take a look at that to resolve this issue ... hopefully within a couple of days.~

No 🎲🎲 .... That only addresses the nonce situation. Also, that sample doesn't match the approach that Stephen adopted for the BWA on https://github.com/dotnet/blazor-samples/pull/240.

guardrex commented 6 months ago

UPDATE: I'm emailing again for notes/code/caveats ... and gotchas 😈 ... to address this issue. For now, I've just made the article section refer to the PU issue for further information.

wildermedeiros commented 6 months ago

Correct me if I'm wrong, I was searching for some way to access the AccessToken after a successfully authentication to use with role claims, and I knocked my head towards the HttpContext with IHttpContextAccessor in the IClaimsTransformation, but didn't work and I believe that was because of the services life cycle. Going forward, exploring the API, I found another way to access the token that I didn't find clearly in the docs:

.AddOpenIdConnect("Oidc", options =>
{
    // OIDC configs

    options.Events.OnTokenValidated = context =>
    {
        var accessToken = context.TokenEndpointResponse!.AccessToken;
        // ...
        return Task.CompletedTask;
    };
 });

In case of having dynamic claims or more authentication schemes, the access token can be stored and accessed later on in the IClaimsTransformation implementation, to provide information for configuring the claims. In any case, it is working and operating for my goal. The project is configured to global interactive server mode. The approach in the code above make sense or have caveats?

bpsc-wkubis commented 5 months ago

I hope this will be resolved sooner rather than later

shurfa commented 5 months ago

seems that it takes too long to solve, my project stopped for this problem to overcome, please give it HP.

guardrex commented 5 months ago

UPDATE (4/21): I've noted this weekly to them because I only understand bits and pieces from the discussion that took place on the product unit's issue. I can't resolve it on my own without their help. There's nothing else that I can do but wait for their response. I think that the workload has been very high for them, and that's why it's taking so long to get an answer. I'll continue to remark on this each week on Fridays. Hopefully, it won't take much longer.

timohermans commented 5 months ago

seems that it takes too long to solve, my project stopped for this problem to overcome, please give it HP.

there have been several suggestions in the issues linked. One of them being using a CircuitHandler to store the token in

setin1219 commented 4 months ago

I am also looking for solution on this same issue. I was trying different things to acquire token once user is authenticated, i can get the claims and all the other stuff but nothing works out for token. Last resort is to move to Blazor WASM and hope it will work!

Kit086 commented 4 months ago

I encountered this issue while using .NET 8's Blazor Web App (@rendermode InteractiveServer). Based on @timohermans suggestion regarding CircuitHandler, I tried using it and managed to resolve the issue. However, I'm not entirely sure if this is the correct and safest approach, but it does work. I'd like to propose it as a temporary solution.

First, create a custom CustomCircuitHandler:

public class CustomCircuitHandler : CircuitHandler
{
    private readonly IServiceProvider _serviceProvider;

    public CustomCircuitHandler(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
    {
        var httpContextAccessor = _serviceProvider.GetRequiredService<IHttpContextAccessor>();
        var httpContext = httpContextAccessor.HttpContext;
        if (httpContext != null)
        {
            var userService = _serviceProvider.GetRequiredService<IdentityUserAccessor>();
            userService.HttpContext = httpContext;
        }

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

My IdentityUserAccessor was automatically generated when creating the Blazor Web App with Individual authentication. I made some slight adjustments, and its code is as follows (with UserManager, ApplicationUser, and IdentityRedirectManager being part of the template):

public sealed class IdentityUserAccessor(
    UserManager<ApplicationUser> userManager,
    IdentityRedirectManager redirectManager)
{
    // Added HttpContext property
    public HttpContext? HttpContext { get; set; }

    public async Task<ApplicationUser> GetRequiredUserAsync(HttpContext? context)
    {
        // Assign context to HttpContext property if not null
        if (context is not null)
        {
            HttpContext = context;
        }

        if (HttpContext is null)
        {
            throw new InvalidOperationException("HttpContext is null.");
        }

        var user = await userManager.GetUserAsync(HttpContext.User);

        if (user is null)
        {
            redirectManager.RedirectToWithStatus("Account/InvalidUser",
                $"Error: Unable to load user with ID '{userManager.GetUserId(HttpContext.User)}'.", HttpContext);
        }

        return user;
    }
}

Ensure these are registered in Program.cs:

builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<CircuitHandler, CustomCircuitHandler>();
builder.Services.AddHttpContextAccessor();

Now, you can use IdentityUserAccessor in your Blazor Web App to get the user:

@inject IdentityUserAccessor UserAccessor
@rendermode InteractiveServer

// ...

@code {
    [CascadingParameter] private HttpContext HttpContext { get; set; } = default!;

    protected override async Task OnInitializedAsync()
    {
        var user = await UserAccessor.GetRequiredUserAsync(HttpContext);
        // ...
    }
    // ...
}

I hope this helps!

Silence-Among-Crows commented 3 months ago

Hi there, I too am having an issue with authentication and would benefit from this documentation. I discovered something quite startling from my own mistakes. I am using Blazor Web App with Interactive auto. This project was created with Individual accounts just as @Kit086 mentioned. And I have the generated IdentityUserAccessor as well.

My Server project has controller endpoints as well as the identity setup and I use EFCore thats all registered there. Treated like an API hosting my Blazor Web App.

I have a service registered on both the server and my client using the same interface. ICoolService. On client-side, I have the Service point to a CoolWebService, which uses HttpClient to send requests to the endpoints, all protected by the authorize attribute and identity. Cool.

I then have my server-side implementation, a scoped service with [authorize] attributes on my methods. This class injects my repository (full of juicy EFCore query methods) and calls the methods that the controller would. Blazor has been so fast (kicked me to wasm too quick) that I never noticed the fatal flaw in my app... the server side is completely unprotected!

I can navigate to a page by pasting it's route in before authenticating (I forgot the authorize attribute) and through server-side Cool Service implementation, the EFCore methods are fired! In my repository I inject HttpContextAccessor to retrieve my UserId from claims, which of course ends up being null. But I was still quite surprised to see it get to this level within the api.

I could protect myself from this by putting authorize on my pages.... oops, but I wanted to know how I could secure the server side implementation of the service because Authorize attributes on those methods did nothing to stop an unauthenticated user from calling those methods... (Probably PEBKAC, please let me know where I went wrong here)

I am going to try @Kit086 's implementation of the circuit handler as described above, as even when authenticated, HTTP context does not give me claims if accessed during interactive server (back to the original issue). I want to know if this is secure. Authentication is really confusing for beginners when starting with Blazor Web App over previous versions like client-side hosted.

ShawnTheBeachy commented 3 months ago

Adding another vote for better docs. My use-case might be slightly outside the scope of this exact issue since I'm specifically trying to implement auth with Microsoft's OIDC libraries, but IMO should be covered in the docs since previous implementations were.

My application is using mixed rendering and needs auth on the pages. It's also serving as a web API. I'm doing something very similar to @Silence-Among-Crows , using an interface and then using database queries on the server side and an HTTP client on the client side. I'm able to get basic auth working with .AddMicrosoftIdentityWebApp() and .AddMicrosoftIdentityWebApi(). I get a JWT and pass it through to the client to use in its API calls like this:

auth.AddMicrosoftIdentityWebApp(options =>
{
    options.Events.OnTokenValidated = context =>
    {
        var accessToken = context.SecurityToken;
        identity.AddClaim(new Claim("token", accessToken.RawData));
        return Task.FromResult(0);
    };
});

However, there doesn't seem to be any good mechanism for handling that JWT expiring. I've tried various approaches, none of them working. I'm sure there's a way, but the docs are severely lacking.

pmi24 commented 1 month ago

I encountered this issue while using .NET 8's Blazor Web App (@rendermode InteractiveServer). Based on @timohermans suggestion regarding CircuitHandler, I tried using it and managed to resolve the issue. However, I'm not entirely sure if this is the correct and safest approach, but it does work. I'd like to propose it as a temporary solution.

This looks promising. Following your approach I am now able to receive the HttpContext in my Blazor Web App w/ global rendermode InteractiveServer. However, when calling SignInManager.SignInAsync(), I am still getting this error:

System.InvalidOperationException: 'Headers are read-only, response has already started.'

@Kit086 How did you manage to get around this?

Kumima commented 6 days ago

@pmi24 For InteractiveServer, HttpContext can actually be accessible without that approach, such as using IHttpContextAccessor. But I think that HttpContext is something like readonly. The reason why you meet the problem is SignInManager.SignInAsync() needs to set the cookie which needs to write the HttpContext. So, the login page in the default template is rendered as static.

Blazor authentication is really a complicated topic, especially now we have the Auto mode. I've notice there are many issues raised to discuss what's the best approach, I hope the documentation will keep improving this.