Start Debugging

Lösung: Unable to find the required 'IAuthenticationService' service mit [Authorize(Policy = ...)] in Blazor

Eine Seite einer Blazor Web App mit [Authorize] wird zu einem Endpunkt, den die AuthorizationMiddleware prüft, und eine fehlgeschlagene Prüfung ruft ChallengeAsync auf, was AddAuthentication voraussetzt. Registrieren Sie ein echtes Schema oder lassen Sie Komponenten-Endpunkte bis zu AuthorizeRouteView durch.

Unable to find the required 'IAuthenticationService' service auf einer Blazor-Seite mit @attribute [Authorize(Policy = "...")] bedeutet, dass die ASP.NET Core AuthorizationMiddleware Ihre Policy für die HTTP-Anfrage ausgewertet hat, die Prüfung fehlgeschlagen ist und sie versucht hat, HttpContext.ChallengeAsync() aufzurufen, ohne dass Authentifizierungsdienste registriert sind. Die eigentliche Lösung ist builder.Services.AddAuthentication(...) mit einem Schema (meist Cookies), damit eine Challenge ein Ziel hat. Wenn Ihre App ausschließlich über einen eigenen AuthenticationStateProvider authentifiziert, registrieren Sie einen IAuthorizationMiddlewareResultHandler, der Razor-Komponenten-Endpunkte durchlässt, und überlassen Sie die Durchsetzung der Policy AuthorizeRouteView.

Alles Folgende wurde auf .NET 10.0.10 (SDK 10.0.302) und .NET 11 RC 1 (SDK 11.0.100-rc.1.26425.128) mit dem unveränderten Template dotnet new blazor -int Server reproduziert. Beide Laufzeiten lieferten in jedem Szenario identische Ergebnisse.

Der Fehler im Kontext

Der Browser erhält bei der ersten Anfrage an die geschützte Seite einen 500er. Das Log zeigt, dass die Challenge von der Autorisierungs-Middleware kommt, nicht von Blazor:

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      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)

Das typische Muster, das Leute zu Stack Overflow und zu dotnet/aspnetcore#55678 führt: Die Navigation zur Seite innerhalb der App funktioniert, aber ein Neuladen, ein Lesezeichen oder das Öffnen als erste Seite einer Sitzung stürzt ab.

Warum das passiert

Drei Bausteine von ASP.NET Core greifen ineinander und erzeugen die Exception.

  1. Eine routbare Komponente ist ein HTTP-Endpunkt. Seit .NET 8 erzeugt MapRazorComponents<App>() einen Endpunkt pro @page. RazorComponentEndpointFactory kopiert jedes Attribut des Komponententyps in die Endpunkt-Metadaten, einschließlich [Authorize] (siehe den Kommentar “All attributes defined for the type are included as metadata” im Quellcode).
  2. UseAuthorization() wird automatisch hinzugefügt. WebApplicationBuilder fügt die Autorisierungs-Middleware automatisch ein, sobald IAuthorizationHandlerProvider registriert ist, und AddAuthorizationCore() registriert ihn. Sie müssen app.UseAuthorization() nicht aufrufen, um betroffen zu sein; meine Reproduktion ruft es nie auf.
  3. Eine fehlgeschlagene Policy wird zu einer Challenge. Der Standard-AuthorizationMiddlewareResultHandler ruft context.ChallengeAsync() für einen anonymen Benutzer auf und context.ForbidAsync() für einen authentifizierten, der die Policy nicht erfüllt. Beide benötigen IAuthenticationService, den nur AddAuthentication() registriert.

Die Middleware prüft Ihre Policy also gegen HttpContext.User. Ihr eigener AuthenticationStateProvider wird auf diesem Weg überhaupt nicht gefragt, weshalb der nächste Punkt viele überrascht: Der Fehler tritt auch bei Benutzern auf, die Ihr Provider als angemeldet betrachtet. In meiner Reproduktion bekam ein Provider, der einen Principal mit der Rolle Admin zurückgibt, auf /admin trotzdem einen 500er, weil HttpContext.User anonym war.

Clientseitige Navigation innerhalb eines interaktiven Circuits löst nie eine HTTP-Anfrage aus, daher sieht die Middleware sie nie. Dort wertet stattdessen AuthorizeRouteView [Authorize] aus, und zwar mit dem AuthenticationStateProvider. Das ist die ganze Erklärung für “funktioniert, wenn ich auf den Link klicke, stürzt bei F5 ab”.

