Autres scénarios de sécurité ASP.NET Core Blazor WebAssembly

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 décrit d’autres scénarios de sécurité pour les applications Blazor WebAssembly.

Attacher des jetons aux requêtes sortantes

AuthorizationMessageHandler est un DelegatingHandler utilisé pour traiter les jetons d’accès. Les jetons sont acquis à l’aide du service IAccessTokenProvider, qui est inscrit par l’infrastructure. Si un jeton ne peut pas être acquis, un AccessTokenNotAvailableException est levé. AccessTokenNotAvailableException a une méthode Redirect qui accède à AccessTokenResult.InteractiveRequestUrl en utilisant le AccessTokenResult.InteractionOptions donné pour permettre l’actualisation du jeton d’accès.

Pour plus de commodité, l’infrastructure fournit le BaseAddressAuthorizationMessageHandler préconfiguré avec l’adresse de base de l’application en tant qu’URL autorisée. Les jetons d’accès sont ajoutés uniquement lorsque l’URI de requête se trouve dans l’URI de base de l’application. Lorsque les URI de requête sortants ne se trouvent pas dans l’URI de base de l’application, utilisez une classe AuthorizationMessageHandler personnalisée (recommandée) ou configurez le AuthorizationMessageHandler.

Remarque

En plus de la configuration de l’application cliente pour l’accès à l’API serveur, l’API serveur doit également autoriser les requêtes cross-origin (CORS) lorsque le client et le serveur ne résident pas à la même adresse de base. Pour plus d’informations sur la configuration de CORS côté serveur, consultez la section Partage de ressources Cross-Origin (CORS) plus loin dans cet article.

Dans l’exemple suivant :

Dans l’exemple suivant, HttpClientFactoryServiceCollectionExtensions.AddHttpClient est une extension dans Microsoft.Extensions.Http. Ajoutez le package à une application qui ne le référence pas déjà.

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.

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient("WebAPI", 
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

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

Pour une Blazorsolution hébergée basée sur le modèle de projet Blazor WebAssembly, les URI de requête se trouvent par défaut dans l’URI de base de l’application. Par conséquent, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) est affecté au HttpClient.BaseAddress dans une application générée à partir du modèle de projet.

Le HttpClient configuré est utilisé pour effectuer des requêtes autorisées à l’aide du modèle try-catch :

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject HttpClient Http

...

protected override async Task OnInitializedAsync()
{
    try
    {
        var examples = 
            await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

        ...
    }
    catch (AccessTokenNotAvailableException exception)
    {
        exception.Redirect();
    }
}

Scénarios de requête d’authentification personnalisée

Les scénarios suivants montrent comment personnaliser les requêtes d’authentification et comment obtenir le chemin de connexion à partir des options d’authentification.

Personnaliser le processus de connexion

Gérez les paramètres supplémentaires d’une requête de connexion avec les méthodes suivantes une ou plusieurs fois sur une nouvelle instance de InteractiveRequestOptions :

Dans l’exemple de composant LoginDisplay suivant, des paramètres supplémentaires sont ajoutés à la requête de connexion :

  • prompt est défini sur login : force l’utilisateur à entrer ses informations d’identification sur cette requête, ce qui annule l’authentification unique.
  • loginHint est défini sur peter@contoso.com : pré-remplit le champ nom d’utilisateur/adresse e-mail de la page de connexion pour que l’utilisateur soit peter@contoso.com. Les applications utilisent souvent ce paramètre lors de la réauthentification, après avoir déjà extrait le nom d’utilisateur d’une connexion précédente à l’aide de la revendication preferred_username.

Shared/LoginDisplay.razor:

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        <button @onclick="BeginLogOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <button @onclick="BeginLogIn">Log in</button>
    </NotAuthorized>
</AuthorizeView>

@code{
    public void BeginLogOut()
    {
        Navigation.NavigateToLogout("authentication/logout");
    }

    public void BeginLogIn()
    {
        InteractiveRequestOptions requestOptions =
            new()
            {
                Interaction = InteractionType.SignIn,
                ReturnUrl = Navigation.Uri,
            };

        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");

        Navigation.NavigateToLogin("authentication/login", requestOptions);
    }
}

Pour plus d'informations, reportez-vous aux ressources suivantes :

Personnaliser les options avant d’obtenir un jeton de façon interactive

Si un AccessTokenNotAvailableException se produit, gérez des paramètres supplémentaires pour une nouvelle requête de jeton d’accès du fournisseur d’identité avec les méthodes suivantes une ou plusieurs fois sur une nouvelle instance de InteractiveRequestOptions :

Dans l’exemple suivant qui obtient des données JSON via l’API web, des paramètres supplémentaires sont ajoutés à la requête de redirection si un jeton d’accès n’est pas disponible (AccessTokenNotAvailableException est levée) :

  • prompt est défini sur login : force l’utilisateur à entrer ses informations d’identification sur cette requête, ce qui annule l’authentification unique.
  • loginHint est défini sur peter@contoso.com : pré-remplit le champ nom d’utilisateur/adresse e-mail de la page de connexion pour que l’utilisateur soit peter@contoso.com. Les applications utilisent souvent ce paramètre lors de la réauthentification, après avoir déjà extrait le nom d’utilisateur d’une connexion précédente à l’aide de la revendication preferred_username.
try
{
    var examples = await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

    ...
}
catch (AccessTokenNotAvailableException ex)
{
    ex.Redirect(requestOptions => {
        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");
    });
}

L’exemple précédent part du principe que :

Pour plus d'informations, reportez-vous aux ressources suivantes :

Personnaliser les options lors de l’utilisation d’un IAccessTokenProvider

Si l’obtention d’un jeton échoue lors de l’utilisation d’un IAccessTokenProvider, gérez des paramètres supplémentaires pour une nouvelle requête de jeton d’accès du fournisseur d’identité avec les méthodes suivantes une ou plusieurs fois sur une nouvelle instance de InteractiveRequestOptions :

Dans l’exemple suivant qui tente d’obtenir un jeton d’accès pour l’utilisateur, des paramètres supplémentaires sont ajoutés à la requête de connexion si la tentative d’obtention d’un jeton échoue quand TryGetToken est appelé :

  • prompt est défini sur login : force l’utilisateur à entrer ses informations d’identification sur cette requête, ce qui annule l’authentification unique.
  • loginHint est défini sur peter@contoso.com : pré-remplit le champ nom d’utilisateur/adresse e-mail de la page de connexion pour que l’utilisateur soit peter@contoso.com. Les applications utilisent souvent ce paramètre lors de la réauthentification, après avoir déjà extrait le nom d’utilisateur d’une connexion précédente à l’aide de la revendication preferred_username.
