Zusätzliche serverseitige Sicherheitsszenarios für ASP.NET Core Blazor

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

Wichtig

Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.

Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

In diesem Artikel wird erläutert, wie Sie Blazor serverseitig für zusätzliche Sicherheitsszenarios konfigurieren, einschließlich der Tokenübergabe an eine Blazor-App.

Hinweis

Die Codebeispiele in diesem Artikel verwenden Nullwerte zulassende Verweistypen (Nullable Reference Types, NRTs) und die statische Analyse des NULL-Zustands des .NET-Compilers, die in ASP.NET Core in .NET 6 oder höher unterstützt werden. Entfernen Sie bei ASP.NET Core 5.0 oder früher die NULL-Typbezeichnung (?) aus den Typen string?, TodoItem[]?, WeatherForecast[]? und IEnumerable<GitHubBranch>? in den Beispielen des Artikels.

Übergeben von Token an eine serverseitige Blazor-App

Token, die außerhalb der Razor-Komponenten in einer serverseitigen Blazor-App verfügbar sind, können mit dem in diesem Abschnitt beschriebenen Ansatz an Komponenten übergeben werden. Das Beispiel in diesem Abschnitt konzentriert sich auf die Übergabe, von Zugriffstoken, Aktualisierungstoken und Anforderungsfälschungssicherheits-Token (XSRF) an die Blazor-App, aber der Ansatz ist auch für andere HTTP-Kontextzustände gültig.

Hinweis

Das Übergeben des XSRF-Tokens an Razor-Komponenten ist in Szenarien nützlich, in denen Komponenten an Identity oder andere Endpunkte POSTEN, die eine Überprüfung erfordern. Wenn Ihre App nur Zugriffs- und Aktualisierungstoken erfordert, können Sie den XSRF-Tokencode aus dem folgenden Beispiel entfernen.

Authentifizieren Sie die App genauso wie eine reguläre Razor Pages- oder MVC-App. Stellen Sie die Token für das Authentifizierungscookie bereit, und speichern Sie diese.

In der Program-Datei:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

In Startup.cs:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

In Startup.cs:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

Optional werden zusätzliche Bereiche mit options.Scope.Add("{SCOPE}"); hinzugefügt, wobei der Platzhalter {SCOPE} der zusätzliche hinzuzufügende Bereich ist.

Definieren Sie einen bereichsbezogenen Tokenanbieterdienst, der innerhalb der Blazor-App verwendet werden kann, um die Token aus der Abhängigkeitsinjektion (Dependency Injection, DI) aufzulösen.

TokenProvider.cs:

public class TokenProvider
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? XsrfToken { get; set; }
}

Fügen Sie in der Program-Datei Dienste für Folgendes hinzu:

  • IHttpClientFactory: Wird in einer WeatherForecastService-Klasse verwendet, die Wetterdaten von einer Server-API mit einem Zugriffstoken abruft.
  • TokenProvider: Enthält die Zugriffs- und Aktualisierungstoken.
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

Fügen Sie in Startup.ConfigureServices von Startup.cs Dienste für Folgendes hinzu:

  • IHttpClientFactory: Wird in einer WeatherForecastService-Klasse verwendet, die Wetterdaten von einer Server-API mit einem Zugriffstoken abruft.
  • TokenProvider: Enthält die Zugriffs- und Aktualisierungstoken.
services.AddHttpClient();
services.AddScoped<TokenProvider>();

Definieren Sie eine Klasse, um den anfänglichen App-Zustand mit den Zugriffs- und Aktualisierungstoken zu übergeben.

InitialApplicationState.cs:

public class InitialApplicationState
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? XsrfToken { get; set; }
}

Erstellen Sie in der Datei Pages/_Host.cshtml eine Instanz von InitialApplicationState, und übergeben Sie diese als Parameter an die App:

Erstellen Sie in der Datei Pages/_Layout.cshtml eine Instanz von InitialApplicationState, und übergeben Sie diese als Parameter an die App:

Erstellen Sie in der Datei Pages/_Host.cshtml eine Instanz von InitialApplicationState, und übergeben Sie diese als Parameter an die App:

@using Microsoft.AspNetCore.Authentication
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf

...

@{
    var tokens = new InitialApplicationState
    {
        AccessToken = await HttpContext.GetTokenAsync("access_token"),
        RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
        XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken
    };
}

<component ... param-InitialState="tokens" ... />

Lösen Sie in der App-Komponente (App.razor) den Dienst auf, und initialisieren Sie diesen mit den Daten aus dem Parameter:

@inject TokenProvider TokenProvider

...

@code {
    [Parameter]
    public InitialApplicationState? InitialState { get; set; }

    protected override Task OnInitializedAsync()
    {
        TokenProvider.AccessToken = InitialState?.AccessToken;
        TokenProvider.RefreshToken = InitialState?.RefreshToken;
        TokenProvider.XsrfToken = InitialState?.XsrfToken;

        return base.OnInitializedAsync();
    }
}

Hinweis

Eine Alternative zum Zuweisen des Anfangszustands zum TokenProvider im vorherigen Beispiel besteht darin, die Daten in einen bereichsbezogenen Dienst innerhalb von OnInitializedAsync für Verwendung in der App zu kopieren.

Fügen Sie der App einen Paketverweis für das NuGet-Paket Microsoft.AspNet.WebApi.Client hinzu.

Hinweis

Einen Leitfaden zum Hinzufügen von Paketen zu .NET-Apps finden Sie in Installieren und Verwalten von Paketen unter Workflow der Nutzung von Paketen (NuGet-Dokumentation). Überprüfen Sie unter NuGet.org, ob die richtige Paketversion verwendet wird.

Fügen Sie den Tokenanbieter in den Dienst ein, der eine sichere API-Anforderung stellt, und rufen Sie das Token für die API-Anforderung ab:

WeatherForecastService.cs:

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class WeatherForecastService
{
    private readonly HttpClient http;
    private readonly TokenProvider tokenProvider;

    public WeatherForecastService(IHttpClientFactory clientFactory, 
        TokenProvider tokenProvider)
    {
        http = clientFactory.CreateClient();
        this.tokenProvider = tokenProvider;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        var token = tokenProvider.AccessToken;
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://localhost:5003/WeatherForecast");
        request.Headers.Add("Authorization", $"Bearer {token}");
        var response = await http.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ?? 
            Array.Empty<WeatherForecast>();
    }
}

Fügen Sie für ein XSRF-Token, das an eine Komponente übergeben wird, den TokenProvider ein, und fügen Sie das XSRF-Token der POST-Anforderung hinzu. Im folgenden Beispiel wird das Token einem Abmeldeendpunkt- POST hinzugefügt. Das Szenario für das folgende Beispiel ist, dass der Abmeldeendpunkt (Areas/Identity/Pages/Account/Logout.cshtml, in die App eingefügt) keinen IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]) angibt, da er zusätzlich zu einem normalen Abmeldevorgang, der geschützt werden muss, eine Aktion ausführt. Der Endpunkt erfordert ein gültiges XSRF-Token, um die Anforderung erfolgreich verarbeiten zu können.

In einer Komponente, die autorisierten Benutzern eine Abmeldeschaltfläche bietet:

@inject TokenProvider TokenProvider

...

<AuthorizeView>
    <Authorized>
        <form action="/Identity/Account/Logout?returnUrl=%2F" method="post">
            <button class="nav-link btn btn-link" type="submit">Logout</button>
            <input name="__RequestVerificationToken" type="hidden" 
                value="@TokenProvider.XsrfToken">
        </form>
    </Authorized>
    <NotAuthorized>
        ...
    </NotAuthorized>
</AuthorizeView>

Festlegen des Authentifizierungsschemas

Für eine App, die mehr als eine Authentifizierungsmiddleware verwendet und daher über mehr als ein Authentifizierungsschema verfügt, kann das von Blazor verwendete Schema explizit in der Endpunktkonfiguration der Program-Datei festgelegt werden. Im folgenden Beispiel wird das OpenID Connect-Schema (OIDC) festgelegt:

