Otros escenarios de seguridad de Blazor WebAssembly en ASP.NET Core

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

En este artículo se describen escenarios de seguridad adicionales para las aplicaciones Blazor WebAssembly.

Adjuntar tokens a solicitudes salientes

AuthorizationMessageHandler es un elemento DelegatingHandler que se usa para procesar tokens de acceso. Los tokens se adquieren mediante el servicio IAccessTokenProvider, registrado por el marco de trabajo. Si no se puede obtener un token, se produce una excepción AccessTokenNotAvailableException. AccessTokenNotAvailableException tiene un método Redirect que navega a AccessTokenResult.InteractiveRequestUrl mediante el valor AccessTokenResult.InteractionOptions especificado para permitir la actualización del token de acceso.

Para mayor comodidad, el marco de trabajo proporciona el elemento BaseAddressAuthorizationMessageHandler preconfigurado con la dirección base de la aplicación como una dirección URL autorizada. Los tokens de acceso solo se agregan cuando el URI de solicitud se encuentra dentro del URI base de la aplicación. Cuando los URI de solicitud de salida no están dentro del URI base de la aplicación, use una clase AuthorizationMessageHandler personalizada (recomendado) o configure AuthorizationMessageHandler.

Nota

Además de la configuración de la aplicación cliente para el acceso a la API del servidor, la API del servidor también debe permitir solicitudes entre orígenes (CORS) cuando el cliente y el servidor no residen en la misma dirección base. Para obtener más información sobre la configuración de CORS del lado servidor, consulte la sección Uso compartido de recursos entre orígenes (CORS) más adelante en este artículo.

En el ejemplo siguiente:

En el ejemplo siguiente, HttpClientFactoryServiceCollectionExtensions.AddHttpClient es una extensión de Microsoft.Extensions.Http. Agregue el paquete a una aplicación que aún no haga referencia a él.

Nota

Para obtener instrucciones sobre cómo agregar paquetes a aplicaciones .NET, consulte los artículos de Instalación y administración de paquetes en Flujo de trabajo de consumo de paquetes (NuGet documentación). Confirme las versiones correctas del paquete en 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"));

En el caso de una soluciónBlazor hospedada y basada en la plantilla del proyecto de Blazor WebAssembly, los URI de solicitud se encuentran en el URI base de la aplicación de forma predeterminada. Por lo tanto, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) se asigna al elemento HttpClient.BaseAddress en una aplicación generada a partir de la plantilla del proyecto.

El cliente HttpClient configurado se usa para realizar solicitudes autorizadas por medio del patrón 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();
    }
}

Escenarios de solicitud de autenticación personalizados

En los escenarios siguientes se muestra cómo personalizar las solicitudes de autenticación y cómo obtener la ruta de inicio de sesión de las opciones de autenticación.

Personalización del proceso de inicio de sesión

Administre parámetros adicionales para una solicitud de inicio de sesión con los métodos siguientes una o varias veces en una nueva instancia de InteractiveRequestOptions:

En el ejemplo siguiente del componente LoginDisplay, se agregan parámetros adicionales a la solicitud de inicio de sesión:

  • prompt se establece en login: obliga al usuario a escribir sus credenciales en esa solicitud, negando el inicio de sesión único.
  • loginHint se establece en peter@contoso.com: rellena previamente el campo de nombre de usuario o dirección de correo electrónico de la página de inicio de sesión del usuario en peter@contoso.com. Las aplicaciones suelen utilizar este parámetro durante la reautenticación, dado que ya han extraído el nombre de usuario de un inicio de sesión anterior mediante la notificación 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);
    }
}

Para obtener más información, consulte los siguientes recursos:

Personalización de opciones antes de obtener un token de forma interactiva

Si se produce una excepción AccessTokenNotAvailableException, administre parámetros adicionales para una nueva solicitud de token de acceso del proveedor de identidades con los métodos siguientes una o varias veces en una nueva instancia de InteractiveRequestOptions:

En el ejemplo siguiente que obtiene datos JSON a través de la API web, se agregan parámetros adicionales a la solicitud de redireccionamiento si no hay disponible un token de acceso (se produce la excepción AccessTokenNotAvailableException):

  • prompt se establece en login: obliga al usuario a escribir sus credenciales en esa solicitud, negando el inicio de sesión único.
  • loginHint se establece en peter@contoso.com: rellena previamente el campo de nombre de usuario o dirección de correo electrónico de la página de inicio de sesión del usuario en peter@contoso.com. Las aplicaciones suelen utilizar este parámetro durante la reautenticación, dado que ya han extraído el nombre de usuario de un inicio de sesión anterior mediante la notificación 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");
    });
}

En el ejemplo anterior se asume:

Para obtener más información, consulte los siguientes recursos:

Personalización de opciones al usar un IAccessTokenProvider

Si se produce un error en la obtención de un token al usar IAccessTokenProvider, administre parámetros adicionales para una nueva solicitud de token de acceso del proveedor de identidades con los métodos siguientes una o varias veces en una nueva instancia de InteractiveRequestOptions:

En el ejemplo siguiente que intenta obtener un token de acceso para el usuario, se agregan parámetros adicionales a la solicitud de inicio de sesión si se produce un error en el intento de obtener un token cuando se llama a TryGetToken:

  • prompt se establece en login: obliga al usuario a escribir sus credenciales en esa solicitud, negando el inicio de sesión único.
  • loginHint se establece en peter@contoso.com: rellena previamente el campo de nombre de usuario o dirección de correo electrónico de la página de inicio de sesión del usuario en peter@contoso.com. Las aplicaciones suelen utilizar este parámetro durante la reautenticación, dado que ya han extraído el nombre de usuario de un inicio de sesión anterior mediante la notificación 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);
}

En el ejemplo anterior se asume:

Para obtener más información, consulte los siguientes recursos:

Cierre de sesión con una dirección URL de retorno personalizada

En el ejemplo siguiente se cierra la sesión del usuario y se devuelve el usuario al punto de conexión /goodbye:

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

Obtención de la ruta de acceso de inicio de sesión desde las opciones de autenticación

Obtenga la ruta de inicio de sesión configurada de RemoteAuthenticationOptions:

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

En el ejemplo anterior se asume:

Clase AuthorizationMessageHandler personalizada

Las instrucciones de esta sección se recomiendan para las aplicaciones cliente que realizan solicitudes salientes a los URI que no están dentro del URI base de la aplicación.

En el ejemplo siguiente, una clase personalizada extiende AuthorizationMessageHandler para su uso como DelegatingHandler de un HttpClient. ConfigureHandler configura este controlador para autorizar las solicitudes HTTP salientes mediante un token de acceso. El token de acceso solo se adjunta si al menos una de las direcciones URL autorizadas es una base del URI de solicitud (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" });
    }
}

En el código anterior, los ámbitos example.read y example.write son ejemplos genéricos que no están diseñados para reflejar ámbitos válidos para ningún proveedor determinado.

En el archivo Program, CustomAuthorizationMessageHandler se registra como un servicio transitorio y se configura como DelegatingHandler para las instancias de HttpResponseMessage salientes realizadas por un elemento llamado HttpClient.

En el ejemplo siguiente, HttpClientFactoryServiceCollectionExtensions.AddHttpClient es una extensión de Microsoft.Extensions.Http. Agregue el paquete a una aplicación que aún no haga referencia a él.

Nota

Para obtener instrucciones sobre cómo agregar paquetes a aplicaciones .NET, consulte los artículos de Instalación y administración de paquetes en Flujo de trabajo de consumo de paquetes (NuGet documentación). Confirme las versiones correctas del paquete en NuGet.org.

builder.Services.AddTransient<CustomAuthorizationMessageHandler>();

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

Nota

En el ejemplo anterior, el CustomAuthorizationMessageHandlerDelegatingHandler se registra como un servicio transitorio para AddHttpMessageHandler. Se recomienda el registro transitorio para IHttpClientFactory, que administra sus propios ámbitos de inserción de dependencias. Para obtener más información, consulte los siguientes recursos:

En el caso de una solución Blazor hospedada y basada en la plantilla del proyecto de Blazor WebAssembly, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) se asigna al elemento HttpClient.BaseAddress de forma predeterminada.

El cliente HttpClient configurado se usa para realizar solicitudes autorizadas por medio del patrón try-catch. Cuando el cliente se crea con CreateClient (paquete Microsoft.Extensions.Http), se proporcionan instancias al cliente HttpClient que incluyen tokens de acceso al realizar solicitudes a la API de servidor. Si el URI de solicitud es un URI relativo, como en el ejemplo siguiente (ExampleAPIMethod), se combina con el elemento BaseAddress cuando la aplicación cliente realiza la solicitud:

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

Configuración de AuthorizationMessageHandler

AuthorizationMessageHandler se puede configurar con las direcciones URL, los ámbitos y una dirección URL de retorno autorizados usando el método ConfigureHandler. ConfigureHandler configura el controlador para autorizar las solicitudes HTTP salientes mediante un token de acceso. El token de acceso solo se adjunta si al menos una de las direcciones URL autorizadas es una base del URI de solicitud (HttpRequestMessage.RequestUri). Si el URI de solicitud es un URI relativo, se combina con el elemento BaseAddress.

En el siguiente ejemplo, AuthorizationMessageHandler configura un HttpClient en el archivo 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")
    });

