Authentifizierung und Autorisierung in Blazor: Ein praktischer Leitfaden

· 13 Min. Lesezeit

Wenn Sie mit ASP.NET MVC oder Razor Pages gearbeitet haben, haben Sie wahrscheinlich ein mentales Modell dafür, wie die Authentifizierung funktioniert: Middleware fängt die Anfrage ab, prüft ein Cookie oder Token, füllt HttpContext.User auf, und schon kann es losgehen. Blazor ändert dieses mentale Modell auf subtile, aber wichtige Weise – und wenn Sie diese Unterschiede nicht frühzeitig verstehen, werden Sie am Ende Authentifizierungsprobleme beheben, die unmöglich erscheinen.

In diesem Beitrag möchte ich erläutern, wie Authentifizierung und Autorisierung in Blazor tatsächlich funktionieren, und dabei sowohl Server- als auch WebAssembly-Hostingmodelle abdecken. Wir werden von den Grundlagen bis hin zu benutzerdefinierten Anbietern, externem OAuth und den Fallstricken vorgehen, die selbst erfahrenen .NET-Entwicklern in die Quere kommen.

Warum die Authentifizierung in Blazor anders ist

In herkömmlichem ASP.NET ist jede Benutzerinteraktion eine HTTP-Anfrage. Der Server validiert Anmeldeinformationen, setzt ein Cookie und jede nachfolgende Anfrage trägt dieses Cookie. Die Authentifizierungspipeline ist linear und vorhersehbar.

Blazor Server arbeitet über eine dauerhafte SignalR-Verbindung. Nachdem die erste HTTP-Anfrage die Seite geladen hat, erfolgen alle nachfolgenden Interaktionen über WebSockets. Es gibt keine neue HTTP-Anfrage für jeden Tastenklick, sodass die Middleware nicht bei jeder Interaktion erneut ausgeführt wird. Der HttpContext ist während der ersten Verbindung verfügbar, sich jedoch während der gesamten Lebensdauer einer Schaltung darauf zu verlassen, ist ein Rezept für Fehler.

Blazor WebAssembly läuft vollständig im Browser. Es gibt überhaupt kein serverseitiges HttpContext. Der Authentifizierungsstatus muss von einer API abgerufen, clientseitig gespeichert und über Tokens – typischerweise JWTs – verwaltet werden. Der Server vertraut dem Client nur, soweit es um die Token-Validierung geht.

Das bedeutet, dass Blazor eine eigene Abstraktion für den Authentifizierungsstatus benötigt, die unabhängig vom Hosting-Modell funktioniert. Diese Abstraktion ist der AuthenticationStateProvider.

Authentifizierungsstatus: Die Stiftung

Das Herzstück des Authentifizierungssystems von Blazor ist AuthenticationStateProvider. Dies ist eine abstrakte Klasse, die eine einzelne kritische Methode verfügbar macht:

public abstract Task<AuthenticationState> GetAuthenticationStateAsync();

Das AuthenticationState-Objekt umschließt ein ClaimsPrincipal – dasselbe Identitätsmodell, das überall in .NET verwendet wird. Komponenten kommunizieren nicht direkt mit Cookies oder Token; Sie fragen den AuthenticationStateProvider nach dem aktuellen Stand.

Um diesen Status für Ihren gesamten Komponentenbaum verfügbar zu machen, stellt Blazor CascadingAuthenticationState bereit. Normalerweise hüllen Sie Ihren Router damit in App.razor oder Ihr Layout ein:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                                DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <p>You're not authorized to view this page.</p>
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Page not found.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Der AuthorizeRouteView erfüllt hier eine doppelte Aufgabe: Er prüft, ob der Benutzer authentifiziert und autorisiert ist, bevor er die übereinstimmende Seitenkomponente rendert, und stellt eine Fallback-Benutzeroberfläche bereit, wenn dies nicht der Fall ist.

In .NET 8 und höher mit dem einheitlichen Blazor-Modell konfigurieren Sie dies in Ihrem App.razor und das Framework verarbeitet den kaskadierenden Parameter automatisch, wenn Sie AddCascadingAuthenticationState() in Ihrer Dienstregistrierung verwenden.

ASP.NET-Identitätsintegration