Für eine App, die mehr als eine Authentifizierungsmiddleware verwendet und daher über mehr als ein Authentifizierungsschema verfügt, kann das von Blazor verwendete Schema explizit in der Endpunktkonfiguration von Startup.cs festgelegt werden. Im folgenden Beispiel wird das OpenID Connect-Schema (OIDC) festgelegt:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapRazorComponents<App>().RequireAuthorization(
    new AuthorizeAttribute
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    })
    .AddInteractiveServerRenderMode();
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute 
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    });

Für eine App, die mehr als eine Authentifizierungsmiddleware verwendet und daher über mehr als ein Authentifizierungsschema verfügt, kann das von Blazor verwendete Schema explizit in der Endpunktkonfiguration von Startup.Configure festgelegt werden. Im folgenden Beispiel wird das Microsoft Entra ID-Schema festgelegt:

endpoints.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute 
    {
        AuthenticationSchemes = AzureADDefaults.AuthenticationScheme
    });

Verwenden von OpenID Connect v2.0-Endpunkten (OIDC)

In Versionen von ASP.NET Core vor 5.0 verwenden die Authentifizierungsbibliothek sowie Blazor-Vorlagen die Version 1.0 der OpenID Connect-Endpunkte (OIDC). Konfigurieren Sie die Option OpenIdConnectOptions.Authority in OpenIdConnectOptions, um einen v2.0-Endpunkt mit Versionen von ASP.NET Core vor 5.0 zu verwenden:

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    }

Alternativ kann die Einstellung in der Datei mit den App-Einstellungen (appsettings.json) festgelegt werden:

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/common/oauth2/v2.0/",
    ...
  }
}

Wenn das Anheften eines Segments an die Autorität für den OIDC-Anbieter der App nicht geeignet ist (z. B. bei Nicht-ME-ID-Anbietern), legen Sie die Authority-Eigenschaft direkt fest. Legen Sie die Eigenschaft entweder in OpenIdConnectOptions oder in der Datei mit den App-Einstellungen mit dem Authority-Schlüssel fest.

Codeänderungen

  • Die Liste der Ansprüche im ID-Token ändert sich für Endpunkte der Version 2.0. Microsoft-Dokumentation zu den Änderungen wurde eingestellt, aber Anleitungen zu den Ansprüchen in einem ID-Token sind im ID-Token-Anspruchsverweis verfügbar.

  • Da Ressourcen in Bereichs-URIs für 2.0-Endpunkte angegeben werden, entfernen Sie die Einstellung der OpenIdConnectOptions.Resource-Eigenschaft in OpenIdConnectOptions:

    services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => 
        {
            ...
            options.Resource = "...";    // REMOVE THIS LINE
            ...
        }
    

App-ID-URI

  • Bei der Verwendung von v2.0-Endpunkten definieren APIs einen App ID URI , der einen eindeutigen Bezeichner für die API darstellen soll.
  • Alle Bereiche enthalten den App-ID-URI als Präfix, und die v2.0-Endpunkte geben Zugriffstoken mit dem App-ID-URI als Zielgruppe aus.
  • Bei Verwendung von v2.0-Endpunkten ändert sich die in der Server-API konfigurierte Client-ID von der API-Anwendungs-ID (Client-ID) in den App-ID-URI.

appsettings.json:

{
  "AzureAd": {
    ...
    "ClientId": "https://{TENANT}.onmicrosoft.com/{PROJECT NAME}"
    ...
  }
}

Sie finden den zu verwendenden App-ID-URI in der Beschreibung zur App-Registrierung des OIDC-Anbieters.

Verbindungshandler zum Erfassen von Benutzern für benutzerdefinierte Dienste