En el código anterior, los ámbitos example.read y example.write son ejemplos genéricos que no están diseñados para reflejar ámbitos válidos para ningún proveedor determinado.

En el caso de una solución Blazor hospedada y basada en la plantilla del proyecto de Blazor WebAssembly, IWebAssemblyHostEnvironment.BaseAddress se asigna al elemento siguiente de forma predeterminada:

  • HttpClient.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)).
  • Dirección URL de la matriz de authorizedUrls.

Con tipo HttpClient

Se puede definir un cliente con tipo que controle todos los problemas de obtención de tokens y HTTP dentro de una misma clase.

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

En el ejemplo anterior, el tipo WeatherForecast es una clase estática que contiene datos de previsión meteorológica. El marcador de posición {ASSEMBLY NAME} es el nombre de ensamblado de la aplicación (por ejemplo, using static BlazorSample.Data;).

En el ejemplo siguiente, HttpClientFactoryServiceCollectionExtensions.AddHttpClient es una extensión de Microsoft.Extensions.Http. Agregue el paquete a una aplicación que aún no haga referencia a él.

Nota

Para obtener instrucciones sobre cómo agregar paquetes a aplicaciones .NET, consulte los artículos de Instalación y administración de paquetes en Flujo de trabajo de consumo de paquetes (NuGet documentación). Confirme las versiones correctas del paquete en NuGet.org.

En el archivo 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>();

En el caso de una solución Blazor hospedada y basada en la plantilla del proyecto de Blazor WebAssembly, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) se asigna al elemento HttpClient.BaseAddress de forma predeterminada.

En un componente que captura datos meteorológicos:

@inject WeatherForecastClient Client

...

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

Configuración del controlador HttpClient

El controlador se puede seguir configurando con ConfigureHandler para las solicitudes HTTP salientes.

En el ejemplo siguiente, HttpClientFactoryServiceCollectionExtensions.AddHttpClient es una extensión de Microsoft.Extensions.Http. Agregue el paquete a una aplicación que aún no haga referencia a él.

Nota

Para obtener instrucciones sobre cómo agregar paquetes a aplicaciones .NET, consulte los artículos de Instalación y administración de paquetes en Flujo de trabajo de consumo de paquetes (NuGet documentación). Confirme las versiones correctas del paquete en NuGet.org.

En el archivo 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" }));

En el código anterior, los ámbitos example.read y example.write son ejemplos genéricos que no están diseñados para reflejar ámbitos válidos para ningún proveedor determinado.

En el caso de una solución Blazor hospedada y basada en la plantilla del proyecto de Blazor WebAssembly, IWebAssemblyHostEnvironment.BaseAddress se asigna al elemento siguiente de forma predeterminada:

  • HttpClient.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)).
  • Dirección URL de la matriz de authorizedUrls.

Solicitudes de API web no autenticadas o no autorizadas en una aplicación con un cliente predeterminado seguro

Una aplicación que normalmente usa un elemento HttpClient predeterminado también podrá realizar solicitudes de API web no autenticadas o no autorizadas configurando un elemento HttpClient con nombre.

En el ejemplo siguiente, HttpClientFactoryServiceCollectionExtensions.AddHttpClient es una extensión de Microsoft.Extensions.Http. Agregue el paquete a una aplicación que aún no haga referencia a él.

Nota

Para obtener instrucciones sobre cómo agregar paquetes a aplicaciones .NET, consulte los artículos de Instalación y administración de paquetes en Flujo de trabajo de consumo de paquetes (NuGet documentación). Confirme las versiones correctas del paquete en NuGet.org.

En el archivo Program:

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

En el caso de una solución Blazor hospedada y basada en la plantilla del proyecto de Blazor WebAssembly, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) se asigna al elemento HttpClient.BaseAddress de forma predeterminada.

El registro anterior se realiza además del registro de HttpClient predeterminado seguro existente.

Un componente crea el cliente HttpClient a partir del cliente IHttpClientFactory (paquete Microsoft.Extensions.Http) para realizar solicitudes no autenticadas o no autorizadas:

@inject IHttpClientFactory ClientFactory

...

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

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

        ...
    }
}

Nota

El controlador de la API de servidor (ExampleNoAuthenticationController en el ejemplo anterior) no está marcado con el atributo [Authorize].

La decisión de si usar un cliente seguro o uno no seguro como la instancia de HttpClient predeterminada depende del desarrollador. Una manera de tomar esta decisión es tener en cuenta el número de puntos de conexión autenticados y no autenticados en los que la aplicación establece el contacto. Si la mayoría de las solicitudes de la aplicación es para proteger los puntos de conexión de la API, use la instancia de HttpClient autenticada como valor predeterminado. De lo contrario, registre la instancia de HttpClient no autenticada como valor predeterminado.