Bei den meisten Projekten müssen Sie die Authentifizierung nicht von Grund auf neu erstellen. ASP.NET Identity bietet Ihnen sofort Benutzerverwaltung, Passwort-Hashing, Zwei-Faktor-Authentifizierung und Kontobestätigung.Die Einrichtung mit Blazor beginnt in Program.cs:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Password.RequireDigit = true;
        options.Password.RequiredLength = 8;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    });

Mit der Blazor-Web-App-Vorlage in .NET 8+ verwendet die gegerüstete Identitäts-Benutzeroberfläche Razor-Komponenten direkt. Sie erhalten Anmelde-, Registrierungs- und Kontoverwaltungsseiten, die sich auf natürliche Weise in den Rest Ihrer Blazor-Anwendung integrieren lassen – keine umständliche Mischung aus Razor-Seiten und Blazor-Komponenten mehr.

Der ApplicationDbContext erbt von IdentityDbContext und Sie müssen Migrationen ausführen, um die Identitätstabellen zu erstellen:

public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }
}
dotnet ef migrations add InitialIdentity
dotnet ef database update

Die AuthorizeView-Komponente

Sobald der Authentifizierungsstatus durch Ihren Komponentenbaum fließt, können Sie mit AuthorizeView die Benutzeroberfläche bedingt rendern:

<AuthorizeView>
    <Authorized>
        <p>Welcome, @context.User.Identity?.Name!</p>
        <a href="/account/manage">Manage Account</a>
        <form method="post" action="/account/logout">
            <button type="submit">Log Out</button>
        </form>
    </Authorized>
    <NotAuthorized>
        <a href="/account/login">Log In</a>
        <a href="/account/register">Register</a>
    </NotAuthorized>
</AuthorizeView>

Mit dem Parameter context in <Authorized> erhalten Sie Zugriff auf AuthenticationState, sodass Sie Ansprüche, Rollen und die Identität des Benutzers direkt in Ihrem Markup überprüfen können.

Sie können AuthorizeView auch mit Rollen und Richtlinien verwenden:

<AuthorizeView Roles="Admin,Moderator">
    <Authorized>
        <button @onclick="DeletePost">Delete Post</button>
    </Authorized>
</AuthorizeView>

<AuthorizeView Policy="CanEditArticles">
    <Authorized>
        <button @onclick="EditArticle">Edit</button>
    </Authorized>
</AuthorizeView>

Beachten Sie Folgendes: AuthorizeView ist ein Problem mit der Benutzeroberfläche. Elemente werden ausgeblendet oder angezeigt, die zugrunde liegende Logik wird jedoch nicht geschützt. Wenn jemand Ihren API-Endpunkt aufrufen oder Ihre Methode direkt aufrufen kann, umgeht er AuthorizeView vollständig. Erzwingen Sie die Autorisierung immer auch auf der Serverseite.

Das [Autorisieren]-Attribut

Um eine ganze Seite zu schützen, wenden Sie das Attribut [Authorize] an:

@page "/admin/dashboard"
@attribute [Authorize(Roles = "Admin")]

<h1>Admin Dashboard</h1>
<p>Only administrators can see this page.</p>

Wenn ein nicht authentifizierter Benutzer zu dieser Seite navigiert, wird AuthorizeRouteView aktiviert und rendert die zuvor definierte <NotAuthorized>-Vorlage. Sie können stattdessen zu einer Anmeldeseite umleiten, indem Sie den Fall NotAuthorized mit der Navigation bearbeiten:

<NotAuthorized>
    @if (!context.User.Identity?.IsAuthenticated ?? true)
    {
        <RedirectToLogin />
    }
    else
    {
        <p>You don't have permission to access this page.</p>
    }
</NotAuthorized>

Eine einfache RedirectToLogin-Komponente könnte wie folgt aussehen:

@inject NavigationManager Navigation

@code {
    protected override void OnInitialized()
    {
        var returnUrl = Uri.EscapeDataString(Navigation.Uri);
        Navigation.NavigateTo($"/account/login?returnUrl={returnUrl}", forceLoad: true);
    }
}

Der forceLoad: true ist hier wichtig – Sie möchten eine tatsächliche HTTP-Navigation, damit die serverseitige Authentifizierungs-Middleware den Anmeldefluss ordnungsgemäß verarbeiten kann.

Rollenbasierte und richtlinienbasierte Autorisierung