var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { ... }
    });

if (!tokenResult.TryGetToken(out var token))
{
    tokenResult.InteractionOptions.TryAddAdditionalParameter("prompt", "login");
    tokenResult.InteractionOptions.TryAddAdditionalParameter("loginHint", 
        "peter@contoso.com");

    Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl, 
        accessTokenResult.InteractionOptions);
}

L’exemple précédent part du principe que :

Pour plus d'informations, reportez-vous aux ressources suivantes :

Se déconnecter avec une URL de retour personnalisée

L’exemple suivant déconnecte l’utilisateur et le retourne au point de terminaison /goodbye :

Navigation.NavigateToLogout("authentication/logout", "goodbye");

Obtenir le chemin de connexion à partir des options d’authentification

Obtenez le chemin de connexion configuré à partir de RemoteAuthenticationOptions :

var loginPath = 
    RemoteAuthOptions.Get(Options.DefaultName).AuthenticationPaths.LogInPath;

L’exemple précédent part du principe que :

Classe AuthorizationMessageHandler personnalisée

L’aide de cette section est recommandée pour les applications clientes qui effectuent des requêtes sortantes vers des URI qui ne se trouvent pas dans l’URI de base de l’application.

Dans l’exemple suivant, une classe personnalisée étend AuthorizationMessageHandler pour une utilisation en tant que DelegatingHandler pour un HttpClient. ConfigureHandler configure ce gestionnaire pour autoriser les requêtes HTTP sortantes à l’aide d’un jeton d’accès. Le jeton d’accès est attaché uniquement si au moins l’une des URL autorisées est une base de l’URI de requête (HttpRequestMessage.RequestUri).

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, 
        NavigationManager navigation)
        : base(provider, navigation)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
            scopes: new[] { "example.read", "example.write" });
    }
}

Dans le code précédent, les étendues example.read et example.write sont des exemples génériques qui ne sont pas destinés à refléter des étendues valides pour un fournisseur particulier.

Dans le fichier Program, CustomAuthorizationMessageHandler est inscrit en tant que service temporaire et est configuré en tant que DelegatingHandler pour les instances HttpResponseMessage sortantes effectuées par un HttpClient nommé.

Dans l’exemple suivant, HttpClientFactoryServiceCollectionExtensions.AddHttpClient est une extension dans Microsoft.Extensions.Http. Ajoutez le package à une application qui ne le référence pas déjà.

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.

builder.Services.AddTransient<CustomAuthorizationMessageHandler>();

builder.Services.AddHttpClient("WebAPI",
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();

Remarque

Dans l’exemple précédent, le CustomAuthorizationMessageHandlerDelegatingHandler est inscrit en tant que service temporaire pour AddHttpMessageHandler. L’inscription temporaire est recommandée pour IHttpClientFactory, qui gère ses propres étendues de DI. Pour plus d'informations, reportez-vous aux ressources suivantes :

Pour une solution Blazor hébergée basée sur le modèle de projet Blazor WebAssembly, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) est affecté au HttpClient.BaseAddress par défaut.

Le HttpClient configuré est utilisé pour effectuer des requêtes autorisées à l’aide du modèle try-catch. Lorsque le client est créé avec CreateClient (package Microsoft.Extensions.Http), le HttpClient consiste en des instances fournies qui incluent des jetons d’accès lors de l’envoi de requêtes à l’API du serveur. Si l’URI de requête est un URI relatif, comme dans l’exemple suivant (ExampleAPIMethod), il est combiné avec le BaseAddress lorsque l’application cliente effectue la demande :

@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        try
        {
            var client = ClientFactory.CreateClient("WebAPI");

            var examples = 
                await client.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

            ...
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

Configurer AuthorizationMessageHandler

AuthorizationMessageHandler peut être configuré avec des URL autorisées, des étendues et une URL de retour à l’aide de la méthode ConfigureHandler. ConfigureHandler configure le gestionnaire pour autoriser les requêtes HTTP sortantes à l’aide d’un jeton d’accès. Le jeton d’accès est attaché uniquement si au moins l’une des URL autorisées est une base de l’URI de requête (HttpRequestMessage.RequestUri). Si l’URI de requête est un URI relatif, il est combiné avec le BaseAddress.

Dans l’exemple suivant, AuthorizationMessageHandler configure un HttpClient dans le fichier Program :

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddScoped(sp => new HttpClient(
    sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }))
    {
        BaseAddress = new Uri("https://api.contoso.com/v1.0")
    });

Dans le code précédent, les étendues example.read et example.write sont des exemples génériques qui ne sont pas destinés à refléter des étendues valides pour un fournisseur particulier.

Pour une solution Blazor hébergée basée sur le modèle de projet Blazor WebAssembly, IWebAssemblyHostEnvironment.BaseAddress est affecté par défaut à l’élément suivant :

HttpClient typé

Un client typé peut être défini pour gérer tous les problèmes d’acquisition HTTP et de jeton au sein d’une seule classe.

WeatherForecastClient.cs:

using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient
{
    private readonly HttpClient http;
    private WeatherForecast[]? forecasts;

    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient
{
    private readonly HttpClient http;
    private WeatherForecast[] forecasts;

    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}

Dans l’exemple précédent, le type WeatherForecast est une classe statique qui contient les données de prévisions météorologiques. L’espace réservé {ASSEMBLY NAME} est le nom de l’assembly de l’application (par exemple, using static BlazorSample.Data;).

Dans l’exemple suivant, HttpClientFactoryServiceCollectionExtensions.AddHttpClient est une extension dans Microsoft.Extensions.Http. Ajoutez le package à une application qui ne le référence pas déjà.

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 fichier Program :

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

Pour une solution Blazor hébergée basée sur le modèle de projet Blazor WebAssembly, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) est affecté au HttpClient.BaseAddress par défaut.

Dans un composant qui extrait les données météorologiques :

@inject WeatherForecastClient Client

...

protected override async Task OnInitializedAsync()
{
    forecasts = await Client.GetForecastAsync();
}