Un enfoque alternativo al uso de IHttpClientFactory es crear un cliente con tipo para el acceso no autenticado a puntos de conexión anónimos.

Solicitar más tokens de acceso

Los tokens de acceso se pueden obtener manualmente llamando a IAccessTokenProvider.RequestAccessToken. En el ejemplo siguiente, una aplicación requiere un ámbito adicional para el HttpClientpredeterminado. En el ejemplo de la biblioteca de autenticación de Microsoft (MSAL) se configura el ámbito con MsalProviderOptions:

En el archivo Program:

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

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

Los marcadores de posición {CUSTOM SCOPE 1} y {CUSTOM SCOPE 2} del ejemplo anterior son ámbitos personalizados.

Nota:

AdditionalScopesToConsent no puede aprovisionar permisos de usuario delegado para Microsoft Graph a través de la interfaz de usuario de consentimiento de Microsoft Entra ID cuando un usuario utiliza por primera vez una aplicación registrada en Microsoft Azure. Para obtener más información, consulte Uso de Graph API con ASP.NET Core Blazor WebAssembly.

El método IAccessTokenProvider.RequestAccessToken proporciona una sobrecarga que permite a una aplicación aprovisionar un token de acceso con un determinado conjunto de ámbitos.

En un componente 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))
{
    ...
}

Los marcadores de posición {CUSTOM SCOPE 1} y {CUSTOM SCOPE 2} del ejemplo anterior son ámbitos personalizados.

AccessTokenResult.TryGetToken devuelve lo siguiente:

  • true con el token para su uso.
  • false si el token no se obtiene.

Uso compartido de recursos entre orígenes (CORS)

Al enviar credenciales (cookies de autorización o encabezados) en solicitudes de CORS, la directiva de CORS debe permitir el encabezado Authorization.

En la directiva siguiente se incluye la configuración de:

  • Orígenes de la solicitud (http://localhost:5000, https://localhost:5001).
  • Cualquier método (verbo).
  • Los encabezados Content-Type y Authorization. Para permitir un encabezado personalizado (por ejemplo, x-custom-header), enumere el encabezado al llamar a WithHeaders.
  • Las credenciales establecidas por el código JavaScript del lado cliente (la propiedad credentials establecida en include).
app.UseCors(policy => 
    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
        .AllowAnyMethod()
        .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, 
            "x-custom-header")
        .AllowCredentials());

Una solución Blazor hospedada y basada en la plantilla del proyecto de Blazor WebAssembly utiliza la misma dirección base para las aplicaciones cliente y servidor. De forma predeterminada, el elemento HttpClient.BaseAddress de la aplicación cliente está establecido en un URI de builder.HostEnvironment.BaseAddress. La configuración de CORS no es necesaria en la configuración predeterminada de una solución Blazor hospedada. Las aplicaciones cliente adicionales que no se hospedan en el proyecto de servidor y no comparten la dirección base de la aplicación de servidor requieren la configuración de CORS en el proyecto de servidor.

Para obtener más información, vea Habilitar solicitudes entre orígenes (CORS) en ASP.NET Core y el componente evaluador de solicitudes HTTP de la aplicación de ejemplo (Components/HTTPRequestTester.razor).

Controlar los errores de solicitudes de token

Cuando una aplicación de página única (SPA) autentica a un usuario a través de OpenID Connect (OIDC), el estado de autenticación se conserva localmente en dicha aplicación de página única y también en el proveedor de Identity en forma de cookie de sesión que se establece como consecuencia de que un usuario haya especificado sus credenciales.

Normalmente, los tokens que el proveedor de identidades emite para el usuario son válidos durante un breve período de tiempo (aproximadamente una hora), por lo que la aplicación cliente debe capturar nuevos tokens de manera regular. De lo contrario, se cerrará la sesión del usuario después de que los tokens concedidos expiren. En la mayoría de los casos, los clientes de OIDC pueden aprovisionar nuevos tokens sin necesidad de que el usuario tenga que autenticarse otra vez, gracias al estado de autenticación o a la "sesión" que el proveedor de identidades conserva.

Hay algunos casos en los que el cliente no puede obtener un token sin interacción del usuario, por ejemplo, cuando por alguna razón el usuario cierra la sesión del proveedor de identidades expresamente. Este escenario sucede si un usuario visita https://login.microsoftonline.com y cierra sesión. En estos casos, la aplicación no sabe de inmediato que el usuario ha cerrado la sesión. Es posible que el token que el cliente contiene ya no sea válido. Además, el cliente no puede aprovisionar un token nuevo sin interacción del usuario después de que el token actual expire.

Estos escenarios no son específicos de la autenticación basada en tokens, sino que forman parte de la idiosincrasia de las aplicaciones de página única. Una aplicación de página única que use cookies tampoco podrá llamar a una API de servidor si la cookie de autenticación se quita.

