aspnetcore
aspnetcore copied to clipboard
Exception triggered by AuthorizeAttribute on the first page loaded in a circuit
Is there an existing issue for this?
- [X] I have searched the existing issues
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
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?
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.
@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.
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 asAuthorizeRouteView.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 anAuthenticationHandler
at all.AuthorizeView
works fine without one, as doesAuthorizeAttribute
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()
.
@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.
@guardrex this seems to be a gap in our migration guide. Could you please include it there? Perhaps include it as part of https://github.com/dotnet/AspNetCore.Docs/issues/32138
Potentially related / same issue: https://github.com/dotnet/aspnetcore/issues/51836
Maybe this or that related issue is the right place to raise this question: why is the ASP.NET Core auth middleware in play at all if I'm not calling AddAuthentication
/UseAuthentication
?
I think the reason is because AddInteractiveServerComponents
calls AddServerSideBlazor
which calls AddSignalR
which calls AddConnections
and AddSignalRCore
both of which in turn call AddAuthorization
. WebApplicationBuilder
then sees that AddAuthorization
has been called without a call UseAuthorization
, calls it for you.
Other things that support [Authorize]
like MVC, Razor Pages and Blazor SSR have ways to add them without calling AddAuthorization
even if they are usually brought in by default. Maybe SignalR could introduce a way to add its dependencies without bringing in auth. Then there could also be new versions of AddInteractiveServerComponents
and AddServerSideBlazor
that use the auth-less version. I think changing the behavior of the existing APIs would be too breaking though, and it's not clear how many people need and would use new auth-less versions of these APIs. It's pretty low cost if it's unused. @BrennanConroy
In the meantime, if you want to make sure the WebApplicationBuilder
doesn't add the middleware for you, you can remove the IAuthorizationHandlerProvider
from the service collection after calling AddInteractiveServerComponents
. #42481 suggested adding WebApplicationOptions.DisableAutoAddAuthMiddleware
as part of this feature, but I don't think that ever happened. It also suggested adding AuthenticationOptions.DisableAutoDefaultScheme
which now exists but is still internal and backed by and AppContext switch. If there's enough public demand, we'd consider adding both.
You can also set IApplicationBuilder.Properties["__AuthorizationMiddlewareSet"] = true;
so WAB won't add it for you.