Configurer le gestionnaire HttpClient

Le gestionnaire peut être davantage configuré avec ConfigureHandler pour les requêtes HTTP sortantes.

Dans l’exemple suivant, HttpClientFactoryServiceCollectionExtensions.AddHttpClient est une extension dans Microsoft.Extensions.Http. Ajoutez le package à une application qui ne le référence pas déjà.

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 fichier Program :

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new [] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }));

Dans le code précédent, les étendues example.read et example.write sont des exemples génériques qui ne sont pas destinés à refléter des étendues valides pour un fournisseur particulier.

Pour une solution Blazor hébergée basée sur le modèle de projet Blazor WebAssembly, IWebAssemblyHostEnvironment.BaseAddress est affecté par défaut à l’élément suivant :

Requêtes d’API web non authentifiées ou non autorisées dans une application avec un client sécurisé par défaut

Une application qui utilise normalement une valeur par défaut HttpClient sécurisée peut également effectuer des requêtes d’API web non authentifiées ou non autorisées en configurant un HttpClient nommé.

Dans l’exemple suivant, HttpClientFactoryServiceCollectionExtensions.AddHttpClient est une extension dans Microsoft.Extensions.Http. Ajoutez le package à une application qui ne le référence pas déjà.

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 fichier Program :

builder.Services.AddHttpClient("WebAPI.NoAuthenticationClient", 
    client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"));

Pour une solution Blazor hébergée basée sur le modèle de projet Blazor WebAssembly, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) est affecté au HttpClient.BaseAddress par défaut.

L’inscription précédente s’ajoute à l’inscription par défaut HttpClient sécurisée existante.

Un composant crée le HttpClient à partir du IHttpClientFactory (package Microsoft.Extensions.Http) pour effectuer des requêtes non authentifiées ou non autorisées :

@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        var client = ClientFactory.CreateClient("WebAPI.NoAuthenticationClient");

        var examples = await client.GetFromJsonAsync<ExampleType[]>(
            "ExampleNoAuthentication");

        ...
    }
}

Remarque

Le contrôleur dans l’API de serveur, ExampleNoAuthenticationController pour l’exemple précédent, n’est pas marqué avec l’attribut [Authorize].

La décision d’utiliser un client sécurisé ou un client non sécurisé comme instance HttpClient par défaut incombe au développeur. Une façon de prendre cette décision consiste à prendre en compte le nombre de points de terminaison authentifiés ou non authentifiés que l’application contacte. Si la majorité des requêtes de l’application sont destinées à sécuriser les points de terminaison d’API, utilisez l’instance HttpClient authentifiée comme valeur par défaut. Sinon, inscrivez l’instance HttpClient non authentifiée comme valeur par défaut.

Une autre approche pour utiliser le IHttpClientFactory consiste à créer un client typé pour un accès non authentifié aux points de terminaison anonymes.

Requête de jetons d’accès supplémentaires

Les jetons d’accès peuvent être obtenus manuellement en appelant IAccessTokenProvider.RequestAccessToken. Dans l’exemple suivant, une étendue supplémentaire est requise par une application pour la valeur par défaut HttpClient. L’exemple de bibliothèque d’authentification Microsoft (MSAL) configure l’étendue avec MsalProviderOptions :

Dans le fichier Program :

builder.Services.AddMsalAuthentication(options =>
{
    ...

    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 1}");
    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 2}");
}

Les espaces réservés {CUSTOM SCOPE 1} et {CUSTOM SCOPE 2} dans l’exemple précédent sont des étendues personnalisées.

Remarque

AdditionalScopesToConsent ne peut pas provisionner les autorisations des utilisateur délégués pour Microsoft Graph via l’interface utilisateur de consentement de Microsoft Entra ID quand un utilisateur utilise d’abord une application inscrite dans Microsoft Azure. Pour plus d’informations, consultez Utiliser l’API Graph avec Blazor WebAssembly ASP.NET Core.

La méthode IAccessTokenProvider.RequestAccessToken fournit une surcharge qui permet à une application d’approvisionner un jeton d’accès avec un ensemble donné d’étendues.

Dans un composant Razor :

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider

...

var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { "{CUSTOM SCOPE 1}", "{CUSTOM SCOPE 2}" }
    });

if (tokenResult.TryGetToken(out var token))
{
    ...
}

Les espaces réservés {CUSTOM SCOPE 1} et {CUSTOM SCOPE 2} dans l’exemple précédent sont des étendues personnalisées.

AccessTokenResult.TryGetToken retourne :

  • true avec le token pour utilisation.
  • false si le jeton n’est pas récupéré.

CORS (Cross Origin Resource Sharing)

Lors de l’envoi d’informations d’identification (cookies/en-têtes d’autorisation) sur des requêtes CORS, l’en-tête Authorization doit être autorisé par la stratégie CORS.

La stratégie suivante inclut la configuration pour :

  • Origines de la requête (http://localhost:5000, https://localhost:5001).
  • N’importe quelle méthode (verbe).
  • En-têtes Content-Type et Authorization. Pour autoriser un en-tête personnalisé (par exemple, x-custom-header), répertoriez l’en-tête lors de l’appel de WithHeaders.
  • Informations d’identification définies par le code JavaScript côté client (propriété credentials définie sur include).
app.UseCors(policy => 
    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
        .AllowAnyMethod()
        .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, 
            "x-custom-header")
        .AllowCredentials());

Une solution Blazor hébergée basée sur le modèle de projet Blazor WebAssembly utilise la même adresse de base pour les applications clientes et serveur. Le HttpClient.BaseAddress de l’application cliente est défini sur un URI de builder.HostEnvironment.BaseAddress par défaut. La configuration CORS n’est pas requise dans la configuration par défaut d’une solution Blazor hébergée. Les applications clientes supplémentaires qui ne sont pas hébergées par le projet serveur et qui ne partagent pas l’adresse de base de l’application serveur nécessitent une configuration CORS dans le projet serveur.

Pour plus d’informations, consultez Activer les demandes cross-origin (CORS) dans ASP.NET Core et le composant testeur de requêtes HTTP de l’exemple d’application (Components/HTTPRequestTester.razor).

Gérer les erreurs de requête de jeton

