Autres scénarios de sécurité ASP.NET Core Blazor côté serveur

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Cet article explique comment configurer Blazor côté serveur pour d’autres scénarios de sécurité, notamment comment passer des jetons à une application Blazor.

Remarque

Les exemples de code de cet article utilisent les types de référence null (NRT, nullable reference types) et l'analyse statique de l'état null du compilateur .NET, qui sont pris en charge dans ASP.NET Core 6 et ses versions ultérieures. Lorsque vous ciblez ASP.NET Core 5.0 ou version antérieure, supprimez la désignation de type Null (?) des types string?, TodoItem[]?, WeatherForecast[]? et IEnumerable<GitHubBranch>? dans les exemples de l’article.

Passer des jetons à une application Blazor côté serveur

Les jetons disponibles en dehors des composants Razor d’une application Blazor côté serveur peuvent être passés aux composants selon l’approche décrite dans cette section. L’exemple de cette section se concentre sur le passage d’accès, d’actualisation et de jeton de falsification de requête (XSRF) jetons à l’application Blazor, mais l’approche est valide pour d’autres états de contexte HTTP.

Remarque

Le passage du jeton XSRF à Razor composants est utile dans les scénarios où les composants POST vers Identity ou d’autres points de terminaison nécessitant une validation. Si votre application nécessite uniquement des jetons d’accès et d’actualisation, vous pouvez supprimer le code de jeton XSRF de l’exemple suivant.

Authentifiez l’application comme vous le feriez avec une application Pages ou MVC Razor standard. Provisionnez et enregistrez les jetons dans l’authentification cookie.

Dans le fichier Program :

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);
});

Dans 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);
});

Dans 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);
});

Si vous le souhaitez, des étendues supplémentaires sont ajoutées avec options.Scope.Add("{SCOPE}");, où l’espace réservé {SCOPE} est l’étendue supplémentaire à ajouter.

Définissez un service de fournisseur de jetons délimités qui peut être utilisé dans l’application Blazor pour convertir les jetons à partir de l’injection de dépendances.

TokenProvider.cs:

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

Dans le fichier Program, ajoutez des services à :

  • IHttpClientFactory : utilisé dans une classe WeatherForecastService qui obtient des données météorologiques à partir d’une API de serveur avec un jeton d’accès.
  • TokenProvider : contient les jetons d’accès et d’actualisation.
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

Dans Startup.ConfigureServices de Startup.cs, ajoutez des services pour :

  • IHttpClientFactory : utilisé dans une classe WeatherForecastService qui obtient des données météorologiques à partir d’une API de serveur avec un jeton d’accès.
  • TokenProvider : contient les jetons d’accès et d’actualisation.
services.AddHttpClient();
services.AddScoped<TokenProvider>();

Définissez une classe à passer dans l’état initial de l’application avec les jetons d’accès et d’actualisation.

InitialApplicationState.cs:

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

Dans le fichier Pages/_Host.cshtml, créez une instance de InitialApplicationState et passez-la en tant que paramètre à l’application :

Dans le fichier Pages/_Layout.cshtml, créez une instance de InitialApplicationState et passez-la en tant que paramètre à l’application :

Dans le fichier Pages/_Host.cshtml, créez une instance de InitialApplicationState et passez-la en tant que paramètre à l’application :

@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" ... />

Dans le composant App (App.razor), convertissez le service et initialisez-le avec les données du paramètre :

@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();
    }
}

Remarque

Une alternative à l’attribution de l’état initial à TokenProvider dans l’exemple précédent consiste à copier les données dans un service délimité dans OnInitializedAsync pour une utilisation dans l’application.

Ajoutez une référence de package à l’application pour le package NuGet Microsoft.AspNet.WebApi.Client.

Remarque

Pour obtenir des conseils sur l’ajout de packages à des applications .NET, consultez les articles figurant sous Installer et gérer des packages dans Flux de travail de la consommation des packages (documentation NuGet). Vérifiez les versions du package sur NuGet.org.

Dans le service qui effectue une requête d’API sécurisée, injectez le fournisseur de jetons et récupérez le jeton pour la requête d’API :

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>();
    }
}

Pour un jeton XSRF passé à un composant, injectez le TokenProvider et ajoutez le jeton XSRF à la requête POST. L’exemple suivant ajoute le jeton à un point de terminaison de déconnexion POST. Le scénario de l’exemple suivant est que le point de terminaison de déconnexion (Areas/Identity/Pages/Account/Logout.cshtml, généré automatiquement dans l’application) ne spécifie pas de IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]), car il effectue une action en plus d’une opération de déconnexion normale qui doit être protégée. Le point de terminaison nécessite un jeton XSRF valide pour traiter correctement la demande.

Dans un composant qui présente un bouton Déconnexion aux utilisateurs autorisés :

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

Définir le schéma d’authentification

Pour une application qui utilise plusieurs intergiciels d’authentification et qui a donc plusieurs schémas d’authentification, le schéma utilisé par Blazor peut être explicitement défini dans la configuration du point de terminaison du fichier Program. L’exemple suivant définit le schéma OpenID Connect (OIDC) :

Pour une application qui utilise plusieurs intergiciels d’authentification et qui a donc plusieurs schémas d’authentification, le schéma utilisé par Blazor peut être explicitement défini dans la configuration du point de terminaison de Startup.cs. L’exemple suivant définit le schéma OpenID Connect (OIDC) :

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
    });

Pour une application qui utilise plusieurs intergiciels d’authentification et qui a donc plusieurs schémas d’authentification, le schéma utilisé par Blazor peut être explicitement défini dans la configuration du point de terminaison de Startup.Configure. L’exemple suivant définit le schéma Microsoft Entra ID :

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

Utiliser des points de terminaison OpenID Connect (OIDC) v2.0