Rollen sind das einfachste Modell: Weisen Sie Benutzer Gruppen wie „Admin“ oder „Editor“ zu und überprüfen Sie dann die Mitgliedschaft. Aber Policen geben Ihnen viel mehr Flexibilität.

Registrieren Sie Richtlinien in Program.cs:

builder.Services.AddAuthorizationCore(options =>
{
    options.AddPolicy("CanPublish", policy =>
        policy.RequireClaim("Permission", "Publish"));

    options.AddPolicy("MinimumAge", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));

    options.AddPolicy("PremiumUser", policy =>
        policy.RequireRole("Premium")
              .RequireClaim("Subscription", "Active"));
});

Benutzerdefinierte Anforderungen erfordern einen Handler:

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }
    public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
}

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst("DateOfBirth");

        if (dateOfBirthClaim is null)
            return Task.CompletedTask;

        var dateOfBirth = DateOnly.Parse(dateOfBirthClaim.Value);
        var age = DateOnly.FromDateTime(DateTime.Today).Year - dateOfBirth.Year;

        if (age >= requirement.MinimumAge)
            context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

Registrieren Sie den Handler:

builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

In Komponenten können Sie die Autorisierung auch programmgesteuert überprüfen, wenn Sie dynamische Logik benötigen:

@inject IAuthorizationService AuthorizationService
@inject AuthenticationStateProvider AuthStateProvider

@code {
    private bool canPublish;

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        var result = await AuthorizationService.AuthorizeAsync(
            authState.User, "CanPublish");
        canPublish = result.Succeeded;
    }
}

Externe OAuth-Anbieter

Die Unterstützung von „Mit Google anmelden“ oder „Mit GitHub anmelden“ ist mit der ASP.NET-Authentifizierungs-Middleware unkompliziert. Diese werden serverseitig konfiguriert, da der OAuth-Fluss HTTP-Weiterleitungen erfordert.

builder.Services.AddAuthentication()
    .AddGoogle(options =>
    {
        options.ClientId = builder.Configuration["Auth:Google:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]!;
        options.Scope.Add("profile");
    })
    .AddMicrosoftAccount(options =>
    {
        options.ClientId = builder.Configuration["Auth:Microsoft:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:Microsoft:ClientSecret"]!;
    })
    .AddGitHub(options =>
    {
        options.ClientId = builder.Configuration["Auth:GitHub:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:GitHub:ClientSecret"]!;
    });

Für GitHub benötigen Sie das NuGet-Paket AspNet.Security.OAuth.GitHub, da es nicht in den Standard-ASP.NET-Bibliotheken enthalten ist.

Die Anmelde-Benutzeroberfläche stellt dann Links bereit, die die externe Herausforderung auslösen:

@page "/account/external-login"

<h2>Sign in with an external provider</h2>

<form method="post" action="/api/auth/external-login">
    <button type="submit" name="provider" value="Google">
        Sign in with Google
    </button>
    <button type="submit" name="provider" value="Microsoft">
        Sign in with Microsoft
    </button>
    <button type="submit" name="provider" value="GitHub">
        Sign in with GitHub
    </button>
</form>

Der API-Endpunkt löst die Herausforderung aus und verarbeitet den Rückruf:

app.MapPost("/api/auth/external-login", (string provider, HttpContext context) =>
{
    var properties = new AuthenticationProperties
    {
        RedirectUri = "/api/auth/external-callback"
    };
    return Results.Challenge(properties, [provider]);
});

Die externe Authentifizierung in Blazor erfordert immer eine ganzseitige Navigation – Sie können eine OAuth-Umleitung nicht innerhalb einer SignalR-Schaltung oder einer WebAssembly-App durchführen, ohne den Server zu durchlaufen.

Das Framework bietet AuthorizationMessageHandler zum automatischen Anhängen von Token:

builder.Services.AddHttpClient("API",
    client => client.BaseAddress = new Uri("https://api.example.com"))
    .AddHttpMessageHandler<AuthorizationMessageHandler>();

builder.Services.AddScoped(sp =>
    sp.GetRequiredService<IHttpClientFactory>().CreateClient("API"));

Für eigenständige Blazor WASM-Apps, die sich anhand ihrer eigenen API authentifizieren, implementieren Sie ein benutzerdefiniertes AuthenticationStateProvider, das das JWT analysiert:

public class JwtAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly ILocalStorageService _localStorage;
    private readonly HttpClient _httpClient;

    public JwtAuthenticationStateProvider(
        ILocalStorageService localStorage,
        HttpClient httpClient)
    {
        _localStorage = localStorage;
        _httpClient = httpClient;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = await _localStorage.GetItemAsync<string>("authToken");

        if (string.IsNullOrWhiteSpace(token))
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));

        _httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        var claims = ParseClaimsFromJwt(token);
        var identity = new ClaimsIdentity(claims, "jwt");

        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

    public void NotifyAuthStateChanged()
    {
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
    {
        var payload = jwt.Split('.')[1];

        var padded = payload.Length % 4 switch
        {
            2 => payload + "==",
            3 => payload + "=",
            _ => payload
        };

        var bytes = Convert.FromBase64String(padded);
        var json = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(bytes);

        return json?.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())) 
               ?? Enumerable.Empty<Claim>();
    }
}