Lorsqu’une application monopage (SPA) authentifie un utilisateur à l’aide d’OpenID Connect (OIDC), l’état d’authentification est conservé localement au sein de la SPA et dans le fournisseur (IP) Identity sous la forme d’une session cookie définie à la suite de la fourniture par l’utilisateur de ses informations d’identification.

Les jetons émis par l’IP pour l’utilisateur sont généralement valides pendant de courtes périodes, environ une heure normalement, de sorte que l’application cliente doit récupérer régulièrement de nouveaux jetons. Dans le cas contraire, l’utilisateur serait déconnecté après l’expiration des jetons accordés. Dans la plupart des cas, les clients OIDC peuvent approvisionner de nouveaux jetons sans exiger que l’utilisateur s’authentifie à nouveau grâce à l’état d’authentification ou à la « session » qui est conservée dans l’IP.

Dans certains cas, le client ne peut pas obtenir de jeton sans interaction de l’utilisateur, par exemple, lorsque, pour une raison quelconque, l’utilisateur se déconnecte explicitement de l’IP. Ce scénario se produit si un utilisateur visite https://login.microsoftonline.com et se déconnecte. Dans ces scénarios, l’application ne sait pas immédiatement que l’utilisateur s’est déconnecté. Tout jeton que le client détient peut ne plus être valide. En outre, le client ne peut pas approvisionner un nouveau jeton sans interaction de l’utilisateur après l’expiration du jeton actuel.

Ces scénarios ne sont pas spécifiques à l’authentification basée sur des jetons. Ils font partie de la nature des SPA. Une SPA utilisant des cookies ne parvient également pas à appeler une API de serveur si l’authentification cookie est supprimée.

Lorsqu’une application effectue des appels d’API auprès de ressources protégées, vous devez connaître les éléments suivants :

  • Pour approvisionner un nouveau jeton d’accès pour appeler l’API, l’utilisateur peut être amené à s’authentifier à nouveau.
  • Même si le client a un jeton qui semble être valide, l’appel au serveur peut échouer, car le jeton a été révoqué par l’utilisateur.

Lorsque l’application demande un jeton, il existe deux résultats possibles :

  • La requête réussit et l’application a un jeton valide.
  • La requête échoue et l’application doit authentifier à nouveau l’utilisateur pour obtenir un nouveau jeton.

En cas d’échec d’une requête de jeton, vous devez décider si vous souhaitez enregistrer un état actuel avant d’effectuer une redirection. Plusieurs approches existent pour enregistrer l’état avec des niveaux de complexité croissants :

  • Enregistrez l’état actuel de la page dans le stockage de session. Pendant la méthode de cycle de vie OnInitializedAsync (OnInitializedAsync), contrôlez si l’état peut être restauré avant de continuer.
  • Ajoutez un paramètre de chaîne de requête et utilisez-le comme un moyen de signaler à l’application qu’elle doit réhydrater l’état précédemment enregistré.
  • Ajoutez un paramètre de chaîne de requête avec un identificateur unique pour enregistrer des données dans le stockage de session sans risquer de collisions avec d’autres éléments.

Enregistrer l’état de l’application avant une opération d’authentification avec le stockage de session

L’exemple suivant montre comment vous :

  • Conservez l’état avant de rediriger vers la page de connexion.
  • Récupérez l’état précédent après l’authentification à l’aide d’un paramètre de chaîne de requête.
...
@using System.Text.Json
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
@inject IJSRuntime JS
@inject NavigationManager Navigation

<EditForm Model="User" OnSubmit="OnSaveAsync">
    <label>
        First Name: 
        <InputText @bind-Value="User!.Name" />
    </label>
    <label>
        Last Name: 
        <InputText @bind-Value="User!.LastName" />
    </label>
    <button type="submit">Save User</button>
</EditForm>

@code {
    public Profile User { get; set; } = new Profile();

    protected override async Task OnInitializedAsync()
    {
        var currentQuery = new Uri(Navigation.Uri).Query;

        if (currentQuery.Contains("state=resumeSavingProfile"))
        {
            var user = await JS.InvokeAsync<string>("sessionStorage.getItem",
                "resumeSavingProfile");

            if (!string.IsNullOrEmpty(user))
            {
                User = JsonSerializer.Deserialize<Profile>(user);
            }
        }
    }

    public async Task OnSaveAsync()
    {
        var http = new HttpClient();
        http.BaseAddress = new Uri(Navigation.BaseUri);

        var resumeUri = Navigation.Uri + $"?state=resumeSavingProfile";

        var tokenResult = await TokenProvider.RequestAccessToken(
            new AccessTokenRequestOptions
            {
                ReturnUrl = resumeUri
            });

        if (tokenResult.TryGetToken(out var token))
        {
            http.DefaultRequestHeaders.Add("Authorization", 
                $"Bearer {token.Value}");
            await http.PostAsJsonAsync("Save", User);
        }
        else
        {
            await JS.InvokeVoidAsync("sessionStorage.setItem", 
                "resumeSavingProfile", JsonSerializer.Serialize(User));
            Navigation.NavigateTo(tokenResult.InteractiveRequestUrl);
        }
    }

    public class Profile
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }
}

Enregistrer l’état de l’application avant une opération d’authentification avec le stockage de session et un conteneur d’état

Lors d’une opération d’authentification, il existe des cas dans lesquels vous souhaitez enregistrer l’état de l’application avant que le navigateur ne soit redirigé vers l’IP. Cela peut être le cas lorsque vous utilisez un conteneur d’état et que vous souhaitez restaurer l’état après la réussite de l’authentification. Vous pouvez utiliser un objet d’état d’authentification personnalisé pour conserver un état spécifique à l’application ou une référence à celui-ci et restaurer cet état une fois l’opération d’authentification terminée. L'exemple suivant illustre l’approche.

Une classe de conteneur d’état est créée dans l’application avec des propriétés pour conserver les valeurs d’état de l’application. Dans l’exemple suivant, le conteneur est utilisé pour conserver la valeur de compteur du composant (Counter.razor) du modèle de projet BlazorCounter par défaut. Les méthodes de sérialisation et de désérialisation du conteneur sont basées sur System.Text.Json.

using System.Text.Json;

public class StateContainer
{
    public int CounterValue { get; set; }

    public string GetStateForLocalStorage()
    {
        return JsonSerializer.Serialize(this);
    }

