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
34.59k stars 9.79k forks source link

Exception triggered by AuthorizeAttribute on the first page loaded in a circuit #55678

Open kjkrum opened 3 weeks ago

kjkrum commented 3 weeks ago

Is there an existing issue for this?

Describe the bug

To learn how auth works in Blazor, I'm writing a minimal implementation from scratch. In the process, I've encountered a strange error.

If the first page loaded in a circuit does not have [Authorize], everything works perfectly. You can freely navigate to a page that does have [Authorize], and the page is displayed as expected. That is, you get either the page content or a "not authorized" message depending on whether the user is authenticated.

However, if the first page loaded in a circuit does have [Authorize], then the app crashes with an error about not being able to find an IAuthenticationService.

This only affects AuthorizeAttribute. There is no error if the first page loaded in the circuit contains an <AuthorizeView>.

Expected Behavior

AuthorizeAttribute should have the same effect on the first page loaded in a circuit as it does on subsequent pages.

Steps To Reproduce

This repo contains the Blazor Web App template and my changes in separate commits for easy review. The README contains instructions for an easy way to see the problem.

https://github.com/kjkrum/BlazorAuthWtf

Exceptions (if any)

System.InvalidOperationException: Unable to find the required 'IAuthenticationService' service. Please add all the required services by calling 'IServiceCollection.AddAuthentication' in the application startup code.

   at Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.GetAuthenticationService(HttpContext context)

   at Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.ChallengeAsync(HttpContext context)

   at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.<>c__DisplayClass0_0.<<HandleAsync>g__Handle|0>d.MoveNext()

--- End of stack trace from previous location ---

   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)

   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

.NET Version

8.0

Anything else?

No response

kjkrum commented 2 weeks ago

I think this is known. This SO answer describes similar behavior:

The way `AuthorizeAttribute works ... will block the page from rendering, so this should prevent Blazor from starting altogether - you will get redirected away to authenticate.

So maybe not a bug per se, but an area that could be improved. What if there were an attribute similar Authorize, but only recognized by Blazor?

mkArtakMSFT commented 2 weeks ago

Thanks for contacting us. Calling MapRazorComponents<App>().AllowAnonymous() might also work if you have the <AuthorizeRouteView> set up correctly. Take a look at https://github.com/dotnet/aspnetcore/issues/53732#issuecomment-2021878231 for more details.

kjkrum commented 2 weeks ago

@mkArtakMSFT Thanks for the link. That's a convenient way of avoiding some of the redundancy. But for full consistency, you still need to ensure that HandleChallengeAsync displays or redirects to the same content as AuthorizeRouteView.NotAuthorized.

If it weren't for the edge case of [Authorize] on the first page, some apps might not need an AuthenticationHandler at all. AuthorizeView works fine without one, as does AuthorizeAttribute anywhere but the first page.

halter73 commented 2 weeks ago

That's a convenient way of avoiding some of the redundancy. But for full consistency, you still need to ensure that HandleChallengeAsync displays or redirects to the same content as AuthorizeRouteView.NotAuthorized.

It would be nice to get rid of this redundancy. Since the AuthenticationHandler is more generic and works for non-Blazor endpoints, that should probably be the source of truth for how to handle unauthorized requests. It might be a good idea to introduce a new default behavior where <AuthorizeRouteView> does a hard refresh whenever there is no explicit <NotAuthorized> content in order to let the authorization middleware observe the request to the [Authorize] endpoint and issue a challenge via the AuthenticationHandler. We'd have to be careful to not get stuck in an infinite refresh loop in the event the authorization middleware doesn't issue a challenge, but it seems doable.

If it weren't for the edge case of [Authorize] on the first page, some apps might not need an AuthenticationHandler at all. AuthorizeView works fine without one, as does AuthorizeAttribute anywhere but the first page.

That's what the MapRazorComponents<App>().AddInteractiveServerRenderMode().AllowAnonymous() suggestion was supposed to fix. Does that not work for you?

If you add that, you shouldn't need an AuthenticationHandler at all unless you have non-Blazor endpoints. That tells the authorization middleware to treat all Blazor endpoints as if they don't require an authenticated user, so it never needs an IAuthenticationService.

Blazor's <AuthorizeRouteView> will still check if the user is authenticated when rendering a page with the [Authorize] attribute, and it will render the <NotAuthorized> content on the first page load just like it does during interactive navigation if you configure MapRazorComponents<App>() with AllowAnonymous().

kjkrum commented 2 weeks ago

@halter73 Aha! I followed that link and saw the AuthenticationHandler wrapping AuthenticationStateProvider but overlooked the significance of the other suggestion. That does neatly solve the problem.

I'm not sure why the authentication middleware is involved in the first place since I'm not calling AddAuthentication/UseAuthentication, but that's probably a rabbit hole for another day.