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.31k stars 9.97k forks source link

Blazor Authorization - AuthorizeRouteView and RedirectToLogin not working. #52063

Open christallire opened 10 months ago

christallire commented 10 months ago

Is there an existing issue for this?

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

image

Steps To Reproduce

1) Create Blazor Web App with Server Interactive render mode, and with Individual Account Authentication type. 2) Replace from "<RedirectToLogin />" to "<p>not authorized</p>" in Routes.razor (to see if your redirection is from RedirectToLogin component) 3) 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.

appsolab commented 10 months ago

Same here. Since RC1 I can't migrate without this issue.

MackinnonBuck commented 10 months ago

@halter73 Would you be able to look into this?

christallire commented 10 months ago

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.

christallire commented 10 months ago

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.

christallire commented 10 months ago

@MackinnonBuck can we urgently have a look into this? this seems serious because Authorization in blazor is basically broken.

joecuevasjr commented 10 months ago

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.

augustevn commented 10 months ago

@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),
        };
    }
augustevn commented 10 months ago

@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 commented 10 months ago

@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?

(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.

christallire commented 10 months ago

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.

Kumima commented 10 months ago

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

christallire commented 10 months ago

I'm afraid I have to disagree that this problem can be solved with documentation.

eholman commented 9 months ago

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)
    };
});
jorg3roch4 commented 9 months ago

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.

dustrat commented 8 months ago

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?

pablofrommars commented 8 months ago

Same problem as @dustrat reported

gyurisicSSPS commented 8 months ago

Running into the same issue with Blazor WASM as @dustrat and @pablofrommars

flennic commented 7 months ago

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!

halter73 commented 7 months ago

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?

flatproject commented 7 months ago

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?

jdbahnick commented 7 months ago

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.

andy250 commented 6 months ago

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:

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.

Maetness commented 6 months ago

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

jdbahnick commented 6 months ago

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.

NathanVG commented 6 months ago

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.

HuaFangYun commented 6 months ago

Would you like to ask if the current problem has been solved? Thank you!

Appli4Ever commented 4 months ago

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.

Liandrel commented 4 months ago

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

cn-ml commented 4 months ago

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.

sinoergin commented 3 months ago

it was working from me below that but #19855 is good idea.

..AddCookie(IdentityConstants.ApplicationScheme, opt => opt.LoginPath = "routeUrl")

Usergitbit commented 3 months ago

@flennic I'm also doing OIDC with IdentityServer and I don't seem to need anything more than

public class BlazorAuthorizationMiddlewareResultHandler() : IAuthorizationMiddlewareResultHandler
{
    public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
    {
        await next(context);
    }
}

In which case did you need that? User not logged in trying to access a page that requires authorization? How do you have the redirect to login provider implemented?

Also, is there any plan on making Blazor Server easier to integrate with OIDC? I'm doing some hacky stuff in order to refresh the access token and then get it updated in the cookie.

flennic commented 2 months ago

@Usergitbit When combining an API and Blazor in the same project. I now do not remember the exact setup, but the API layer middleware comes before the request gets to Blazor. So e.g. "Unauthorized" should not be blocked by the API, but should be forwarded to Blazor to show a "Hey, you do not have permission to see this page" message. But "Challanged" should respond with the correct OAuth 2.0 / OIDC flow and not forward the request to Blazor.

So the redirect to the identity provider is the default OIDC ASP.NET implementation, it is not handled by Blazor.

And regarding you last point, I agree. The integration for projects where API and Blazor are mixed should be improved. Also I do not understand why refresh token generation is not enabled (or at least implemented) by default as there actually exist Microsoft examples which can be copy and pasted, maybe with minor adjustments which cloud be put into an Action<RefreshOptions> extension method.

There are even more quirks, e.g. only OAuth 2.0 (for example for an API) where only the JWKS endpoint is used, is also not supported by default. A friendly user provides his own library for fixing that, I think it should be part of the stack the same as OIDC. But there are probably more reasons for it why decisions are made as they are, I only see it from my perspective.

Joaomfcarmo commented 2 months ago

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!

Hi, can you show me your _defaultHandler declaration, please.

flennic commented 2 months ago

@Joaomfcarmo It is a field in the IAuthorizationMiddlewareResultHandler declared like this:

private readonly AuthorizationMiddlewareResultHandler _defaultHander = new();
killyp commented 2 months ago

Is there official documentation on this yet? I am still using the IAuthorizationMiddlewareResultHandler workaround on my Blazor Webapp to get the NotAuthorized component to trigger and redirect to a landing page.

danieldespainTLS commented 2 months ago

The template code is very misleading. Looking at Routes.razor makes it look like the <RedirectToLogin /> component is where the "magic" happens - which is the way it used to be. As mentioned, that component isn't involved anymore (so why is it in the Routes template??). You can open up the RedirectToLogin.razor file and change the NavigateTo(...) path but it will still take you to Account/Login?…. You can put a breakpoint in that component, and it never gets hit.

The solution from @Liandrel about services.ConfigureApplicationCookie(…) is what finally helped me out - after wasting way too many hours.

Appli4Ever commented 2 months ago