    public void SetStateFromLocalStorage(string locallyStoredState)
    {
        var deserializedState = 
            JsonSerializer.Deserialize<StateContainer>(locallyStoredState);

        CounterValue = deserializedState.CounterValue;
    }
}

Le composant Counter utilise le conteneur d’état pour conserver la valeur currentCount en dehors du composant :

@page "/counter"
@inject StateContainer State

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    protected override void OnInitialized()
    {
        if (State.CounterValue > 0)
        {
            currentCount = State.CounterValue;
        }
    }

    private void IncrementCount()
    {
        currentCount++;
        State.CounterValue = currentCount;
    }
}

Créez un ApplicationAuthenticationState à partir de RemoteAuthenticationState. Fournissez une propriété Id, qui sert d’identificateur pour l’état stocké localement.

ApplicationAuthenticationState.cs:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string? Id { get; set; }
}
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string Id { get; set; }
}

Le composant Authentication (Authentication.razor) enregistre et restaure l’état de l’application à l’aide du stockage de session local avec les méthodes de sérialisation et de désérialisation StateContainer, GetStateForLocalStorage et SetStateFromLocalStorage :

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

@code {
    [Parameter]
    public string? Action { get; set; }

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

@code {
    [Parameter]
    public string Action { get; set; }

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}

Cet exemple utilise l’ID Microsoft Entra (ME-ID) pour l’authentification. Dans le fichier Program :

  • Le ApplicationAuthenticationState est configuré comme type RemoteAuthenticationState Bibliothèque d’authentification Microsoft (MSAL).
  • Le conteneur d’état est inscrit dans le conteneur de service.
builder.Services.AddMsalAuthentication<ApplicationAuthenticationState>(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});

builder.Services.AddSingleton<StateContainer>();

Personnaliser les itinéraires d’application

Par défaut, la bibliothèque Microsoft.AspNetCore.Components.WebAssembly.Authentication utilise les itinéraires indiqués dans le tableau suivant pour représenter différents états d’authentification.

Route Objectif
authentication/login Déclenche une opération de connexion.
authentication/login-callback Gère le résultat de toute opération de connexion.
authentication/login-failed Affiche les messages d’erreur lorsque l’opération de connexion échoue pour une raison quelconque.
authentication/logout Déclenche une opération de déconnexion.
authentication/logout-callback Gère le résultat d’une opération de déconnexion.
authentication/logout-failed Affiche les messages d’erreur lorsque l’opération de connexion échoue pour une raison quelconque.
authentication/logged-out Indique que l’utilisateur a réussi à se déconnecter.
authentication/profile Déclenche une opération de modification du profil utilisateur.
authentication/register Déclenche une opération d’inscription d’un nouvel utilisateur.

Les itinéraires indiqués dans le tableau précédent sont configurables via RemoteAuthenticationOptions<TRemoteAuthenticationProviderOptions>.AuthenticationPaths. Lorsque vous définissez des options pour fournir des itinéraires personnalisés, confirmez que l’application dispose d’un itinéraire qui gère chaque chemin d’accès.

Dans l’exemple suivant, tous les chemins d’accès sont précédés de /security.

Composant Authentication (Authentication.razor) :

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

@code{
    [Parameter]
    public string? Action { get; set; }
}
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

@code{
    [Parameter]
    public string Action { get; set; }
}

Dans le fichier Program :

builder.Services.AddApiAuthorization(options => { 
    options.AuthenticationPaths.LogInPath = "security/login";
    options.AuthenticationPaths.LogInCallbackPath = "security/login-callback";
    options.AuthenticationPaths.LogInFailedPath = "security/login-failed";
    options.AuthenticationPaths.LogOutPath = "security/logout";
    options.AuthenticationPaths.LogOutCallbackPath = "security/logout-callback";
    options.AuthenticationPaths.LogOutFailedPath = "security/logout-failed";
    options.AuthenticationPaths.LogOutSucceededPath = "security/logged-out";
    options.AuthenticationPaths.ProfilePath = "security/profile";
    options.AuthenticationPaths.RegisterPath = "security/register";
});

Si l’exigence appelle des chemins d’accès complètement différents, définissez les itinéraires comme décrit précédemment et affichez le RemoteAuthenticatorView avec un paramètre d’action explicite :

@page "/register"

<RemoteAuthenticatorView Action="RemoteAuthenticationActions.Register" />

Vous êtes autorisé à diviser l’interface utilisateur en différentes pages si vous choisissez de le faire.

Personnaliser l’interface utilisateur d’authentification

RemoteAuthenticatorView inclut un ensemble par défaut de fragments d’interface utilisateur pour chaque état d’authentification. Chaque état peut être personnalisé en transmettant un RenderFragment personnalisé. Pour personnaliser le texte affiché pendant le processus de connexion initial, vous pouvez modifier le RemoteAuthenticatorView comme suit.

Composant Authentication (Authentication.razor) :

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string? Action { get; set; }
}
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string Action { get; set; }
}

Le RemoteAuthenticatorView a un fragment qui peut être utilisé par itinéraire d’authentification indiqué dans le tableau suivant.

Route Fragment
authentication/login <LoggingIn>
authentication/login-callback <CompletingLoggingIn>
authentication/login-failed <LogInFailed>
authentication/logout <LogOut>
authentication/logout-callback <CompletingLogOut>
authentication/logout-failed <LogOutFailed>
authentication/logged-out <LogOutSucceeded>
authentication/profile <UserProfile>
authentication/register <Registering>

Personnaliser l’utilisateur

Les utilisateurs liés à l’application peuvent être personnalisés.

Personnaliser l’utilisateur avec une revendication de charge utile

Dans l’exemple suivant, les utilisateurs authentifiés de l’application reçoivent une revendication amr pour chacune des méthodes d’authentification de l’utilisateur. La revendication amr identifie la façon dont l’objet du jeton a été authentifié dans les revendications de charge utile de la plateforme Identity Microsoft v1.0. L’exemple utilise une classe de compte d’utilisateur personnalisée basée sur RemoteUserAccount.

Créez une classe qui étend la classe RemoteUserAccount. L’exemple suivant définit la propriété AuthenticationMethod sur le tableau de valeurs de propriété amrJSON de l’utilisateur. AuthenticationMethod est renseigné automatiquement par l’infrastructure lorsque l’utilisateur est authentifié.

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[]? AuthenticationMethod { get; set; }
}
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[] AuthenticationMethod { get; set; }
}

