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

Blazor .NET 8 with Authorize attribute, "No Authentication Scheme" when running in Azure App Services #52326

Closed dhindrik closed 7 months ago

dhindrik commented 11 months ago

Is there an existing issue for this?

Describe the bug

We have upgraded our Blazor site to .NET 8. After we deployed it to Azure we get this exception:

InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).

In Azure we have Authentication enabled with Microsoft as provider. It worked fine with .NET 7 and it also works fine with .NET 8 if we just changes target framework. If we rewrite it in the new style, with App, Routes, MapRazorComponents etc we get the exception.

We can also reproduce it with a new app (se reproduction repo.

After a week of trying to isolate the problem we realized it is about the Authorize attribute we have in the base component. The exception is only thrown when we have the base component for a component with a page directive.

[Authorize]
public class MyComponentBase : ComponentBase
{
}
@page "/"

@inherits MyComponentBase

<h1>Hello, world!</h1>

Expected Behavior

Site should work as it does with .NET 7.

Steps To Reproduce

  1. Create a new Blazor app with .NET 8
  2. Add Authorization and Authentication in Program.cs
  3. Create a base component class
  4. Add the Authorize attribute
  5. Use the class in a component with page directive
  6. Deploy to Azure App Service
  7. Enable Authentication in Azure with Microsoft as provider

Repo: https://github.com/dhindrik/Blazor8AuthAzure

Exceptions (if any)

No response

.NET Version

8.0.100

Anything else?

No response

JohanDonne commented 10 months ago

I Had the same problem (authorizing with local Keycloak server). If I manually logged in (calling HttpContext.ChallengeAsync("oidc") and then visited the page with the Authorize, everything works fine. If I visited the page with the Authorize attribute before being authenticated, I got the same error message as OP (instead of being redirected to the Login page as configures in Routes.razor.

It turned out the DefaultChallengeScheme I configured in the AddAuthentication options was no the same string as the one I used in my AddOpenIdConnect call. When that was corrected, everything worked fine.

yablos commented 9 months ago

Same trouble here, in NET 7.0 was used startup.cs:

 services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
 app.UseRouting();
 app.UseAuthentication();
 app.UseAuthorization();
app.UseAntiforgery(); // <-- NEW in net8.0

// modified endpoints for net8.0:
 app.UseEndpoints(endpoints =>
 {
    endpoints.MapControllers();
     endpoints.MapRazorComponents<Pages.App>().AddInteractiveServerRenderMode()
     .AddAdditionalAssemblies(new System.Reflection.Assembly[] { typeof(NavBar).Assembly });
 });

Everything worked, redirect to login also.

Routes.razor net8.0 and 7.0

<CascadingAuthenticationState>
    <StartupLoadingScreen>
        <Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(NavBar).Assembly }">
            <Found Context="routeData">
                <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" >
                    <NotAuthorized>
                        **<E1W.Pages.RedirToLogin/>**
                    </NotAuthorized>
                </AuthorizeRouteView>
....

but in NET 8.0 it fails - when you enter direct url(any different page then login) into the browser and user is not authorized.

Also related-more detail to someones else:

Temporary solution - but also bug: https://github.com/dotnet/aspnetcore/issues/52317#issuecomment-1830673284

So if it is the case, please provide updated "documentation", for net 8.0 (minimalised setup):

Stefan13-13 commented 9 months ago

I have the same issue.

If have the following AuthorizationRequirement and handler to check if the logged in user has a certain claim:

public class RequireClaimPresent : IAuthorizationRequirement
{
    public RequireClaimPresent(string claimType)
    {
        ClaimType = claimType;
    }
    public string ClaimType { get; set; }
}
public class RequireClaimPresentHandler : AuthorizationHandler<RequireClaimPresent>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, RequireClaimPresent requirement)
    {
        bool hasClaim = context.User.Claims.Any(claim => claim.Type == requirement.ClaimType);
        if (hasClaim)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

I add it to my services using:

builder.Services.AddAuthorization(x => x.AddPolicy("MyClaimType", policy => policy.AddRequirements(new RequireClaimPresent("MyClaimType"))));
services.AddSingleton<IAuthorizationHandler, RequireClaimPresentHandler>();

When using it in an AutorizedView, it works:

<CascadingAuthenticationState>
    <AuthorizeView Policy="MyClaimType">
        You are authorized!
    </AuthorizeView>
</CascadingAuthenticationState>

I use this change the page depending on user rights.

However, sometimes I want non-authorized users to be unable to access the page at all, also not via a direct url. For this I attempt to use the Authorize attribute at the top of the page:

@attribute [Authorize(Policy = "MyClaimType")]

As soon as I add the attribute, I get the error specified above:

An unhandled exception occurred while processing the request.
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).
Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)

I'm using Blazor in .NET 8. Before I used Blazor in .NET 6 and there verything just worked (although many details in the program.cs have changed since then so it is hard to rule out if it due to one of these things).

If someone spots a mistake please let me know. Otherwise I hope this problem gets fixed soon.

halter73 commented 8 months ago