The template code is very misleading. Looking at Routes.razor makes it look like the <RedirectToLogin /> component is where the "magic" happens - which is the way it used to be. As mentioned, that component isn't involved anymore (so why is it in the Routes template??). You can open up the RedirectToLogin.razor file and change the NavigateTo(...) path but it will still take you to Account/Login?…. You can put a breakpoint in that component, and it never gets hit.

The solution from @Liandrel about services.ConfigureApplicationCookie(…) is what finally helped me out - after wasting way too many hours.

So I know what the "problem" semes to be: Prerender

  1. Create a new Blazor Web App
  2. Select Individual Accounts
  3. Select Interactive Render Mode
  4. Change NavigateTo(...) to counter page (or anything else)
  5. Start project.
  6. You will get redirected to counter always.

Something to look out for, if you have prerender enabled: I does not use RedirectToLogin.razor when you prerender a page with @attribute [Authorize].

This is because:

app.MapRazorComponents<App>()
    .[your render modes]
    .AddAdditionalAssemblies(typeof(test.Client._Imports).Assembly); 

redirects you to the login page, befor App renders Routes.

Simple Fix:

app.MapRazorComponents<App>()
    .[your render modes]
    .AddAdditionalAssemblies(typeof(test.Client._Imports).Assembly)
    .AllowAnonymous();
Appli4Ever commented 2 months ago

To be more specific: Asp.Net Core Redirects you while prerender, not Blazor. If you call .AllowAnonymous() App.razor will prerender anonymous and Routes will execute RedirectToLogin or anything else in

<NotAuthorized>
...
 </NotAuthorized>

Use this: if you want to change the behavior of Asp.Net core redirecting ur API calls:

.AddIdentityCookies(o =>
{
    o.ApplicationCookie?.Configure(
        co =>
        {
            co.Events.OnRedirectToLogin = e =>
            {
                e.Response.StatusCode = 401;
                return Task.CompletedTask;
            };
            co.Events.OnRedirectToAccessDenied = e =>
            {
                e.Response.StatusCode = 401;
                return Task.CompletedTask;
            };
            ...
        });
});

This way, API Endpoints wont send back a redirect result when called from Blazor WASM.

Liandrel commented 2 months ago

That solution works like a charm. I didn't think about it initially, but it makes perfect sense that it was redirecting to the default login path because of the pre-render on the server. And by allowing all paths to be anonymous, it reaches App.razor and displays the NotAuthorized content.

Mecerburn commented 2 months ago

I had the same issue as described in this post https://github.com/dotnet/aspnetcore/issues/52317 But instead of doing this overwrite public class BlazorAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler { public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) { return next(context); } }

I went with making splash screen on routes rather Screenshot 2024-08-06 123804

This way the refresh can happen and after render my javascript interop that reads session for token on auth will work. Wanted to post on initial post but it was close. Hope someone finds this as an helpfull way to workaround the issue and is a better work around without messing with the rest of the auth

nemtajo commented 1 month ago

I have the same problem. I tried creating custom auth library with Cookie authentication scheme, but it seems whatever is in the Router is not rendered at all.

In all my components where AuthorizeView is used, even though all custom cookies are deleted and the user is logged out via await _httpContextAccessor.HttpContext.SignOutAsync(LingoDub.Blazor.Auth.Model.Constants.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" }); The components always render Authorized content...

It would be great if Microsoft provided example implementation of different custom sign in options (openid, jwt bearer, cookie, etc) on both WebAssembly and Server hosting models.

Mecerburn commented 1 month ago

I have the same problem. I tried creating custom auth library with Cookie authentication scheme, but it seems whatever is in the Router is not rendered at all.

In all my components where AuthorizeView is used, even though all custom cookies are deleted and the user is logged out via await _httpContextAccessor.HttpContext.SignOutAsync(LingoDub.Blazor.Auth.Model.Constants.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" }); The components always render Authorized content...

It would be great if Microsoft provided example implementation of different custom sign in options (openid, jwt bearer, cookie, etc) on both WebAssembly and Server hosting models.

If you use the browser storage then the components need to complete the rendering to be able to access the storage. That is the issue, so adding the loading as I given in the image will sort that out. When you logout are you calling the NotifyAuthenticationStateChanged so that blazor can rerender? For me calling that logs out and no more authorized views show. Only issue is using the browser storage to read and write the auth tokens or cookies, this can be worked around with the loading stuff I shared

nemtajo commented 3 weeks ago

@Mecerburn Thank you for sharing a possible solution. I think wrapping Router in an if statement, whose condition is changed only after rendering the page, disables prerendering completely for the whole webapp, and therefore might not be a suitable solution for everyone. Afterall, I want to have prerendering enabled for some pages - it is an awesome feature to have.

I discovered that my issue would be resolved with the workaround proposed above, if I didn't have additional problems in my auth library. I just want to mention the solution, in case someone else wants to implement auth library of their own. There is a very unclear behaviour of ClaimsIdentity constructor, depending on which parameters you pass in, you will get IsAuthenticated property set to always true or always false. I hope that ASP.NET Core team cleans up this confusion. For reference, this blog helped me tackle my problem: https://www.benday.com/2021/08/13/3-common-problems-with-claimsidentity-and-claimsprincipal-in-asp-net-core/