In .NET 7 Blazor Server funktionierte das, weil _Host.cshtml der einzige Endpunkt war und Komponenten nie einzeln gemappt wurden. Die Migration zu einer Blazor Web App ist genau der Moment, in dem die meisten diesem Fehler begegnen.

Minimale Reproduktion

Eine Blazor Web App, die sich gegen eine externe API authentifiziert und das Ergebnis nur über einen eigenen AuthenticationStateProvider bereitstellt, ohne ASP.NET Core Authentifizierungsschema:

// .NET 10.0.10 / .NET 11 RC 1, Program.cs
using System.Security.Claims;
using AuthRepro.Components;
using Microsoft.AspNetCore.Components.Authorization;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, ApiTokenAuthStateProvider>();
builder.Services.AddAuthorizationCore(o =>
    o.AddPolicy("Admins", p => p.RequireRole("Admin")));

var app = builder.Build();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.Run();

class ApiTokenAuthStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
}
@* .NET 10 / 11, Components/Pages/Admin.razor *@
@page "/admin"
@attribute [Authorize(Policy = "Admins")]
<h1>Admin area</h1>

Routes.razor verwendet <AuthorizeRouteView> mit einem <NotAuthorized>-Block. Ein curl gegen die laufende App ergibt:

AnfrageErgebnis
GET /200
GET /admin500, IAuthenticationService-Exception
GET /admin, Provider liefert einen Admin500, dieselbe Exception

Lösung 1: ein echtes Authentifizierungsschema registrieren (empfohlen)

Wenn sich Benutzer überhaupt bei Ihrer App anmelden, geben Sie ASP.NET Core ein Schema, das bei der HTTP-Anfrage weiß, wer sie sind. Für eine serverseitig gerenderte Blazor-App sind das fast immer Cookies:

// .NET 10 / 11, Program.cs
using Microsoft.AspNetCore.Authentication.Cookies;

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(o =>
    {
        o.LoginPath = "/login";
        o.AccessDeniedPath = "/access-denied";
    });

builder.Services.AddAuthorization(o =>
    o.AddPolicy("Admins", p => p.RequireRole("Admin")));

Damit liefert GET /admin für einen anonymen Benutzer ein 302 nach /login?ReturnUrl=%2Fadmin, statt eine Exception zu werfen. Ein angemeldeter Benutzer ohne die Rolle wird zu AccessDeniedPath geschickt.

Zwei weitere Schritte machen das korrekt, statt es nur zum Schweigen zu bringen:

Wenn Sie unsicher sind, ob Cookies oder Tokens zu Ihrer App passen, erläutert JWT vs. Cookie-Authentifizierung in ASP.NET Core die Abwägung. Für eine Blazor Web App, die ihre eigene UI ausliefert, gewinnen Cookies fast immer.

Ein Detail, das man kennen sollte: Seit .NET 7 wird ein Schema automatisch zum Standard, wenn genau eines registriert ist. AddAuthentication().AddCookie() ohne Standardschema-Argument funktionierte in meiner Reproduktion ebenfalls und leitete zum Standardpfad /Account/Login des Cookie-Handlers um. Sobald Sie ein zweites Schema hinzufügen (OpenID Connect, JWT Bearer), benennen Sie die Standards explizit, sonst tauschen Sie diesen Fehler gegen No authenticationScheme was specified, and there was no DefaultChallengeScheme found.

Lösung 2: Komponenten-Endpunkte bis zu AuthorizeRouteView durchlassen

Manche Apps haben tatsächlich keine Identität auf HTTP-Ebene: Die Blazor-Server-App ruft eine separate Web API auf, hält das Token im Circuit-Zustand und stellt den Benutzer nur über einen eigenen AuthenticationStateProvider bereit. Ein Cookie-Schema hinzuzufügen hieße dort, den Login-Ablauf neu zu bauen. Die Alternative ist, zu ändern, was die Autorisierungs-Middleware bei einer fehlgeschlagenen Policy tut, über den dokumentierten Erweiterungspunkt IAuthorizationMiddlewareResultHandler:

// .NET 10 / 11, Program.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Components.Endpoints;

builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler,
    BlazorAuthorizationMiddlewareResultHandler>();

sealed class BlazorAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
    private readonly AuthorizationMiddlewareResultHandler _default = new();

    public Task HandleAsync(RequestDelegate next, HttpContext context,
        AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
    {
        // Razor component pages: let the request through. AuthorizeRouteView
        // re-checks [Authorize] against the AuthenticationStateProvider.
        if (context.GetEndpoint()?.Metadata.GetMetadata<ComponentTypeMetadata>() is not null)
            return next(context);

        return _default.HandleAsync(next, context, policy, authorizeResult);
    }
}