Cuando una aplicación realiza llamadas API a recursos protegidos, hay que tener en cuenta lo siguiente:

  • Para aprovisionar un nuevo token de acceso para llamar a la API, puede que el usuario deba autenticarse de nuevo.
  • Aun cuando el cliente tenga un token que parezca válido, puede que se produzca un error en la llamada al servidor porque el usuario ha revocado dicho token.

Cuando la aplicación solicita un token, hay dos resultados posibles:

  • La solicitud se realiza correctamente y la aplicación tiene un token válido.
  • La solicitud no se realiza correctamente y la aplicación debe volver a autenticar al usuario para obtener un nuevo token.

Cuando una solicitud de token no se realiza correctamente, debe decidir si quiere guardar el estado actual antes de realizar una redirección. Para almacenar el estado con niveles de complejidad crecientes, existen varios enfoques:

  • Almacenar el estado de la página actual en el almacenamiento de sesión. Durante el método de ciclo de vidaOnInitializedAsync (OnInitializedAsync), compruebe si el estado se puede restaurar antes de continuar.
  • Agregar un parámetro de cadena de consulta y usarlo como una manera de señalar la aplicación que necesita para volver a hidratar el estado guardado anteriormente.
  • Agregue un parámetro de cadena de consulta con un identificador único para almacenar los datos en el almacenamiento de sesión, sin riesgo de que se produzcan discrepancias con otros elementos.

Guardado del estado de la aplicación antes de una operación de autenticación con el almacenamiento de sesión

El ejemplo siguiente muestra cómo:

  • Conservar el estado antes de redirigir a la página de inicio de sesión.
  • Recuperar el estado anterior después de la autenticación mediante un parámetro de cadena de consulta.
...
@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; }
    }
}

Guardar el estado de la aplicación antes de una operación de autenticación con el almacenamiento de sesión y un contenedor de estado.

Durante una operación de autenticación, hay casos en los que conviene guardar el estado de la aplicación antes de que el explorador se redirija a la dirección IP. Esto puede suceder cuando se usa un contenedor de estado y se quiere restaurar el estado después de que la autenticación se realice correctamente. Se puede usar un objeto de estado de autenticación personalizado para conservar el estado específico de la aplicación o una referencia a este, y restaurar el estado después de que la operación de autenticación se complete correctamente. Este método se describe en el siguiente ejemplo.

Se crea una clase de contenedor de estado en la aplicación con propiedades que contienen los valores de estado de la aplicación. En el ejemplo siguiente, el contenedor se usa para mantener el valor de contador del componente Counter de la plantilla del proyecto de Blazor predeterminada (Counter.razor). Los métodos para serializar y deserializar el contenedor se basan en 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;
    }
}

El componente Counter usa el contenedor de estado para conservar el valor de currentCount fuera del componente:

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

Cree un objeto ApplicationAuthenticationState a partir de RemoteAuthenticationState. Proporcione una propiedad Id, que actúa como identificador del estado almacenado localmente.

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

El componente Authentication (Authentication.razor) guarda y restaura el estado de la aplicación usando el almacenamiento de sesión local con los métodos de serialización y deserialización de StateContainer, GetStateForLocalStorage y 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);
            }
        }
    }
}

En este ejemplo se usa Microsoft Entra (ME-ID) para la autenticación. En el archivo Program:

  • ApplicationAuthenticationState se configura como el tipo RemoteAuthenticationState de la biblioteca de autenticación de Microsoft (MSAL).
  • El contenedor de estado se registra en el contenedor de servicios.
builder.Services.AddMsalAuthentication<ApplicationAuthenticationState>(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});

builder.Services.AddSingleton<StateContainer>();

Personalizar rutas de aplicación

La biblioteca Microsoft.AspNetCore.Components.WebAssembly.Authentication usa de forma predeterminada las rutas indicadas en la siguiente tabla para representar distintos estados de autenticación.

Ruta Propósito
authentication/login Desencadena una operación de inicio de sesión.
authentication/login-callback Controla el resultado de cualquier operación de inicio de sesión.
authentication/login-failed Muestra mensajes de error cuando, por algún motivo, se produce un error en la operación de inicio de sesión.
authentication/logout Desencadena una operación de cierre de sesión.
authentication/logout-callback Controla el resultado de una operación de cierre de sesión.
authentication/logout-failed Muestra mensajes de error cuando, por algún motivo, se produce un error en la operación de cierre de sesión.
authentication/logged-out Indica que el usuario ha cerrado sesión correctamente.
authentication/profile Desencadena una operación para editar el perfil de usuario.
authentication/register Desencadena una operación para registrar un usuario nuevo.