Créez une fabrique qui étend AccountClaimsPrincipalFactory<TAccount> pour créer des revendications à partir des méthodes d’authentification de l’utilisateur stockées dans CustomUserAccount.AuthenticationMethod :

using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory 
    : AccountClaimsPrincipalFactory<CustomUserAccount>
{
    public CustomAccountFactory(NavigationManager navigation, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            if (account.AuthenticationMethod is not null)
            {
                foreach (var value in account.AuthenticationMethod)
                {
                    userIdentity.AddClaim(new Claim("amr", value));
                }
            }
        }

        return initialUser;
    }
}
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory 
    : AccountClaimsPrincipalFactory<CustomUserAccount>
{
    public CustomAccountFactory(NavigationManager navigation, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            foreach (var value in account.AuthenticationMethod)
            {
                userIdentity.AddClaim(new Claim("amr", value));
            }
        }

        return initialUser;
    }
}

Inscrivez le CustomAccountFactory pour le fournisseur d’authentification en cours d’utilisation. L’une des inscriptions suivantes est valide :

  • AddOidcAuthentication :

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddOidcAuthentication<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    
  • AddMsalAuthentication :

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddMsalAuthentication<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    
  • AddApiAuthorization :

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddApiAuthorization<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    

Groupes et rôles de sécurité ME-ID avec une classe de compte d’utilisateur personnalisée

Pour obtenir un autre exemple qui fonctionne avec des groupes de sécurité ME-ID et des rôles d’administrateur ME-ID, ainsi qu’une classe de compte d’utilisateur personnalisée, consultez ASP.NET Core Blazor WebAssembly avec les groupes et rôles ID Microsoft Entra.

Prérendu avec authentification

Le prérendu de contenu qui nécessite une authentification et une autorisation n’est actuellement pas pris en charge. Après avoir suivi l’aide de l’une des rubriques relatives à l’application de sécurité Blazor WebAssembly, suivez les instructions suivantes pour créer une application qui :

  • Effectue un prérendu des chemins pour lesquels l’autorisation n’est pas requise.
  • N’effectue pas de prérendu des chemins pour lesquels l’autorisation est requise.

Pour le fichier Program du projet Client, factorisez les inscriptions de service courantes dans une méthode distincte (par exemple, créez une méthode ConfigureCommonServices dans le projet Client). Les services courants sont ceux que le développeur inscrit pour utilisation par les projets client et serveur.

public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}

Dans le fichier Program :

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...

builder.Services.AddScoped( ... );

ConfigureCommonServices(builder.Services);

await builder.Build().RunAsync();

Dans le fichier Program du projet Server, inscrivez les services supplémentaires suivants et appelez ConfigureCommonServices :

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddRazorPages();
builder.Services.TryAddScoped<AuthenticationStateProvider, 
    ServerAuthenticationStateProvider>();

Client.Program.ConfigureCommonServices(services);

Dans la méthode Startup.ConfigureServices du projet Server, inscrivez les services supplémentaires suivants et appelez ConfigureCommonServices :

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddRazorPages();
    services.AddScoped<AuthenticationStateProvider, 
        ServerAuthenticationStateProvider>();
    services.AddScoped<SignOutSessionStateManager>();

    Client.Program.ConfigureCommonServices(services);
}

Pour plus d’informations sur le fournisseur d’authentification du serveur d’infrastructure Blazor (ServerAuthenticationStateProvider), consultez Authentification et autorisation d’ASP.NET Core Blazor.

Dans le fichier Pages/_Host.cshtml du projet Server, remplacez le Tag Helper Component (<component ... />) par ce qui suit :

<div id="app">
    @if (HttpContext.Request.Path.StartsWithSegments("/authentication"))
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssembly" />
    }
    else
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssemblyPrerendered" />
    }
</div>

Dans l'exemple précédent :

  • L’espace réservé {CLIENT APP ASSEMBLY NAME} est le nom de l’assembly de l’application cliente (par exemple, BlazorSample.Client).
  • La vérification conditionnelle pour le segment de chemin d’accès /authentication :
    • Évite le prérendu (render-mode="WebAssembly") pour les chemins d’authentification.
    • Prérendu (render-mode="WebAssemblyPrerendered") pour les chemins d’accès non approuvés par authentification.

Options pour les applications hébergées et les fournisseurs de connexion tiers

Lors de l’authentification et de l’autorisation d’une application Blazor WebAssembly hébergée auprès d’un fournisseur tiers, plusieurs options sont disponibles pour authentifier l’utilisateur. Celle que vous choisissez dépend de votre scénario.

Pour plus d’informations, consultez Rendre persistants les revendications et les jetons supplémentaires provenant de fournisseurs externes dans ASP.NET Core.

Authentifier des utilisateurs pour appeler uniquement des API tierces protégées

Authentifiez l’utilisateur avec un flux OAuth côté client par rapport au fournisseur d’API tiers :

builder.services.AddOidcAuthentication(options => { ... });

Dans ce scénario :

  • Le serveur hébergeant l’application ne joue pas de rôle.
  • Les API sur le serveur ne peuvent pas être protégées.
  • L’application peut uniquement appeler des API tierces protégées.

Authentifier des utilisateurs avec un fournisseur tiers et appeler des API protégées sur le serveur hôte et le tiers

Configurez Identity avec un fournisseur de connexion tiers. Obtenez les jetons requis pour l’accès à l’API tierce et enregistrez-les.

Lorsqu’un utilisateur se connecte, Identity collecte les jetons d’accès et d’actualisation dans le cadre du processus d’authentification. À ce stade, il existe deux approches disponibles pour effectuer des appels d’API à des API tierces.

Utiliser un jeton d’accès au serveur pour récupérer le jeton d’accès tiers

Utilisez le jeton d’accès généré sur le serveur pour récupérer le jeton d’accès tiers à partir d’un point de terminaison d’API serveur. À partir de là, utilisez le jeton d’accès tiers pour appeler des ressources d’API tierces directement à partir du Identity sur le client.

