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.37k stars 9.99k forks source link

[Blazor] rendermode="InteractiveAuto" cleared injection of AuthenticationStateProvider #57274

Closed AlexNek closed 2 months ago

AlexNek commented 2 months ago

Is there an existing issue for this?

Describe the bug

I am working on a .NET 8.0 Blazor project utilizing the "auto" render mode, with cookie-based authentication. Demo repo When using a client-side component with the render mode InteractiveAuto on the server side:

<UserInfo @rendermode="InteractiveAuto"/>

And injecting AuthenticationStateProvider within the component:

@inject AuthenticationStateProvider AuthStateProvider

You might encounter a peculiar issue. Upon logging in or logging out, the following error appears:

Error: One or more errors occurred. (Cannot provide a value for property 'AuthStateProvider' on type 'BlazorAuthenticationTest.Client.Components.UserInfo'. There is no registered service of type 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider'.)
    at Jn (marshal-to-js.ts:349:18)
    at Tl (marshal-to-js.ts:306:28)
    at 00b21cf6:0x1fad7
    at 00b21cf6:0x1bf9f
    at 00b21cf6:0xf16c
    at 00b21cf6:0x1e7f1
    at 00b21cf6:0x1efe7
    at 00b21cf6:0xcfbc
    at 00b21cf6:0x44213
    at e.<computed> (cwraps.ts:338:24)

Interestingly, if you clear all cookies, the error disappears. In some cases, clearing the solution and rebuilding the project may also resolve the issue.

Expected Behavior

Independent of render mode I must always have default AuthenticationStateProvider injected

Steps To Reproduce

Demo repo Login/ Logout

Exceptions (if any)

Error: One or more errors occurred. (Cannot provide a value for property 'AuthStateProvider' on type 'BlazorAuthenticationTest.Client.Components.UserInfo'. There is no registered service of type 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider'.) at Jn (marshal-to-js.ts:349:18) at Tl (marshal-to-js.ts:306:28) at 00b21cf6:0x1fad7 at 00b21cf6:0x1bf9f at 00b21cf6:0xf16c at 00b21cf6:0x1e7f1 at 00b21cf6:0x1efe7 at 00b21cf6:0xcfbc at 00b21cf6:0x44213 at e. (cwraps.ts:338:24)

.NET Version

8.0

Anything else?

No response

javiercn commented 2 months ago

@AlexNek thanks for contacting us.

When running in auto mode you have 2 separate processes, one for server and one for webassembly. They are completely independent, and you are responsible for flowing any necessary information between the two.

Your app will potentially execute in three different scopes:

These are three different DI scopes and 2 different processes (server/browser) and for all intents and purposes, three different instantiations of most services.

You need to handle the setup and transfer any information between any of the three scopes. Our implementation on the template for auth shows this pattern.

AlexNek commented 2 months ago

Thanks for the quick answer

Our implementation on the template for auth shows this pattern.

It uses MS Identity. Do you have any additional documentation?

MackinnonBuck commented 2 months ago

@AlexNek the exception indicates that there's no AuthenticationStateProvider registered in DI.

See this commented out line; there should be an AuthenticationStateProvider registered here in order for the service to get injected when the Auto render mode resolves to using WebAssembly.

If you want to share authentication state between the server and client projects, we recommend using an approach similar to what the templates do (using persistent component state).

Please let us know if this resolves the problem you're facing.

AlexNek commented 2 months ago

@MackinnonBuck thanks for notice. It was my first try which is not working. Look like that I must copy internal sealed class PersistingRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider for server and internal class PersistentAuthenticationStateProvider : AuthenticationStateProvider for client /with service registering/ from MS Identity template?

My problem is that all user management is implemented via external API with JWT tokens. So I decided to use cookie identification along with Blazor auto render mode per component.

I try to adapt PersistingRevalidatingAuthenticationStateProvider to cookie authentication:

protected override async Task<bool> ValidateAuthenticationStateAsync(
    AuthenticationState authenticationState, CancellationToken cancellationToken)
{
    // Extract the ClaimsPrincipal from the authentication state
    var user = authenticationState.User;

    // Check if the user is authenticated
    if (user.Identity is { IsAuthenticated: false })
    {
        return false; // User is not authenticated, so the state is invalid
    }

    // Optionally, validate specific claims
    var userIdClaim = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    if (string.IsNullOrEmpty(userIdClaim))
    {
        return false; // No user ID claim found, invalid state
    }

    // Get the user manager from a new scope to ensure it fetches fresh data
    //await using var scope = scopeFactory.CreateAsyncScope();
    //var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
    //return await ValidateSecurityStampAsync(userManager, authenticationState.User);
    // TODO check user state, possible check tokens
    return true;
}

And the next problem JWT update. Try to use server middleware but again problem with cookie update on client side.

Are you planning a full authentication/authorization library for non-MS identity?

javiercn commented 2 months ago

@AlexNek In 9.0 I believe we ship an AuthenticationStateProvider for webassembly that handles persisting the authentication state to the client.

The AuthenticationStateProvider is completely independent of the the Authentication mechanism on the server. Its only "job" is to handle the ClaimsPrincipal.

If you are using cookie authentication there's nothing that should happen client-side, other than transferring the claims principal from the server to the client, either via persisted component state or by hitting an HTTP endpoint from the client to retrieve the data.

There's no magical mechanism for updating the identity on the client unless you create your own via an HTTP endpoint.

AlexNek commented 2 months ago

@javiercn Thanks for the explanation

There's no magical mechanism for updating the identity on the client unless you create your own via an HTTP endpoint.

Why this lines on server working fine for client?

await httpContext.SignInAsync(
     CookieAuthenticationDefaults.AuthenticationScheme,
     claimsPrincipal,
     authProperties);

// Force a full reload to apply the cookie
NavigationManager.NavigateTo("/secure-page", forceLoad: true);

The problem is only when I call the client component that needs _email = user.FindFirst(ClaimTypes.Email)?.Value; which has <UserComponent @rendermode="InteractiveAuto"/>. And I need to use @inject AuthenticationStateProvider AuthStateProvider. Many other non-interactive components on the client side used AuthenticationStateProvider without any problem.

Or are you just talking for the Interactive mode?

javiercn commented 2 months ago

@AlexNek what do you mean by client?

That won't work when running the component on webassembly.

AlexNek commented 2 months ago

@javiercn I have Blazor .NET 8.0 with individual rendering mode by components. So I have components on server side and on client side. In addition, I use Blazor FluentUI library in real project another implementation like in Demo repo I have some components with rendermode InteractiveAuto, some with InteractiveServer and some could work in SSR. I can place components on server side or on client side. If the component uses InteractiveAuto mode, it must be placed on the client side but I can call it from server side. So main project I call server and ProjectName.Client I call client.

MackinnonBuck commented 2 months ago

@AlexNek, placing a component in the .Client project only enables it to be run using WebAssembly, but it does not require it. If a component is rendered non-interactively, it doesn't make a difference whether it's defined in the server or .Client projects. It will always render statically on the server.

So if you're able to access an HttpContext, the component is definitely running on the server.

If a component using the WebAssembly or Auto render modes needs to inject an AuthenticationStateProvider, then the .Client project's Program.cs must register one in DI. We recommend using PersistentComponentState like the templates do (even if they use cookies).

AlexNek commented 2 months ago

@MackinnonBuck

it doesn't make a difference whether it's defined in the server or .Client projects. It will always render statically on the server.

I think only if we forget all other related things. Then why MS FluentUI team placed NavMenu with AutorizeView on client side? In this case menu item is fully functional only after client is loaded.

I am trying to implement your suggestion and copy AuthenticationStateProvider from template but it works partially. As long as WASM is loaded, login state disappears from top right info. I am also trying to move NavMenu to server. ... Error found: it must be user id defined too.

halter73 commented 2 months ago

I took a look at your repro project at https://github.com/ANTestGit/BlazorAuthenticationTest and tried it out. I'm not seeing the issue with the login state disappearing from the top right info.

Error found: it must be user id defined too.

Is this what resolved the issue with the login state disappearing? Overall, what you have right now looks good to me, but it still has a lot of extraneous stuff. You should no longer need <PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.1.34" /> in BlazorAuthenticationTest.Client.csproj (which isn't supported on net8.0 anyway). The Microsoft.Extensions.Http dependency is supported, but no longer needed by the client project. And it looks like CustomAuthStateProviderClient and BaseAuthenticationStateProvider can be removed.

I see you're still using the IHttpClientFactory for some logout methods, but that shouldn't be necessary. Just linking to your logout page should be enough and work on both the server and client. Hitting the logout endpoint with HttpClient will only clear browser cookies when called from the client not the server.

If you want to require an anti-csrf token, you can see how that's done in the Blazor Web OIDC sample here and here, but what you have is fine too. A malicious actor might be able to log people out of your site unwittingly, but that's not a huge deal as far as security risks go imo.

Do you still have any problems with the AuthenticationStateProviders? Or is the persistent component state approach now working well for you?

dotnet-policy-service[bot] commented 2 months ago

Hi @AlexNek. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

AlexNek commented 2 months ago

@halter73 Thank you for your review. It is only the test project, I do not clear it. Can you recommend good blazor forum? I have some other problem like updating access token user friendly.

Or is the persistent component state approach now working well for you?

Thank you all for your help, working nice.