Las rutas que se muestran en la tabla anterior se pueden configurar a través de RemoteAuthenticationOptions<TRemoteAuthenticationProviderOptions>.AuthenticationPaths. Al establecer las opciones para proporcionar rutas personalizadas, confirme que la aplicación tiene una ruta que controla cada ruta de acceso.

En el siguiente ejemplo, todas las rutas de acceso tienen el prefijo /security.

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

En el archivo 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 el requisito precisa de rutas de acceso completamente diferentes, establezca las rutas tal y como se ha descrito anteriormente y represente la vista RemoteAuthenticatorView con un parámetro de acción explícito:

@page "/register"

<RemoteAuthenticatorView Action="RemoteAuthenticationActions.Register" />

La interfaz de usuario se puede fragmentar en diferentes páginas si así decide hacerlo.

Personalizar la interfaz de usuario de autenticación

RemoteAuthenticatorView incluye un conjunto predeterminado de fragmentos de la interfaz de usuario para cada estado de autenticación. Cada estado se puede personalizar pasando un objeto RenderFragment personalizado. Para personalizar el texto que aparece durante el proceso de inicio de sesión inicial, puede cambiar la vista RemoteAuthenticatorView como se indica aquí.

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

La vista RemoteAuthenticatorView tiene un fragmento que se puede usar por cada ruta de autenticación que se muestra en la siguiente tabla.

Ruta Fragmento
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>

Personalizar el usuario

Los usuarios enlazados a la aplicación se pueden personalizar.

Personalización del usuario con una notificación de carga

En el siguiente ejemplo, los usuarios autenticados de la aplicación reciben una notificación de amr por cada uno de los métodos de autenticación del usuario. La notificación de amr identifica cómo se autenticó el firmante del token en las notificaciones de carga de la plataforma de Identity de Microsoft v1.0. En el ejemplo se usa una clase de cuenta de usuario personalizada basada en RemoteUserAccount.

Cree una clase que extienda la clase RemoteUserAccount. En el ejemplo siguiente se establece la propiedad AuthenticationMethod en la matriz del usuario de valores de propiedad JSON de amr. El marco rellena AuthenticationMethod automáticamente cuando se autentica el usuario.

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

Cree un generador que extienda AccountClaimsPrincipalFactory<TAccount> para crear notificaciones desde los métodos de autenticación del usuario almacenados en 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;
    }
}

Registre la fábrica CustomAccountFactory del proveedor de autenticación en uso. Cualquiera de los siguientes registros es válido:

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

Grupos y roles de seguridad ME-ID con una clase de cuenta de usuario personalizada

Para ver un ejemplo adicional que funciona con grupos de seguridad ME-ID y roles de administrador ME-ID y una clase de cuenta de usuario personalizada, consulta ASP.NETBlazor WebAssembly Core con grupos y roles Microsoft Entra ID.

Representación previa con la autenticación

Actualmente no se admite la representación previa de contenido que requiere autenticación y autorización. Después de seguir las instrucciones que aparecen en uno de los temas sobre seguridad de la aplicación Blazor WebAssembly, use estas instrucciones para crear una aplicación que:

  • Representa previamente las rutas de acceso para las que no se requiere autorización.
  • No representa previamente las rutas de acceso para las que se requiere autorización.

Para el archivo Client del proyectoProgram, factorice los registros de servicio comunes en un método independiente (por ejemplo, cree un ConfigureCommonServicesmétodo en el proyecto Client ). Los servicios comunes son aquellos que el desarrollador registra para que los usen las aplicaciones cliente y servidor.

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

En el archivo Program:

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

builder.Services.AddScoped( ... );

ConfigureCommonServices(builder.Services);

await builder.Build().RunAsync();

En el archivo Server del proyecto de Program, registre los siguientes servicios adicionales y llame a 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);

En el método Startup.ConfigureServices del proyecto Server, registre los siguientes servicios adicionales y llame a 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);
}

Para obtener más información sobre el Blazor proveedor de autenticación del servidor de marco (ServerAuthenticationStateProvider), consulte Autenticación y autorización de Blazor en ASP.NET Core.

En el archivo Pages/_Host.cshtml del proyecto Server, reemplace el asistente de etiquetas Component (<component ... />) por lo siguiente:

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

En el ejemplo anterior:

  • El marcador de posición {CLIENT APP ASSEMBLY NAME} es el nombre de ensamblado de la aplicación cliente (por ejemplo, BlazorSample.Client).
  • Comprobación condicional del segmento de ruta /authentication:
    • Evita la representación previa (render-mode="WebAssembly") para las rutas de autenticación.
    • Realiza la representación previa (render-mode="WebAssemblyPrerendered") para las rutas que no son de autenticación.