Nach erfolgreicher Anmeldung speichern Sie das Token und teilen den Authentifizierungsstatus mit:

public class AuthService
{
    private readonly HttpClient _httpClient;
    private readonly ILocalStorageService _localStorage;
    private readonly JwtAuthenticationStateProvider _authStateProvider;

    public AuthService(
        HttpClient httpClient,
        ILocalStorageService localStorage,
        AuthenticationStateProvider authStateProvider)
    {
        _httpClient = httpClient;
        _localStorage = localStorage;
        _authStateProvider = (JwtAuthenticationStateProvider)authStateProvider;
    }

    public async Task<bool> LoginAsync(string email, string password)
    {
        var response = await _httpClient.PostAsJsonAsync("/api/auth/login",
            new { Email = email, Password = password });

        if (!response.IsSuccessStatusCode)
            return false;

        var result = await response.Content
            .ReadFromJsonAsync<LoginResponse>();

        await _localStorage.SetItemAsync("authToken", result!.Token);
        _authStateProvider.NotifyAuthStateChanged();

        return true;
    }

    public async Task LogoutAsync()
    {
        await _localStorage.RemoveItemAsync("authToken");
        _authStateProvider.NotifyAuthStateChanged();
    }
}

Ein Wort der Vorsicht: Durch das Speichern von JWTs in localStorage sind sie XSS-Angriffen ausgesetzt. Für Anwendungen mit höherer Sicherheit sollten Sie erwägen, Token nur im Speicher zu belassen und Aktualisierungstoken zu verwenden oder das Backend-for-Frontend-Muster (BFF) zu übernehmen, bei dem der Server Token verwaltet und der Client reine HTTP-Cookies verwendet.

Blazor Server vs. WebAssembly: Sicherheitsüberlegungen

Das Hosting-Modell verändert Ihre Sicherheitslage grundlegend.

Blazor Server speichert Ihre gesamte Komponentenlogik auf dem Server. Der Client sieht nur gerenderte HTML-Unterschiede über SignalR. Das bedeutet:

  • Sensible Logik verlässt niemals den Server
  • Sie können direkt von Komponenten aus auf Datenbanken und interne Dienste zugreifen – Der Authentifizierungsstatus stammt vom HttpContext des Servers bei der ersten Verbindung – Die Verbindung kann das Authentifizierungs-Cookie überleben – wenn das Cookie eines Benutzers abläuft, bleibt die Verbindung bestehen, bis die Verbindung getrennt wird
  • Sie sollten die Verbindungsunterbrechung ordnungsgemäß handhaben und den Authentifizierungsstatus bei erneuter Verbindung erneut überprüfen

Blazor WebAssembly läuft vollständig im Browser. Das bedeutet:

  • Ihr gesamter Komponentencode ist herunterladbar und einsehbar – Fügen Sie niemals Geheimnisse, Verbindungszeichenfolgen oder vertrauliche Geschäftslogik in WASM-Komponenten ein – Auth wird nur auf dem Client für UX erzwungen; Die eigentliche Durchsetzung muss auf Ihrer API-Ebene erfolgen
  • Die Tokenverwaltung liegt in Ihrer Verantwortung – Erwägen Sie die Verwendung des gehosteten Modells, bei dem ein Serverprojekt die Authentifizierung übernimmt und die WASM-App bereitstellt