Nous déconseillons cette approche. Cette approche nécessite de traiter le jeton d’accès tiers comme s’il avait été généré pour un client public. En termes OAuth, l’application publique n’a pas de secret client, car elle ne peut pas être approuvée pour stocker les secrets en toute sécurité, et le jeton d’accès est produit pour un client confidentiel. Un client confidentiel est un client qui a une clé secrète client et est supposé être en mesure de stocker des secrets en toute sécurité.

  • Le jeton d’accès tiers peut se voir accorder des étendues supplémentaires pour effectuer des opérations sensibles en fonction du fait que le tiers a émis le jeton pour un client plus fiable.
  • De même, les jetons d’actualisation ne doivent pas être émis à un client qui n’est pas approuvé, car cela donne au client un accès illimité, sauf si d’autres restrictions sont mises en place.

Effectuer des appels d’API à partir du client vers l’API serveur afin d’appeler des API tierces

Effectuez un appel d’API à partir du client vers l’API du serveur. À partir du serveur, récupérez le jeton d’accès pour la ressource d’API tierce et émettez tout appel nécessaire.

Nous recommandons cette approche. Bien que cette approche nécessite un tronçon réseau supplémentaire via le serveur pour appeler une API tierce, elle aboutit finalement à une expérience plus sûre :

  • Le serveur peut stocker des jetons d’actualisation et s’assurer que l’application ne perd pas l’accès aux ressources tierces.
  • L’application ne peut pas divulguer de jetons d’accès du serveur qui peuvent contenir des autorisations plus sensibles.

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

La bibliothèque d’authentification et les modèles de projet Blazor utilisent des points de terminaison OpenID Connect (OIDC) v1.0. Pour utiliser un point de terminaison v2.0, configurez l’option JWT Bearer JwtBearerOptions.Authority. Dans l’exemple suivant, ME-ID est configuré pour v2.0 en ajoutant un segment v2.0 à la propriété Authority :

using Microsoft.AspNetCore.Authentication.JwtBearer;

...

builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    });

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