Opciones para aplicaciones hospedadas y proveedores de inicio de sesión de terceros

Al autenticar y autorizar una aplicación Blazor WebAssembly con un proveedor de terceros, hay varias opciones disponibles para autenticar al usuario. Su elección depende del escenario.

Para obtener más información, vea Conservar notificaciones y tokens adicionales de proveedores externos en ASP.NET Core.

Autenticación de usuarios solo para llamadas a las API de terceros protegidas

Autentique al usuario con un flujo de OAuth del lado cliente en el proveedor de la API de terceros:

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

En este escenario:

  • El servidor que hospeda la aplicación no desempeña ningún rol.
  • No se pueden proteger las API en el servidor.
  • La aplicación solo puede llamar a las API de terceros protegidas.

Autenticación de usuarios con un proveedor de terceros y llamada a las API protegidas en el servidor host y en el tercero

Configure Identity con un proveedor de inicio de sesión de terceros. Obtenga los tokens necesarios para el acceso de API de terceros y almacénelos.

Cuando un usuario inicia sesión, Identity recopila los tokens de acceso y actualización como parte del proceso de autenticación. En ese momento, hay un par de enfoques disponibles para realizar llamadas API a las API de terceros.

Uso de un token de acceso de servidor para recuperar el token de acceso de terceros

Use el token de acceso generado en el servidor para recuperar el token de acceso de terceros de un punto de conexión de la API de servidor. A partir de ahí, use el token de acceso de terceros para llamar directamente a los recursos de la API de terceros desde Identity en el cliente.

Este enfoque no se recomienda. Este enfoque requiere tratar el token de acceso de terceros como si se hubiera generado para un cliente público. En términos de OAuth, la aplicación pública no tiene un secreto de cliente porque no es de confianza para almacenar secretos de forma segura y el token de acceso se genera para un cliente confidencial. Un cliente confidencial es un cliente que tiene un secreto de cliente y se supone que puede almacenar secretos de forma segura.

  • El token de acceso de terceros podría recibir ámbitos adicionales para realizar operaciones confidenciales en función del hecho de que la tercera parte emitió el token para un cliente de más confianza.
  • Del mismo modo, los tokens de actualización no deben emitirse para un cliente que no sea de confianza, ya que esto proporciona acceso ilimitado al cliente a menos que se apliquen otras restricciones.

Realización de llamadas API desde el cliente a la API de servidor para llamar a las API de terceros

Realice una llamada API desde el cliente a la API del servidor. En el servidor, recupere el token de acceso para el recurso de la API de terceros y emita cualquier llamada que sea necesaria.

Este es el enfoque recomendado. Aunque este enfoque requiere un salto de red adicional mediante el servidor para llamar a una API de terceros, a la larga resulta una experiencia más segura:

  • El servidor puede almacenar tokens de actualización y asegurarse de que la aplicación no pierde el acceso a recursos de terceros.
  • La aplicación no puede filtrar los tokens de acceso del servidor que puedan contener permisos más confidenciales.

Uso de puntos de conexión de OpenID Connect (OIDC) v2.0

La biblioteca de autenticación y las plantillas de proyecto de Blazor usan puntos de conexión de OpenID Connect (OIDC) v1.0. Para usar un punto de conexión de la versión 2.0, configure la opción JwtBearerOptions.Authority del portador de JWT. En el siguiente ejemplo, AAD se ha configurado para la versión 2.0 anexando un v2.0segmento a la propiedad Authority:

using Microsoft.AspNetCore.Authentication.JwtBearer;

...

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

Como alternativa, esta opción se puede establecer en el archivo de configuración de la aplicación (appsettings.json):

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

Si la anexión de un segmento a la propiedad Authority no es adecuado para el proveedor de OIDC de la aplicación, como es el caso de los proveedores que no son AAD, establezca la propiedad Authority directamente. Establezca la propiedad en JwtBearerOptions o en el archivo de configuración de la aplicación (appsettings.json) con la clave Authority.

La lista de notificaciones en el token de identificador cambia en el caso de los puntos de conexión de la versión 2.0. Aunque la documentación de Microsoft sobre los cambios se ha retirado, se puede encontrar una guía sobre las notificaciones de un token de identificación en la referencia de notificaciones del token de identificación.

Configuración y uso de gRPC en componentes

Para configurar una aplicación Blazor WebAssembly que use el marco gRPC de ASP.NET Core:

Nota:

La representación previa está habilitada de forma predeterminada en Blazor Web Apps, por lo que debe tener en cuenta la representación de componentes primero desde el servidor y, después, desde el cliente. Cualquier estado anterior debe fluir al cliente para que se pueda reutilizar. Para más información, consulte Representación previa de componentes Razor de ASP.NET Core .

Nota:

La representación previa está habilitada de forma predeterminada en aplicaciones Blazor WebAssembly hospedadas, por lo que debe tener en cuenta la representación de componentes primero desde el servidor y, después, desde el cliente. Cualquier estado anterior debe fluir al cliente para que se pueda reutilizar. Para más información, vea Integración y representación previa de componentes Razor de ASP.NET Core.

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 componente de la aplicación cliente puede realizar llamadas a gRPC mediante el cliente de 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();
        }
    }
}

Para usar la propiedad Status.DebugException, use Grpc.Net.Client, versión 2.30.0 o posterior.

Para obtener más información, consulte gRPC-Web en aplicaciones gRPC de ASP.NET Core.

Reemplazar la implementación de AuthenticationService

En las subsecciones siguientes se explica cómo realizar la sustitución:

  • Cualquier implementación de AuthenticationService de JavaScript.
  • La biblioteca de autenticación de Microsoft para JavaScript (MSAL.js).

Reemplazar cualquier implementación de AuthenticationService de JavaScript

Cree una biblioteca de JavaScript para controlar los detalles de autenticación personalizados.

Advertencia

La guía de esta sección es un detalle de implementación de la instancia predeterminada de RemoteAuthenticationService<TRemoteAuthenticationState,TAccount,TProviderOptions>. El código TypeScript de esta sección se aplica específicamente a ASP.NET Core en .NET 7 y está sujeto a cambios sin previo aviso en próximas versiones de 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 };
};

Para importar la biblioteca, quite la etiqueta <script> original y agregue una etiqueta <script> que cargue la biblioteca personalizada. En el ejemplo siguiente se muestra cómo reemplazar la etiqueta <script> predeterminada por otra que cargue una biblioteca denominada CustomAuthenticationService.js desde la carpeta wwwroot/js.

En wwwroot/index.html antes del script Blazor (_framework/blazor.webassembly.js) dentro de la etiqueta de cierre </body>:

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

Para más información, consulte AuthenticationService.ts en el repositorio de GitHub dotnet/aspnetcore.

Nota:

Los vínculos de la documentación al origen de referencia de .NET cargan normalmente la rama predeterminada del repositorio, que representa el desarrollo actual para la próxima versión de .NET. Para seleccionar una etiqueta de una versión específica, use la lista desplegable Cambiar ramas o etiquetas. Para obtener más información, vea Procedimientos para seleccionar una etiqueta de versión de código fuente de ASP.NET Core (dotnet/AspNetCore.Docs #26205).

Reemplazar la biblioteca de autenticación de Microsoft para JavaScript (MSAL.js)

Si una aplicación requiere una versión personalizada de la biblioteca de autenticación de Microsoft para JavaScript (MSAL.js), realice los pasos siguientes:

  1. Confirme que el sistema tiene el SDK de .NET para desarrolladores más reciente u obtenga e instale el SDK para desarrolladores más reciente de SDK de .NET Core: instaladores y binarios. La configuración de fuentes de NuGet internas no es necesaria para este escenario.
  2. Configure el repositorio de GitHub dotnet/aspnetcore para el desarrollo según la documentación de Compilación de ASP.NET Core desde el origen. Bifurque y clone o descargue un archivo ZIP del dotnet/aspnetcore repositorio de GitHub.
  3. Abra el archivo src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json y establezca la versión deseada de @azure/msal-browser. Para obtener una lista de las versiones publicadas, visite el sitio web de npm @azure/msal-browser y seleccione la pestaña de las versiones.
  4. Compile el proyecto Authentication.Msal en la carpeta src/Components/WebAssembly/Authentication.Msal/src con el comando yarn build en un shell de comandos.
  5. Si la aplicación usa recursos comprimidos (Brotli/gzip), comprima el archivo Interop/dist/Release/AuthenticationService.js.
  6. Copie el archivo AuthenticationService.js y las versiones comprimidas (.br/.gz) del archivo, si se producen, de la carpeta Interop/dist/Release en la carpeta publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal de la aplicación en los recursos publicados de la aplicación.

Pasar opciones de proveedor personalizadas

Defina una clase para pasar los datos a la biblioteca de JavaScript subyacente.

Importante

La estructura de la clase debe coincidir con lo que espera la biblioteca cuando el JSON se serializa con System.Text.Json.

En el ejemplo siguiente se muestra una clase ProviderOptions con atributos JsonPropertyName que coinciden con las expectativas de una biblioteca de proveedores personalizada hipotética:

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

Registre las opciones de proveedor en el sistema de inserción de dependencias y configure los valores adecuados:

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 = "...";
    });

En el ejemplo anterior se establecen URI de redireccionamiento con literales de cadena normales. Se encuentran disponibles las siguientes alternativas:

Recursos adicionales