Blazor Authorization - AuthorizeRouteView and RedirectToLogin not working.
Is there an existing issue for this?
- [X] I have searched the existing issues
Describe the bug
I've created Blazor WebApp via VS 2022 template with individual identity.
No matter what I do, <AuthorizeRouteView> and <NotAuthorized> are not working and redirect any request to /Account/Login. putting <p>not working</p> doesn't help.
Expected Behavior
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
<NotAuthorized>
<p>not authorized</p>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>
Prints "not authorized" when you click "Auth Required" in the template app
Steps To Reproduce
- Create Blazor Web App with Server Interactive render mode, and with Individual Account Authentication type.
- Replace from "
<RedirectToLogin />" to "<p>not authorized</p>" in Routes.razor (to see if your redirection is from RedirectToLogin component) - Click "Auth Required" in template app.
Exceptions (if any)
No response
.NET Version
8.0.100
Anything else?
This is a blocker since I was migrating .NET 7.0 app to .NET 8.0, but It behaves very differently.
Same here. Since RC1 I can't migrate without this issue.
@halter73 Would you be able to look into this?
Another thing I noticed Is that an authorization check (the route with [Authorize] Attribute) is done before the component lifecycle or Routing. For an example. I used to put OnInitialized and handle some logic in what we now call Routes.razor. When I refresh the page, no routing or component lifecycle logic runs. Is this where we have the bug?
.NET 7.0 No auth - Route OnInitialized = YES Req Auth- Route OnInitialized = YES
.NET 8.0 No auth - Route Oninitialized = YES Req Auth - Route OnInitialized = NO
Upon investigating all other interactivity - the same thing happens when refresh. It looks like there's something with pipeline.
it seems the new 8.0 routing adds pages with [Authorization] to route, so that they have authorization metadata and can be checked with AuthorizationMiddleware if we prerender or first render (when we have HttpContext)
The request runs through pipelines, Invoking Authentication middleware first, and if this fails (no login, so always) - doing redirection defined in middleware. But this is different, especially when we navigate with SPA and there's no Authentication middleware (obviously), and AuthenticationStateProvider is invoked instead of the middleware.
I think someone really messed this up.
edit: When I add JwtBearer authentication to the application they just throw 401 so authorization middleware it is.
@MackinnonBuck can we urgently have a look into this? this seems serious because Authorization in blazor is basically broken.
I'm experiencing the same issue as @christallire. This was working as expected in NET 7. In my case I'm using a Cookie for auth but I don't utilize the Cookie's options.LoginPath configuration. I prefer to use the <NotAuthorized> template directive.
@halter73 @MackinnonBuck @mkArtakMSFT
I did some digging, it seems like the user Claims are no longer returned from /manage/info from the .MapIdentityApi<User>().
This change broke my app, worked in RC2 not in RTM. Is this intentional? Possibly related.
IdentityApiEndpointRouteBuilderExtensions.cs, line 455:
private static async Task<InfoResponse> CreateInfoResponseAsync<TUser>(TUser user, UserManager<TUser> userManager)
where TUser : class
{
return new()
{
Email = await userManager.GetEmailAsync(user) ?? throw new NotSupportedException("Users must have an email."),
IsEmailConfirmed = await userManager.IsEmailConfirmedAsync(user),
};
}
@christallire How do you implement the AuthenticationStateProvider and do you pass your claims to it? If so, could you check whether your claims are empty or not?
@christallire How do you implement the
AuthenticationStateProviderand do you pass your claims to it? If so, could you check whether your claims are empty or not?
(edit: oh shoot sorry I thought you're asking me about my "original" implementation)
I haven't changed anything from the template project except RedirectToLogin part.
Well, I found a workaround by: define following class and register it where server initializes
public class BlazorAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
{
return next(context);
}
}
and handler:
services.AddSingleton<IAuthorizationMiddlewareResultHandler, BlazorAuthorizationMiddlewareResultHandler>();
This class will prevent redirection from the new .net 8 authorization middleware response. The problem still exists, but we can at least we can use .NET 8 new feature the way we used to in Blazor and .NET 7.
You may need to separate endpoint routing other than blazor routes since it basically renders serverside authorization (via .AddAuthentication(...).AddJwtBearer(...)) ineffective.
hope this helps, enjoy .net 8. while the team fixes the problem.
After investigation, I can understand it's by design now. But this is still a very weird behavior of navigation.
If the interactive is not ready, the app is matching Static Stuff Routes, the request route is managed by the pipeline. It will be redirected by CookieAuthentication* stuff. Only after the app is ready to be interactive, the interactive Routes component takes over the routing. If you refresh the browser manually, there is no ready interactive component.
This is just one of the problems that may arise. A worse scenario is the pages may flicker caused by this. Because the Static Response during Prerender may significantly differ from the Interactive Content. Check #51203 to see the behavior.
A deeper investigation is under #52176
I'm afraid I have to disagree that this problem can be solved with documentation.
Well, I found a workaround by: define following class and register it where server initializes
public class BlazorAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler { public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) { return next(context); } }and handler:
services.AddSingleton<IAuthorizationMiddlewareResultHandler, BlazorAuthorizationMiddlewareResultHandler>();This class will prevent redirection from the new .net 8 authorization middleware response. The problem still exists, but we can at least we can use .NET 8 new feature the way we used to in Blazor and .NET 7.
You may need to separate endpoint routing other than blazor routes since it basically renders serverside authorization (via
.AddAuthentication(...).AddJwtBearer(...)) ineffective.hope this helps, enjoy .net 8. while the team fixes the problem.
The Blazor Server template with .Net 8 and Identity worked fine out of the box for me. But with the Jwt-Bearer as authentication the symptoms where exactly the same. The 401 http response code was thrown instead, before the AuthorizeRouteView component kicked in and isn't able to redirect to login this way.
The workaround of above works like a charm. I unfortunately don't have time to dig in further why this works.
This is my program minimal api authentication code:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
if (context.Request.Cookies.TryGetValue("token", out var token))
{
context.Token = token;
}
return Task.CompletedTask;
}
};
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuerSigningKey = false,
ValidateIssuer = false,
SignatureValidator = (token, parameters) => new Microsoft.IdentityModel.JsonWebTokens.JsonWebToken(token)
};
});
Well, I found a workaround by: define following class and register it where server initializes
public class BlazorAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler { public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) { return next(context); } }and handler:
services.AddSingleton<IAuthorizationMiddlewareResultHandler, BlazorAuthorizationMiddlewareResultHandler>();This class will prevent redirection from the new .net 8 authorization middleware response. The problem still exists, but we can at least we can use .NET 8 new feature the way we used to in Blazor and .NET 7. You may need to separate endpoint routing other than blazor routes since it basically renders serverside authorization (via
.AddAuthentication(...).AddJwtBearer(...)) ineffective. hope this helps, enjoy .net 8. while the team fixes the problem.The Blazor Server template with .Net 8 and Identity worked fine out of the box for me. But with the Jwt-Bearer as authentication the symptoms where exactly the same. The 401 http response code was thrown instead, before the AuthorizeRouteView component kicked in and isn't able to redirect to login this way.
The workaround of above works like a charm. I unfortunately don't have time to dig in further why this works.
This is my program minimal api authentication code:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => { options.Events = new JwtBearerEvents { OnMessageReceived = context => { if (context.Request.Cookies.TryGetValue("token", out var token)) { context.Token = token; } return Task.CompletedTask; } }; options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, ValidateIssuerSigningKey = false, ValidateIssuer = false, SignatureValidator = (token, parameters) => new Microsoft.IdentityModel.JsonWebTokens.JsonWebToken(token) }; });
You don't need that on the client side; the same happened to me. I removed it, and it worked perfectly. It's only used in the API code.
We have a similar issue with Blazor Webassembly (hosted) and I tried applying the workaround in the Host API, but it didn't work. Anyone have any idea how to solve it in WASM?
Same problem as @dustrat reported
Running into the same issue with Blazor WASM as @dustrat and @pablofrommars
Well, I found a workaround by: define following class and register it where server initializes
public class BlazorAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler { public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) { return next(context); } }and handler:
services.AddSingleton<IAuthorizationMiddlewareResultHandler, BlazorAuthorizationMiddlewareResultHandler>();This class will prevent redirection from the new .net 8 authorization middleware response. The problem still exists, but we can at least we can use .NET 8 new feature the way we used to in Blazor and .NET 7.
You may need to separate endpoint routing other than blazor routes since it basically renders server-side authorization (via
.AddAuthentication(...).AddJwtBearer(...)) ineffective.hope this helps, enjoy .net 8. while the team fixes the problem.
Encountering the same problem and ended up using your recommended workaround, so thanks a lot for the hint. This unfortunately has some implications. The project I am working on using OIDC for logging in users, so the authorization result Challenged still needs to intercept to call for the OAuth 2.0 redirect to the identity provider. I have solved this by making this adjustment:
public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
{
if (authorizeResult.Challenged)
await _defaultHandler.HandleAsync(next, context, policy, authorizeResult);
else
await next(context);
}
However, my biggest concern is the implication which you already mentioned. This more or less disables the ASP.NET authorization layer, so if you have an API you must move that one to another project. I would like to re-use the cookie session but this is not as straight forward anymore. I will look into using a shared state for the cookie data protection and might be able to re-use the cookie when Blazor project and API are running under the same domain. Another way would be to either re-use the id_token or access_token for the API. Probably the id_token because this is the token meant to be for the application and not the underlying APIs (so a different API layer). So to say the JWT bearer.
Nevertheless—even assuming finding a good solution—this definitely does not seem optimal and might be difficult for not so experienced programmers. It would be nice if the ASP.NET middleware somehow knows when a request is meant to be routed to Blazor. I have not looked too much into it yet, but as HttpContext, AuthorizationPolicy and PolicyAuthorizationResult are available one could probably build something here.
The main thing is, it does not really feel "round", it would be nice if Blazor projects with authentication and authorization would be carved into one stone. I do not have a good solution here, but hope that I could lay out the problem a bit more in detail.
Edit: On another note, I also noticed another issue if you use Blazor SSR and WebAssembly only for interactivity. In this case, no AuthenticationStateProvider is registered, which means that you will get a runtime exception when using builder.Services.AddCascadingAuthenticationState();. It feels that a default implementation mapping the authentication state, the same way at the Blazor server project uses, would suffice as a good starting point. It feels that there are so many "manual" tweaks one has to do to get a standard setup "Blazor with OIDC" to work properly.
Thanks for reading!
Running into the same issue with Blazor WASM as @dustrat and @pablofrommars
Are you using the new MapRazorComponents<App>() with AddInteractiveWebAssemblyRenderMode(), or are you trying to keep a hosted Blazor WebAssembly app working with <component type="typeof(App)" render-mode="WebAssembly"> in a _Host.cshtml? Can someone please provide a copy of what they've tried so far to get Blazor WASM working?
Latest visual studio, starting a new blazor project, no interactivity (just SSR), the <RedirectToLogin/> never gets hit when clicking on the "Auth Required" link. When navigating to Account/Manage/Index without being authenticated it redirects me to InvalidUser which is a redirection hapenning in the IdentityUserAccessor. The _imports file has an [Authorize] attribute. Shouldn't the authorize attribute work first before the IdentityUserAccessor used inside the component? Bug or by design?
I'm currently working on a .NET 8 Blazor PWA implenting Auth0 B2C and am having the same issue. In the <AuthorizeRouteView>, the <Authorizing> component is hit. However, the <NotAuthorized> component is just straight up ignored. What's worse, it's not until I have an exception thrown by my API and bubbled up to the browser due to a lack of access token that a call to authentication/login actually does what it's supposed to. Adding `[Authorize]' to the _Imports file does absolutely nothing.
I am also following best practice and not using Cookie Authentication.
I'm not sure if I'm missing something or what the issue is. Some guidance would be appreciated.
EDIT:
So I finally figured out that I have to explicitly put the [Authorize] attribute on top of each page I want to secure. So placing an attribute in _Imports has no affect, using an AuthorizeView in my main layout just created an infinite loop of authentication requests.
Perhaps I'm not securing my entire client app correctly? I would think I should just be able to specify that my app can only be used by authorized users.
It's been 4 months, the problem is still there. The docs are wrong saying that the RedirectToLogin component is doing the redirection (https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-8.0&preserve-view=true#customize-unauthorized-content-with-the-router-component). This is not true, the component is never fired. It can be reproduced by:
- creating a fresh Blazor Web App project from the template in VS2022 (with authentication)
- putting a breakpoint in
RedirectToLogin.razorinsideOnInitialized - running the project and clicking "Auth Required" menu item in the navbar
- the breakpoint is not hit (so
<NotAuthorized>section insideRoutes.razorwas not triggered) but user is redirected to login page by some other means
Here is the Routes.razor for reference from this project:
@using IndetityAuth.Components.Account.Shared
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
Some comments here indicate this is by design, but it's really a puzzle to understand what is going on with all the auth and redirection magic. It is really disappointing such a basic security mechanism seems to be broken since Nov 2023. I am working on a custom setup with external auth provider and making blazor part work is a nightmare. I think I will resort to AuthorizeView in the MainLayout and other pages probably for now.
I have a smiliar problem where the basic authentication and redirects are working but as soon as I implement any further authorization rules on a page (role or policy based) it just infinitly loops between the redirectToLogin component and tries to authenticated the already logged-in user.
realted issue: https://github.com/dotnet/aspnetcore/issues/19855
I abandoned the AuthorizeRouteView all together since I could only get it to properly trigger the NotAuthorized portion after trying to render a page that had the Authorize attribute on it. I decided to do an AuthorizeView in the main layout with the RedirectToLogin component in the NotAuthorized portion. This seems to work as the AuthorizeRouteView should. And I guess technically since at the App.razor level you're working with the routes and not with the pages directly, routes are not what is being secured but the actual page. However, at the AuthorizeRouteView level, I would think there should be an explicit check to see if the current user has Identity.IsAuthenticated be true. If not, then you're not authorized.
None of this seems to function as I think it should. Templating the App.razor in this fashion seems like an oversite if this is not the way Blazor Wasm is intended to work. Sure I currently have a work around for the issue, but it feels wrong to handle the state of authentication from a layout.
I am having a related problem (I think): https://github.com/dotnet/aspnetcore/issues/54666
I'm fairly new to Blazor and .net as a whole and it has been a nightmare trying to figure this out. I've read all sorts of docs but it's a maze if you're not at home in this ecosystem.
I scaffolded the app using the template provided, and set up everyting in Azure AD, authentication for the API works fine when just normally navigating the app, but when the user refreshes all authorization for the api is lost.
Would you like to ask if the current problem has been solved? Thank you!
I have a hosted Blazor WASM app in Asp.Net Core. I have prerendering enabled. My problem was, that builder.Services.AddCascadingAuthenticationState(); is not the same as CascadingAuthenticationState Componente in the Routes.razor despite being described as such. The differenz is while prerendering, the GetAuthenticationStateAsync() method is not called when adding the CascadingAuthenticationState via the ServiceCollection.
Since I have some specially authentication (anonymous user are being logged in with cookies) this was an essential part of my solution.
If someone still struggles with this problem I found another workaround.
services.ConfigureApplicationCookie(opts => { opts.LoginPath = new PathString("/authorize/login"); });
With this workaround when accessing page with [Authorize] attribute it redirects to "/autorize/login" instead of "Account/Login"
Still it would be better if this was fixed on blazor framework level and AuthorizeRouteView worked like it should do
I also just wasted a couple hours on debugging my requests until i found this. I just wanted to do some experimentations with blazor, but this is a real bummer.
it was working from me below that but #19855 is good idea.
..AddCookie(IdentityConstants.ApplicationScheme, opt => opt.LoginPath = "routeUrl")