{
  "Local": {
    "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 directement la propriété Authority. Définissez la propriété dans JwtBearerOptions ou dans le fichier de paramètres de l’application (appsettings.json) avec la clé Authority.

La liste des revendications dans le jeton d’ID change pour les points de terminaison v2.0. La documentation Microsoft sur les changements a été mise hors service. Toutefois, des conseils d’aide sur les revendications dans un jeton d’ID sont disponibles dans la référence des revendications de jeton d’ID.

Configurer et utiliser gRPC dans les composants

Pour configurer une application Blazor WebAssembly afin qu’elle utilise l’infrastructure ASP.NET Core gRPC :

Remarque

Le prérendu est activé par défaut dans des Web Apps Blazor. Vous devez donc prendre en compte au préalable le rendu du composant à partir du serveur, puis du client. Tout état précalculé doit être acheminé vers le client afin d’être réutilisé. Pour plus d’informations, consultez Prévisualiser les composants ASP.NET Core Razor.

Remarque

Le prérendu est activé par défaut dans des applications Blazor WebAssembly hébergées. Vous devez donc prendre en compte au préalable le rendu du composant à partir du serveur, puis du client. Tout état précalculé doit être acheminé vers le client afin d’être réutilisé. Pour plus d’informations, consultez Prérendu et intégration des composants ASP.NET Core Razor.

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;

...

builder.Services.AddScoped(sp =>
{
    var baseAddressMessageHandler = 
        sp.GetRequiredService<BaseAddressAuthorizationMessageHandler>();
    baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
    var grpcWebHandler = 
        new GrpcWebHandler(GrpcWebMode.GrpcWeb, baseAddressMessageHandler);
    var channel = GrpcChannel.ForAddress(builder.HostEnvironment.BaseAddress, 
        new GrpcChannelOptions { HttpHandler = grpcWebHandler });

    return new Greeter.GreeterClient(channel);
});

Un composant de l’application cliente peut effectuer des appels gRPC à l’aide du client gRPC (Grpc.razor) :

@page "/grpc"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject Greeter.GreeterClient GreeterClient

<h1>Invoke gRPC service</h1>

<p>
    <input @bind="name" placeholder="Type your name" />
    <button @onclick="GetGreeting" class="btn btn-primary">Call gRPC service</button>
</p>

Server response: <strong>@serverResponse</strong>

@code {
    private string name = "Bert";
    private string? serverResponse;

    private async Task GetGreeting()
    {
        try
        {
            var request = new HelloRequest { Name = name };
            var reply = await GreeterClient.SayHelloAsync(request);
            serverResponse = reply.Message;
        }
        catch (Grpc.Core.RpcException ex)
            when (ex.Status.DebugException is 
                AccessTokenNotAvailableException tokenEx)
        {
            tokenEx.Redirect();
        }
    }
}

Pour utiliser la propriété Status.DebugException, utilisez la version Grpc.Net.Client 2.30.0 ou ultérieure.

Pour plus d’informations, consultez gRPC-Web dans les applications gRPC ASP.NET Core.

Remplacer l’implémentation AuthenticationService

Les sous-sections suivantes expliquent comment remplacer :

  • Toute implémentation JavaScript AuthenticationService.
  • La bibliothèque d’authentification Microsoft pour JavaScript (MSAL.js).

Remplacer toute implémentation JavaScript AuthenticationService

Créez une bibliothèque JavaScript pour gérer vos détails d’authentification personnalisée.

Warning

L’aide de cette section est un détail d’implémentation de la valeur par défaut RemoteAuthenticationService<TRemoteAuthenticationState,TAccount,TProviderOptions>. Le code TypeScript de cette section s’applique spécifiquement à ASP.NET Core dans .NET 7 et est susceptible d’être modifié sans préavis dans les prochaines versions d’ASP.NET Core.

// .NET makes calls to an AuthenticationService object in the Window.
declare global {
  interface Window { AuthenticationService: AuthenticationService }
}

export interface AuthenticationService {
  // Init is called to initialize the AuthenticationService.
  public static init(settings: UserManagerSettings & AuthorizeServiceSettings, logger: any) : Promise<void>;

  // Gets the currently authenticated user.
  public static getUser() : Promise<{[key: string] : string }>;

  // Tries to get an access token silently.
  public static getAccessToken(options: AccessTokenRequestOptions) : Promise<AccessTokenResult>;

  // Tries to sign in the user or get an access token interactively.
  public static signIn(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the sign-in process when a redirect is used.
  public static async completeSignIn(url: string) : Promise<AuthenticationResult>;

  // Signs the user out.
  public static signOut(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the signout callback when a redirect is used.
  public static async completeSignOut(url: string) : Promise<AuthenticationResult>;
}

// The rest of these interfaces match their C# definitions.

export interface AccessTokenRequestOptions {
  scopes: string[];
  returnUrl: string;
}

export interface AccessTokenResult {
  status: AccessTokenResultStatus;
  token?: AccessToken;
}

export interface AccessToken {
  value: string;
  expires: Date;
  grantedScopes: string[];
}

export enum AccessTokenResultStatus {
  Success = 'Success',
  RequiresRedirect = 'RequiresRedirect'
}

export enum AuthenticationResultStatus {
  Redirect = 'Redirect',
  Success = 'Success',
  Failure = 'Failure',
  OperationCompleted = 'OperationCompleted'
};

export interface AuthenticationResult {
  status: AuthenticationResultStatus;
  state?: unknown;
  message?: string;
}

export interface AuthenticationContext {
  state?: unknown;
  interactiveRequest: InteractiveAuthenticationRequest;
}

export interface InteractiveAuthenticationRequest {
  scopes?: string[];
  additionalRequestParameters?: { [key: string]: any };
};

Vous pouvez importer la bibliothèque en supprimant la balise d’origine <script> et en ajoutant une balise <script> qui charge la bibliothèque personnalisée. L’exemple suivant illustre le remplacement de la balise par défaut <script> par une balise qui charge une bibliothèque nommée CustomAuthenticationService.js à partir du dossier wwwroot/js.

Dans wwwroot/index.html avant le script Blazor (_framework/blazor.webassembly.js) à l’intérieur de la balise fermante </body> :

- <script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
+ <script src="js/CustomAuthenticationService.js"></script>

Pour plus d’informations, consultez AuthenticationService.tsdans le référentiel GitHub dotnet/aspnetcore.

Remarque

Les liens de documentation vers la source de référence .NET chargent généralement la branche par défaut du référentiel, qui représente le développement actuel pour la prochaine version de .NET. Pour sélectionner une balise pour une version spécifique, utilisez la liste déroulante Échanger les branches ou les balises. Pour plus d’informations, consultez Comment sélectionner une balise de version du code source ASP.NET Core (dotnet/AspNetCore.Docs #26205).

Remplacer la bibliothèque d’authentification Microsoft pour JavaScript (MSAL.js)

Si une application nécessite une version personnalisée de la bibliothèque d’authentification Microsoft pour JavaScript (MSAL.js), procédez comme suit :

  1. Vérifiez que le système dispose du dernier kit SDK .NET de développeur ou obtenez et installez le dernier SDK de développeur à partir de SDK .NET Core : Programmes d’installation et fichiers binaires. La configuration des flux NuGet internes n’est pas requise pour ce scénario.
  2. Configurez le référentiel GitHub dotnet/aspnetcore pour le développement en suivant la documentation de la page Générer ASP.NET Core à partir de la source. Dupliquez et clonez ou téléchargez une archive ZIP du dépôt GitHub dotnet/aspnetcore.
  3. Ouvrez le fichier src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json et définissez la version souhaitée de @azure/msal-browser. Pour obtenir une liste des versions publiées, visitez le site web npm @azure/msal-browser et sélectionnez l’onglet Versions.
  4. Générez le projet Authentication.Msal dans le dossier src/Components/WebAssembly/Authentication.Msal/src avec la commande yarn build dans un interpréteur de commandes.
  5. Si l’application utilise des ressources compressées (Brotli/Gzip), compressez le fichier Interop/dist/Release/AuthenticationService.js.
  6. Copiez le fichier AuthenticationService.js et les versions compressées (.br/.gz) du fichier, le cas échéant, à partir du dossier Interop/dist/Release dans le dossier de l’application publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal dans les ressources publiées de l’application.

Passer les options du fournisseur personnalisé

Définissez une classe pour passer les données à la bibliothèque JavaScript sous-jacente.

Important

La structure de la classe doit correspondre à ce que la bibliothèque attend quand JSON est sérialisé avec System.Text.Json.

L’exemple suivant illustre une classe ProviderOptions avec des attributsJsonPropertyName correspondant aux attentes d’une bibliothèque de fournisseur personnalisé hypothétique :

public class ProviderOptions
{
    public string? Authority { get; set; }
    public string? MetadataUrl { get; set; }

    [JsonPropertyName("client_id")]
    public string? ClientId { get; set; }

    public IList<string> DefaultScopes { get; } = 
        new List<string> { "openid", "profile" };

    [JsonPropertyName("redirect_uri")]
    public string? RedirectUri { get; set; }

    [JsonPropertyName("post_logout_redirect_uri")]
    public string? PostLogoutRedirectUri { get; set; }

    [JsonPropertyName("response_type")]
    public string? ResponseType { get; set; }

    [JsonPropertyName("response_mode")]
    public string? ResponseMode { get; set; }
}
public class ProviderOptions
{
    public string Authority { get; set; }
    public string MetadataUrl { get; set; }

    [JsonPropertyName("client_id")]
    public string ClientId { get; set; }

    public IList<string> DefaultScopes { get; } = 
        new List<string> { "openid", "profile" };

    [JsonPropertyName("redirect_uri")]
    public string RedirectUri { get; set; }

    [JsonPropertyName("post_logout_redirect_uri")]
    public string PostLogoutRedirectUri { get; set; }

    [JsonPropertyName("response_type")]
    public string ResponseType { get; set; }

    [JsonPropertyName("response_mode")]
    public string ResponseMode { get; set; }
}

Inscrivez les options du fournisseur dans le système DI et configurez les valeurs appropriées :

builder.Services.AddRemoteAuthentication<RemoteAuthenticationState, RemoteUserAccount,
    ProviderOptions>(options => {
        options.Authority = "...";
        options.MetadataUrl = "...";
        options.ClientId = "...";
        options.DefaultScopes = new List<string> { "openid", "profile", "myApi" };
        options.RedirectUri = "https://localhost:5001/authentication/login-callback";
        options.PostLogoutRedirectUri = "https://localhost:5001/authentication/logout-callback";
        options.ResponseType = "...";
        options.ResponseMode = "...";
    });

L’exemple précédent définit des URI de redirection avec des littéraux de chaîne standard. Les alternatives suivantes sont disponibles :

Ressources supplémentaires