Ein Muster, das ich für WebAssembly-Apps empfehle, besteht darin, jede Komponente so zu behandeln, als wäre sie eine „nicht vertrauenswürdige Benutzeroberfläche“ und jeder API-Endpunkt so, als würde er von einem unbekannten Client aufgerufen. Validieren Sie alles serverseitig, unabhängig davon, was der Client überprüft.

Erstellen eines benutzerdefinierten AuthenticationStateProviders

Manchmal passen die integrierten Anbieter nicht zu Ihrer Architektur. Möglicherweise integrieren Sie ein älteres Authentifizierungssystem oder Sie müssen eine Abfrage nach Authentifizierungsstatusänderungen durchführen. Hier ist ein umfassenderer benutzerdefinierter Anbieter für Blazor Server:

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IUserService _userService;

    public CustomAuthStateProvider(
        IHttpContextAccessor httpContextAccessor,
        IUserService userService)
    {
        _httpContextAccessor = httpContextAccessor;
        _userService = userService;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var httpContext = _httpContextAccessor.HttpContext;

        if (httpContext?.User?.Identity?.IsAuthenticated != true)
            return new AuthenticationState(
                new ClaimsPrincipal(new ClaimsIdentity()));

        var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

        if (userId is null)
            return new AuthenticationState(
                new ClaimsPrincipal(new ClaimsIdentity()));

        var user = await _userService.GetUserWithClaimsAsync(userId);

        if (user is null)
            return new AuthenticationState(
                new ClaimsPrincipal(new ClaimsIdentity()));

        var claims = new List<Claim>
        {
            new(ClaimTypes.NameIdentifier, user.Id),
            new(ClaimTypes.Name, user.DisplayName),
            new(ClaimTypes.Email, user.Email)
        };

        claims.AddRange(user.Roles.Select(r => new Claim(ClaimTypes.Role, r)));
        claims.AddRange(user.Permissions.Select(p => new Claim("Permission", p)));

        var identity = new ClaimsIdentity(claims, "Custom");
        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

    public void MarkUserAsAuthenticated(string userId)
    {
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public void MarkUserAsLoggedOut()
    {
        var anonymous = new ClaimsPrincipal(new ClaimsIdentity());
        var authState = Task.FromResult(new AuthenticationState(anonymous));
        NotifyAuthenticationStateChanged(authState);
    }
}

Registrieren Sie es in Program.cs:

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddScoped<CustomAuthStateProvider>();

Die wichtigste Erkenntnis hier ist NotifyAuthenticationStateChanged – der Aufruf löst die Aktualisierung des kaskadierenden Parameters aus, der alle AuthorizeView und AuthorizeRouteView in Ihrem Komponentenbaum neu auswertet. Auf diese Weise können Sie dafür sorgen, dass die Benutzeroberfläche auf Anmelde-/Abmeldeereignisse reagiert, ohne dass eine vollständige Seitenaktualisierung erforderlich ist.

Häufige Fallstricke und Lösungen

Nachdem ich in vielen Projekten mit Blazor Auth gearbeitet habe, sehe ich hier die Probleme, die am häufigsten auftreten:

1. Verwenden von HttpContext in Blazor Server-KomponentenHttpContext ist während der ersten HTTP-Anfrage verfügbar, aber während SignalR-Interaktionen ist es null oder veraltet. Fügen Sie IHttpContextAccessor nicht in Komponenten ein, die nach dem ersten Rendern ausgeführt werden.

Lösung: Erfassen Sie während der Initialisierung, was Sie von HttpContext benötigen, und speichern Sie es in einem bereichsbezogenen Dienst:

public class UserContext
{
    public string? UserId { get; set; }
    public string? AccessToken { get; set; }
}

// In a component that renders during the initial HTTP request:
@inject IHttpContextAccessor HttpContextAccessor
@inject UserContext UserContext

@code {
    protected override void OnInitialized()
    {
        var context = HttpContextAccessor.HttpContext;
        UserContext.UserId = context?.User.FindFirstValue(ClaimTypes.NameIdentifier);
        UserContext.AccessToken = context?.Request.Headers.Authorization
            .ToString().Replace("Bearer ", "");
    }
}

2. Authentifizierungsstatus wird nach der Anmeldung nicht aktualisiert

Sie rufen Ihre Anmelde-API auf, sie ist erfolgreich, aber auf der Benutzeroberfläche wird weiterhin „Anmelden“ angezeigt.

Lösung: Sie müssen NotifyAuthenticationStateChanged auf Ihrem AuthenticationStateProvider aufrufen, nachdem sich der Authentifizierungsstatus geändert hat. Das Framework erkennt nicht auf magische Weise, dass ein Token gespeichert oder ein Cookie gesetzt wurde.

3. Autorisieren-Attribut funktioniert bei Komponenten nicht

Sie fügen [Authorize] zu einer Komponente hinzu, aber nicht authentifizierte Benutzer werden dadurch nicht blockiert.

Lösung: Stellen Sie sicher, dass Sie AuthorizeRouteView anstelle von einfachem RouteView in Ihrem App.razor verwenden. Der Standard RouteView ignoriert Autorisierungsattribute vollständig.

4. Durch das Vorrendern wird der Authentifizierungsstatus unterbrochen

Beim serverseitigen Vorrendern in Blazor WebAssembly ist kein Authentifizierungstoken verfügbar. Komponenten werden als nicht authentifiziert dargestellt und wechseln dann nach dem Laden von WASM in den authentifizierten Zustand.

Lösung: Deaktivieren Sie entweder das Vorrendern für authentisierungsempfindliche Seiten mit @rendermode InteractiveWebAssembly (ohne Vorrendern) oder behandeln Sie den Ladezustand ordnungsgemäß:

<AuthorizeView>
    <Authorized>
        <p>Welcome back, @context.User.Identity?.Name</p>
    </Authorized>
    <Authorizing>
        <p>Loading...</p>
    </Authorizing>
    <NotAuthorized>
        <a href="/login">Sign in</a>
    </NotAuthorized>
</AuthorizeView>

5. Token-Ablauf in lang laufenden Schaltkreisen

Blazor-Server-Verbindungen können stundenlang aktiv bleiben. Wenn Ihr Token oder Ihre Sitzung abläuft, bleibt der Benutzer in der Benutzeroberfläche „authentifiziert“, API-Aufrufe schlagen jedoch fehl.

Lösung: Führen Sie eine regelmäßige Überprüfung durch oder verwenden Sie einen RevalidatingServerAuthenticationStateProvider:

public class RevalidatingAuthStateProvider
    : RevalidatingServerAuthenticationStateProvider
{
    private readonly IServiceScopeFactory _scopeFactory;

    public RevalidatingAuthStateProvider(
        ILoggerFactory loggerFactory,
        IServiceScopeFactory scopeFactory)
        : base(loggerFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
        await using var scope = _scopeFactory.CreateAsyncScope();
        var userManager = scope.ServiceProvider
            .GetRequiredService<UserManager<IdentityUser>>();

        var user = await userManager.GetUserAsync(authenticationState.User);
        return user is not null;
    }
}

Dadurch wird alle 30 Minuten überprüft, ob der Benutzer noch existiert (und sein Sicherheitsstempel sich nicht geändert hat).

Fazit

Authentifizierung und Autorisierung in Blazor erfordern einen Umdenken gegenüber dem traditionellen Request-Response-ASP.NET. Die AuthenticationStateProvider-Abstraktion ist der Schlüssel zum Verständnis, wie alles zusammenpasst – sobald man das verinnerlicht hat, ergibt sich der Rest von selbst.

Beginnen Sie für die meisten Anwendungen mit ASP.NET Identity und den integrierten Vorlagen. Sie kümmern sich um die schwere Arbeit der Benutzerverwaltung, des Passwort-Hashings und der Token-Generierung. Fügen Sie Richtlinien und anspruchsbasierte Autorisierung hinzu, wenn Ihre Anforderungen wachsen. Fügen Sie externe OAuth-Anbieter hinzu, wenn Ihre Benutzer sie erwarten.

Das Hosting-Modell ist wichtig: Blazor Server bietet Ihnen eine traditionellere Sicherheitslage, bei der der Code auf dem Server bleibt, während WebAssembly Sie zu einem API-First-Denken drängt, bei dem der Client vom Design her nicht vertrauenswürdig ist. Keines von beiden ist von Natur aus sicherer – sie verfügen lediglich über unterschiedliche Bedrohungsmodelle.

Für welchen Ansatz Sie sich auch entscheiden, denken Sie an die goldene Regel: Die Autorisierung in der Benutzeroberfläche dient der Benutzererfahrung, die Autorisierung auf dem Server dient der Sicherheit. Erzwingen Sie immer beides.