ComponentTypeMetadata (öffentlich, in Microsoft.AspNetCore.Components.Endpoints) hängt an jedem Endpunkt, den MapRazorComponents erzeugt, daher gilt das Durchlassen nur für Seiten. Alles andere behält das Standardverhalten.

Gemessene Ergebnisse mit diesem Handler und ohne Aufruf von AddAuthentication():

AnfrageBenutzer laut ProviderErgebnis
GET /adminanonym200, <NotAuthorized>-Inhalt gerendert
GET /adminhat die Rolle Admin200, Seite gerendert
GET /api/secret (RequireAuthorization("Admins"))beide500, IAuthenticationService-Exception

Machen Sie sich klar, worauf Sie sich mit dieser Lösung einlassen:

“Beheben” Sie das nicht, indem Sie einen wirkungslosen Authentifizierungs-Handler registrieren, dessen Challenge nichts tut. Die Middleware bricht nach einer Challenge ab, sodass Benutzer eine leere 200-Seite ohne Erklärung bekommen, was schlimmer ist als die Exception.

Lösung 3: die Prüfung in die Komponente verlagern

Wenn nur ein Abschnitt einer Seite eingeschränkt ist, ist das Attribut das falsche Werkzeug. Entfernen Sie @attribute [Authorize(...)] und umschließen Sie das geschützte Markup:

@* .NET 10 / 11 *@
@page "/admin"

<AuthorizeView Policy="Admins">
    <Authorized><h1>Admin area</h1></Authorized>
    <NotAuthorized><p>You need the Admin role.</p></NotAuthorized>
</AuthorizeView>

Keine Endpunkt-Metadaten, keine Prüfung durch die Middleware, keine Exception: In derselben Reproduktion (weiterhin ohne AddAuthentication()) lieferte diese Seite 200 mit dem <NotAuthorized>-Inhalt für einen anonymen Benutzer und das Admin-Markup für einen Admin. Das deckt sich mit der Beobachtung des Melders in #55678, dass <AuthorizeView> auf der ersten Seite nie fehlschlug. Für das Ein- und Ausblenden von UI ist das in Ordnung, aber denken Sie daran, dass alles serverseitig Gerenderte trotzdem an Benutzer gesendet wird, die die Prüfung bestehen. Der Datenzugriff in der Komponente sollte daher ebenfalls die Autorisierung prüfen, nicht nur das Markup.

Stolperfallen und ähnliche Fehler

Eine FallbackPolicy bricht jede Seite, auch die Startseite. AddAuthorization(o => o.FallbackPolicy = ...RequireAuthenticatedUser()...) gilt für jeden Endpunkt ohne eigene Autorisierungs-Metadaten. In meiner Reproduktion ging GET / mit derselben Exception von 200 auf 500. Klassische Blazor-Server-Apps, die noch _Host.cshtml und MapBlazorHub() verwenden, treffen auf diesem Weg auf den Fehler (oder über MapBlazorHub().RequireAuthorization()), da ihre Komponenten keine einzelnen Endpunkte sind.

AddAuthorizationCore() vs. AddAuthorization() ist nicht die Ursache. Beide registrieren den Handler-Provider, der WebApplicationBuilder dazu bringt, UseAuthorization() einzufügen. Ein Wechsel zwischen ihnen ändert hier nichts.

Eigenständiges Blazor WebAssembly zeigt diesen Fehler nie. Auf dem Client gibt es keine ASP.NET Core Pipeline. Wenn eine WebAssembly-App den Benutzer nach der Anmeldung für anonym hält, ist das ein anderes Problem, behandelt in IsAuthenticated ist nach einem MSAL-Upgrade false.

Der Render-Modus spielt keine Rolle. Die fehlschlagende Anfrage ist der initiale HTTP-GET, der die Seite ausliefert, bevor irgendeine Interaktivität beginnt. Seiten mit Static SSR, Interactive Server, WebAssembly und Auto verhalten sich gleich. Wenn Render-Modi sich noch unscharf anfühlen, erklärt welcher Render-Modus meine Komponente ausführt, wo jeder einzelne läuft.

Der Benutzer aus dem AuthenticationStateProvider erreicht HttpContext.User nicht. Der Fluss geht nur in die andere Richtung: ServerAuthenticationStateProvider liest aus HttpContext. Alles, was vor dem Rendern der Komponenten läuft (Middleware, Endpunktfilter, nach Benutzer partitioniertes Rate Limiting), sieht nur, was ein Authentifizierungs-Handler dort hinterlegt hat.

Verwandte Artikel

Quellen

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Zurück