There appear to be a couple issues going on here. The two most recent comments from @yablos and @Stefan13-13 seem to be related #52063 which basically comes down to #46996 changing the way routing works for MapRazorComponents vs the old MapBlazorHub/MapFallbackToPage("/_Host").

Since MapRazorComponents now uses endpoint routing, authentication and authorization is first handled by the UseAuthentication and UseAuthorization middleware and any associated authentication schemes and authorization policies.

This means that if a Blazor component requires authentication, but the client is unauthenticated, the authorization middleware will ask the authentication scheme to "challenge" the client. When using cookie-based authentication schemes, this will redirect you to CookieAuthenticationOptions.LoginPath before any custom, blazor-specific AuthenticationStateProvider gets a chance to run or any AuthorizeView or AuthorizeRouteView gets a chance to render.

As long as the request is authenticated, meaning the HttpContext.User.Identity.IsAuthenticated is true and HttpContext.User.Identity.Name is not null when the authorization middleware runs, then the rest of Blazor authentication should continue as normal if you're doing server rendering since AddInteractiveServerComponents adds the ServerAuthenticationStateProvider which copies the authentication state from the HttpContext or HubContext via SetAuthenticationState.

I know that you may not want cookie auth, but cookies are the best way to transparently capture and preserve authentication state across requests even those that don't involve Blazor. You don't need to use Identity to use basic authentication cookies. If you follow the guidance at https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie, you can create custom /login endpoint that allows you to programmatically define any ClaimsPrincipal you want and configure things like refresh and expiration.

If all your endpoints really are Blazor endpoints, you're free to register a custom IAuthorizationMiddlewareResultHandler that causes the authentication middleware to skip any "challenge" that would otherwise be initiated for unauthenticated requests to endpoints that require authorization as suggested in #52063 and the "**Temporary solution" mentioned earlier, but I wouldn't recommend it in the even any non-Blazor endpoints get added at a later time not realizing the built-in security usually implemented by the authorization middleware is being bypassed. This seems like a security hole waiting to happen.

I think your best bet is to rely on standard authentication schemes and policies that work for Blazor and any other kind of ASP.NET Core endpoint.


As for the original issue, I'm not sure how an authentication scheme was ever specified without a call to .AddNegotiate() as demonstrated in https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth?view=aspnetcore-6.0&tabs=visual-studio#iisiis-express or some other call to add an authentication handler. For me, I see the "No authenticationScheme was specified" when I run dhindrik/Blazor8AuthAzure locally with IIS Express without even needing to publish to Azure.

I see it on Azure too, but it seems expected to me when no authentication handler is registered. Does Azure App Service register an authentication handler automatically with it's SiteExtensions? I thought if you tried to add an Identity provider to your App Service via the portal, it tried to authenticate every request regardless of how the app was configured in termps of the [Authorize] attribute. Is that not the case? @dhindrik Can you provide a sample of the .NET 7 application that does work?

divvjson commented 8 months ago

How can I use HttpContext.SignInAsync (Cookies Auth) in a Blazor Web App with @rendermode="InteractiveServer"? HttpContext is always null.

halter73 commented 7 months ago

You cannot call HttpContext.SignInAsync directly with @rendermode="InteractiveServer", because code could be running in the context of a SignalR WebSocket connection which has already sent response headers. Cookie sign in needs the client to make a new request in order to send its Set-Cookie header.

Fortunately, you can create a minimal endpoint accepting a form post that calls SignInAsync and redirects back to your component. The template already does something similar for logout since NavMenu.razor, which contains the logout form, can be rendered interactively. The "/Logout" endpoint below sends the Set-Cookie header via SignInAsync as part of a 302 redirect response landing the browser back at the returnUrl where it started.

https://github.com/dotnet/aspnetcore/blob/2dc2ab32eddb6ea697824d89fa6c854bfe13463f/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs#L43-L50

https://github.com/dotnet/aspnetcore/blob/2dc2ab32eddb6ea697824d89fa6c854bfe13463f/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor#L52-L58

For login, the template is configured to always render Login.razor non-interactively with this logic in App.razor, so it can use a <EditForm> rather than a plain <form> and count on the HttpContext being available as a [CascadingParameter].

But if you really want to render the login form on an interactive component, nothing is stopping you from taking the same approach the template does with minimal endpoint for logout and use it for login as well.

g-martin772 commented 5 months ago

@halter73 could you please provide an example of how you would implement a login and registration page using this approach? because I can't figure out how I can get all the form submissions, validations, and redirects to work together as they should. I am probably just incompetent but some help would be appreciated!

rwb196884 commented 3 months ago

Is this still broken?

I'm trying to use my DuendeIdentityServer to log into a Blazor app and it redirects to Identity to do the login but then ends up in an infinite loop as the page requiring authorization just starts the OIDC flow again.

iustin94 commented 3 months ago

Yes ... still same issue on .net 8

NavirBox commented 3 months ago

Anyone looking into .NET 8 Auth with Server-side Blazor should read carefully @halter73 explanation. It's helped me very much to setup a simple login.