Verwenden Sie einen CircuitHandler, um einen Benutzer aus dem AuthenticationStateProvider zu erfassen und den Benutzer in einem Dienst festzulegen. Wenn Sie den Benutzer aktualisieren möchten, registrieren Sie einen Rückruf bei AuthenticationStateChanged, und stellen Sie einen Task in die Warteschlange, um den neuen Benutzer abzurufen und den Dienst zu aktualisieren. Im folgenden Beispiel wird dieser Ansatz veranschaulicht.

Im folgenden Beispiel:

  • OnConnectionUpAsync wird jedes Mal aufgerufen, wenn die Verbindung wiederhergestellt wird, wobei der Benutzer für die Lebensdauer der Verbindung festgelegt wird. Nur die OnConnectionUpAsync-Methode ist erforderlich, es sei denn, Sie implementieren Updates über einen Handler für Authentifizierungsänderungen (AuthenticationChanged im folgenden Beispiel).
  • OnCircuitOpenedAsync wird aufgerufen, um den Handler für geänderte Authentifizierung (AuthenticationChanged) anzufügen, um den Benutzer zu aktualisieren.
  • Der catch-Block des UpdateAuthentication-Tasks führt keine Aktion für Ausnahmen aus, da es keine Möglichkeit gibt, sie zu diesem Zeitpunkt bei der Codeausführung zu melden. Wenn eine Ausnahme vom Task ausgelöst wird, wird die Ausnahme an anderer Stelle in der App gemeldet.

UserService.cs:

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService
{
    private ClaimsPrincipal currentUser = new(new ClaimsIdentity());

    public ClaimsPrincipal GetUser()
    {
        return currentUser;
    }

    internal void SetUser(ClaimsPrincipal user)
    {
        if (currentUser != user)
        {
            currentUser = user;
        }
    }
}

internal sealed class UserCircuitHandler : CircuitHandler, IDisposable
{
    private readonly AuthenticationStateProvider authenticationStateProvider;
    private readonly UserService userService;

    public UserCircuitHandler(
        AuthenticationStateProvider authenticationStateProvider,
        UserService userService)
    {
        this.authenticationStateProvider = authenticationStateProvider;
        this.userService = userService;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        authenticationStateProvider.AuthenticationStateChanged += 
            AuthenticationChanged;

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    private void AuthenticationChanged(Task<AuthenticationState> task)
    {
        _ = UpdateAuthentication(task);

        async Task UpdateAuthentication(Task<AuthenticationState> task)
        {
            try
            {
                var state = await task;
                userService.SetUser(state.User);
            }
            catch
            {
            }
        }
    }

    public override async Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        var state = await authenticationStateProvider.GetAuthenticationStateAsync();
        userService.SetUser(state.User);
    }

    public void Dispose()
    {
        authenticationStateProvider.AuthenticationStateChanged -= 
            AuthenticationChanged;
    }
}
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public ClaimsPrincipal GetUser()
    {
        return currentUser;
    }

    internal void SetUser(ClaimsPrincipal user)
    {
        if (currentUser != user)
        {
            currentUser = user;
        }
    }
}

internal sealed class UserCircuitHandler : CircuitHandler, IDisposable
{
    private readonly AuthenticationStateProvider authenticationStateProvider;
    private readonly UserService userService;

    public UserCircuitHandler(
        AuthenticationStateProvider authenticationStateProvider,
        UserService userService)
    {
        this.authenticationStateProvider = authenticationStateProvider;
        this.userService = userService;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        authenticationStateProvider.AuthenticationStateChanged += 
            AuthenticationChanged;

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    private void AuthenticationChanged(Task<AuthenticationState> task)
    {
        _ = UpdateAuthentication(task);

        async Task UpdateAuthentication(Task<AuthenticationState> task)
        {
            try
            {
                var state = await task;
                userService.SetUser(state.User);
            }
            catch
            {
            }
        }
    }

    public override async Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        var state = await authenticationStateProvider.GetAuthenticationStateAsync();
        userService.SetUser(state.User);
    }

    public void Dispose()
    {
        authenticationStateProvider.AuthenticationStateChanged -= 
            AuthenticationChanged;
    }
}