Dans les versions de ASP.NET Core antérieures à la version 5.0, la bibliothèque d’authentification et les modèles Blazor utilisent des points de terminaison OpenID Connect (OIDC) v1.0. Pour utiliser un point de terminaison v2.0 avec des versions de ASP.NET Core antérieures à la version 5.0, configurez l’option OpenIdConnectOptions.Authority dans le OpenIdConnectOptions :

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

Vous pouvez également définir le paramètre dans le fichier de paramètres de l’application (appsettings.json) :

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

Si l’ajout d’un segment à l’autorité n’est pas approprié pour le fournisseur OIDC de l’application, par exemple avec des fournisseurs non-ME-ID, définissez la propriété Authority directement. Définissez la propriété dans OpenIdConnectOptions ou dans le fichier de paramètres de l’application avec la clé Authority.

Modifications du code

  • La liste des revendications dans le jeton d’ID change pour les points de terminaison v2.0. La documentation Microsoft sur les modifications a été mise hors service, mais de l’aide sur les revendications dans un jeton d’ID est disponible dans la référence des revendications de jeton d’ID.

  • Étant donné que les ressources sont spécifiées dans les URI d’étendue pour les points de terminaison v2.0, supprimez le paramètre de la propriété OpenIdConnectOptions.Resource dans OpenIdConnectOptions :

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

URI ID d’application

  • Lorsque vous utilisez des points de terminaison v2.0, les API définissent un App ID URI destiné à représenter un identificateur unique de l’API.
  • Toutes les étendues incluent l’URI d’ID d’application comme préfixe, et les points de terminaison v2.0 émettent des jetons d’accès avec l’URI d’ID d’application comme public.
  • Lorsque vous utilisez des points de terminaison V2.0, l’ID client configuré dans l’API serveur passe de l’ID d’application d’API (ID client) à l’URI d’ID d’application.

appsettings.json:

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

Vous trouverez l’URI d’ID d’application à utiliser dans la description d’inscription à l’application du fournisseur OIDC.

Gestionnaire de circuit pour capturer des utilisateurs des services personnalisés

Utilisez un CircuitHandler pour capturer un utilisateur à partir du AuthenticationStateProvider et définissez l’utilisateur dans un service. Si vous souhaitez mettre à jour l’utilisateur, inscrivez un rappel vers AuthenticationStateChanged et mettez en file d’attente un Task pour obtenir le nouvel utilisateur et mettre à jour le service. L'exemple suivant illustre l’approche.

Dans l’exemple suivant :

  • OnConnectionUpAsync est appelé chaque fois que le circuit se reconnecte, définissant l’utilisateur pour la durée de vie de la connexion. Seule la méthode OnConnectionUpAsync est obligatoire, sauf si vous implémentez des mises à jour via un gestionnaire des modifications de l’authentification (AuthenticationChanged dans l’exemple suivant).
  • OnCircuitOpenedAsync est appelé pour joindre le gestionnaire de l’authentification modifiée, AuthenticationChanged, pour mettre à jour l’utilisateur.
  • Le bloc catch de la tâche UpdateAuthentication n’effectue aucune action sur les exceptions, car il n’existe aucun moyen de les signaler à ce stade dans l’exécution du code. Si une exception est levée à partir de la tâche, l’exception est signalée ailleurs dans l’application.

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;
    }
}

Dans le fichier Program :

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

...

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

Dans Startup.ConfigureServices de Startup.cs :

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

...

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

Utilisez le service dans un composant pour obtenir l’utilisateur :

@inject UserService UserService

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

Pour définir l’utilisateur dans l’intergiciel pour MVC, Pages Razor et dans d’autres scénarios d’ASP.NET Core, appelez SetUser sur le UserService dans l’intergiciel personnalisé une fois l’intergiciel d’authentification exécuté ou définissez l’utilisateur avec une implémentation IClaimsTransformation. L’exemple suivant adopte l’approche d’intergiciel.

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);
    }
}

Juste avant l’appel à app.MapRazorComponents<App>() dans le fichier Program, appelez l’intergiciel :

Juste avant l’appel à app.MapBlazorHub() dans le fichier Program, appelez l’intergiciel :

Appelez l’intergiciel juste avant l’appel à app.MapBlazorHub() dans Startup.Configure ou Startup.cs :

app.UseMiddleware<UserServiceMiddleware>();

Accès AuthenticationStateProvider dans l’intergiciel de requête sortante

Le AuthenticationStateProvider d’un DelegatingHandler pour HttpClient créé avec IHttpClientFactory est accessible dans un intergiciel de requête sortant à l’aide d’un gestionnaire d’activités de circuit.

Remarque

Pour obtenir des conseils généraux sur la définition de gestionnaires pour les requêtes HTTP par HttpClient instances créées à l’aide de IHttpClientFactory dans les applications ASP.NET Core, consultez les sections suivantes de Effectuer des requêtes HTTP à l’aide d’IHttpClientFactory dans ASP.NET Core :

L’exemple suivant utilise AuthenticationStateProvider pour attacher un en-tête de nom d’utilisateur personnalisé pour les utilisateurs authentifiés aux requêtes sortantes.

Tout d’abord, implémentez la classe CircuitServicesAccessor dans la section suivante de l’article d’injection de dépendances (DI) Blazor :

Accéder aux services côté serveurBlazor à partir d’une autre étendue de DI

Utilisez le CircuitServicesAccessor pour accéder au AuthenticationStateProvider dans l’implémentation DelegatingHandler.

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);
    }
}

Dans le fichier Program, inscrivez AuthenticationStateHandler et ajoutez le gestionnaire à IHttpClientFactory qui crée des instances HttpClient :

builder.Services.AddTransient<AuthenticationStateHandler>();

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