In der Program-Datei:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

builder.Services.AddScoped<UserService>();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());

In Startup.ConfigureServices von Startup.cs:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

services.AddScoped<UserService>();
services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());

Verwenden Sie den Dienst in einer Komponente, um den Benutzer abzurufen:

@inject UserService UserService

<h1>Hello, @(UserService.GetUser().Identity?.Name ?? "world")!</h1>

Um Benutzer*innen in Middleware für MVC, Razor Pages und in anderen ASP.NET Core-Szenarien festzulegen, rufen Sie SetUser für UserService in benutzerdefinierter Middleware auf, wenn die Authentifizierungsmiddleware ausgeführt wird, oder legen Sie Benutzer*innen mit einer IClaimsTransformation-Implementierung fest. Im folgenden Beispiel wird der Middlewareansatz übernommen.

UserServiceMiddleware.cs:

public class UserServiceMiddleware
{
    private readonly RequestDelegate next;

    public UserServiceMiddleware(RequestDelegate next)
    {
        this.next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task InvokeAsync(HttpContext context, UserService service)
    {
        service.SetUser(context.User);
        await next(context);
    }
}

Rufen Sie unmittelbar vor dem Aufruf von app.MapRazorComponents<App>() in der Program-Datei die Middleware auf:

Rufen Sie unmittelbar vor dem Aufruf von app.MapBlazorHub() in der Program-Datei die Middleware auf:

Rufen Sie unmittelbar vor dem Aufruf von app.MapBlazorHub() in Startup.Configure von Startup.cs die Middleware auf:

app.UseMiddleware<UserServiceMiddleware>();

Zugreifen auf AuthenticationStateProvider in Middleware für ausgehende Anforderungen

Auf den Authentifizierungszustandsanbieter (AuthenticationStateProvider) eines delegierenden Handlers (DelegatingHandler) für einen HTTP-Client (HttpClient), der mit IHttpClientFactory erstellt wurde, kann in Middleware für ausgehende Anforderungen mithilfe eines Leitungsaktivitätshandlers zugegriffen werden.

Hinweis

Allgemeine Anleitungen zum Definieren von delegierenden Handlern für HTTP-Anforderungen von HttpClient-Instanzen, die mit IHttpClientFactory in ASP.NET Core-Apps erstellt wurden, finden Sie in den folgenden Abschnitten des Artikels zum Stellen von HTTP-Anforderungen mithilfe von IHttpClientFactory in ASP.NET Core:

Im folgenden Beispiel wird AuthenticationStateProvider verwendet, um einen benutzerdefinierten Benutzernamenheader für authentifizierte Benutzer*innen an ausgehende Anforderungen anzufügen.

Implementieren Sie zunächst die CircuitServicesAccessor-Klasse im folgenden Abschnitt des Artikels zur Blazor-Abhängigkeitsinjektion (Dependency Injection, DI):

Zugreifen auf serverseitige Blazor-Dienste aus einem anderen DI-Bereich

Verwenden Sie CircuitServicesAccessor, um auf AuthenticationStateProvider in der DelegatingHandler-Implementierung zuzugreifen.

AuthenticationStateHandler.cs:

public class AuthenticationStateHandler : DelegatingHandler
{
    readonly CircuitServicesAccessor circuitServicesAccessor;

    public AuthenticationStateHandler(
        CircuitServicesAccessor circuitServicesAccessor)
    {
        this.circuitServicesAccessor = circuitServicesAccessor;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var authStateProvider = circuitServicesAccessor.Services
            .GetRequiredService<AuthenticationStateProvider>();
        var authState = await authStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            request.Headers.Add("X-USER-IDENTITY-NAME", user.Identity.Name);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Registrieren Sie AuthenticationStateHandler in der Program-Datei, und fügen Sie den Handler zum IHttpClientFactory-Element hinzu, mit dem HttpClient-Instanzen erstellt werden:

builder.Services.AddTransient<AuthenticationStateHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<AuthenticationStateHandler>();