Otros escenarios de seguridad de Blazor en ASP.NET Core del lado servidor

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 explica cómo configurar Blazor del lado servidor para escenarios de seguridad adicionales, incluido cómo pasar tokens a una aplicación Blazor.

Nota:

En los ejemplos de código de este artículo se adoptan tipos de referencia que admiten un valor NULL (NRT) y análisis estático de estado NULL del compilador de .NET, que se admiten en ASP.NET Core en .NET 6 o posterior. Al tener como destino ASP.NET Core 5.0 o versiones anteriores, quite la designación de tipo null (?) de los tipos string?, TodoItem[]?, WeatherForecast[]? y IEnumerable<GitHubBranch>? de los ejemplos del artículo.

Paso de tokens a una aplicación Blazor del lado servidor

Con el enfoque descrito en esta sección, los tokens disponibles fuera de los componentes Razor de una aplicación Blazor del lado servidor se pueden pasar a otros componentes. El ejemplo de esta sección se centra en pasar tokens de acceso, actualización y prevención de ataques de falsificación de solicitudes (XSRF) a la aplicación Blazor, pero el enfoque es válido para otros estados de contexto HTTP.

Nota

Pasar el token XSRF a los componentes de Razor es útil en escenarios en los que los componentes usan POST para Identity u otros puntos de conexión que requieren validación. Si la aplicación solo requiere tokens de acceso y actualización, puede quitar el código de token XSRF del ejemplo siguiente.

Autentique la aplicación como lo haría con una instancia de Razor Pages o aplicación MVC al uso. Aprovisione y guarde los tokens en la cookie de autenticación.

En el archivo Program:

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

...

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

En Startup.cs:

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

...

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

En Startup.cs:

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

...

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

Opcionalmente, se agregan ámbitos adicionales con options.Scope.Add("{SCOPE}");, donde el marcador de posición {SCOPE} es el ámbito adicional que se va a agregar.

Defina un servicio de proveedor de tokens con ámbito que se pueda usar en la aplicación de Blazor para resolver los tokens procedentes de inserciones de dependencias.

TokenProvider.cs:

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

En el archivo Program, agregue servicios para lo siguiente:

  • IHttpClientFactory: se usa en una clase de WeatherForecastService que obtiene datos meteorológicos de una API de servidor con un token de acceso.
  • TokenProvider: contiene los tokens de acceso y actualización.
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

En Startup.ConfigureServices de Startup.cs, agregue servicios para:

  • IHttpClientFactory: se usa en una clase de WeatherForecastService que obtiene datos meteorológicos de una API de servidor con un token de acceso.
  • TokenProvider: contiene los tokens de acceso y actualización.
services.AddHttpClient();
services.AddScoped<TokenProvider>();

Defina una clase para pasar el estado de la aplicación inicial con los tokens de acceso y actualización.

InitialApplicationState.cs:

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

En el archivo Pages/_Host.cshtml, cree una instancia de InitialApplicationState y pásela como parámetro a la aplicación:

En el archivo Pages/_Layout.cshtml, cree una instancia de InitialApplicationState y pásela como parámetro a la aplicación:

En el archivo Pages/_Host.cshtml, cree una instancia de InitialApplicationState y pásela como parámetro a la aplicación:

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

...

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

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

En el componente App (App.razor), resuelva el servicio e inicialícelo con los datos del parámetro:

@inject TokenProvider TokenProvider

...

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

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

        return base.OnInitializedAsync();
    }
}

Nota:

Una alternativa a asignar el estado inicial al TokenProvider en el ejemplo anterior es copiar los datos en un servicio con ámbito dentro de OnInitializedAsync para su uso en toda la aplicación.

Agregue una referencia de paquete a la aplicación para el paquete de NuGet Microsoft.AspNet.WebApi.Client.

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 servicio que realiza una solicitud de API segura, inserte el proveedor de tokens y recupere el token para la solicitud de API:

WeatherForecastService.cs:

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

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

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

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

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

Para un token XSRF pasado a un componente, inserte TokenProvider y agregue el token XSRF a la solicitud POST. En el ejemplo siguiente se agrega el token a un comando POST de punto de conexión de cierre de sesión. El escenario del ejemplo siguiente es que el punto de conexión de cierre de sesión (Areas/Identity/Pages/Account/Logout.cshtml, estructurado en la aplicación) no especifica un objeto IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]) porque realiza alguna acción además de una operación de cierre de sesión normal que se debe proteger. El punto de conexión requiere un token XSRF válido para procesar correctamente la solicitud.

En un componente que presenta un botón Cerrar sesión para los usuarios autorizados:

@inject TokenProvider TokenProvider

...

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

Establecimiento del esquema de autenticación

En una aplicación que usa más de un middleware de autenticación (y que, por tanto, tiene más de un esquema de autenticación), el esquema que Blazor usa se puede establecer de forma explícita en la configuración del punto de conexión del archivo Program. En el ejemplo siguiente se establece el esquema OpenID Connect (OIDC):

En una aplicación que usa más de un middleware de autenticación (y que, por tanto, tiene más de un esquema de autenticación), el esquema que Blazor usa se puede establecer explícitamente en la configuración del punto de conexión de Startup.cs. En el ejemplo siguiente se establece el esquema OpenID Connect (OIDC):

using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

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

...

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

En una aplicación que usa más de un middleware de autenticación (y que, por tanto, tiene más de un esquema de autenticación), el esquema que Blazor usa se puede establecer explícitamente en la configuración del punto de conexión de Startup.Configure. En el ejemplo siguiente se establece el esquema de Microsoft Entra ID:

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

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

En las versiones de ASP.NET Core anteriores a la 5.0, la biblioteca de autenticación y las plantillas de Blazor usan puntos de conexión de OpenID Connect (OIDC) v1.0. Para usar un punto de conexión v2.0 con versiones de ASP.NET Core anteriores a la 5.0, configure la opción OpenIdConnectOptions.Authority en OpenIdConnectOptions:

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, 
    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):

{
  "AzureAd": {
    "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 OpenIdConnectOptions o en el archivo de configuración de la aplicación con la clave Authority.

Cambios en el código

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

  • Dado que los recursos de los puntos de conexión de la versión 2.0 se especifican en URI de ámbito, quite el valor de la propiedad OpenIdConnectOptions.Resource en OpenIdConnectOptions:

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

URI de id. de aplicación

  • Cuando se usan puntos de conexión 2.0, las API definen un elemento App ID URI , que está pensado a fin de representar un identificador único para la API.
  • Todos los ámbitos incluyen el URI del id. de aplicación como prefijo, y los puntos de conexión de la versión 2.0 emiten tokens de acceso con el URI del id. de aplicación como audiencia.
  • Cuando se usan puntos de conexión de la versión 2.0, el id. de cliente configurado en Server API cambia del id. de aplicación de la API (id. de cliente) al URI del id. de aplicación.

appsettings.json:

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

Encontrará el URI del identificador de aplicación que se va a usar en la descripción del registro de la aplicación del proveedor de OIDC.

Controlador de circuito para capturar usuarios para servicios personalizados

Use un CircuitHandler para capturar un usuario del AuthenticationStateProvider y establecer el usuario en un servicio. Si desea actualizar el usuario, registre una devolución de llamada a AuthenticationStateChanged y ponga en cola una Task para obtener el nuevo usuario y actualizar el servicio. Este método se describe en el siguiente ejemplo.

En el ejemplo siguiente:

  • OnConnectionUpAsync se llama cada vez que el circuito se vuelve a conectar, y establece el usuario durante la vigencia de la conexión. Solo se requiere el método OnConnectionUpAsync a menos que implemente actualizaciones a través de un controlador para cambios de autenticación (AuthenticationChanged en el ejemplo siguiente).
  • OnCircuitOpenedAsync se llama para adjuntar el controlador cambiado de autenticación, AuthenticationChanged, para actualizar el usuario.
  • El bloque catch de la tarea UpdateAuthentication no realiza ninguna acción sobre excepciones porque por ahora no hay ninguna manera de notificarlas en la ejecución del código. Si se produce una excepción desde la tarea, la excepción se notifica en otra parte de la aplicación.

UserService.cs:

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

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

    public ClaimsPrincipal GetUser()
    {
        return currentUser;
    }

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

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

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

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

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

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

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

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

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

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

    public ClaimsPrincipal GetUser()
    {
        return currentUser;
    }

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

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

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

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

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

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

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

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

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

En el archivo Program:

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

...

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

En Startup.ConfigureServices de Startup.cs:

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

...

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

Use el servicio en un componente para obtener el usuario:

@inject UserService UserService

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

Para establecer el usuario en middleware para MVC, páginas de Razor y en otros escenarios de ASP.NET Core, llame a SetUser en el UserService en middleware personalizado después de que se ejecute el middleware de autenticación o establezca el usuario con una implementación de IClaimsTransformation. En el ejemplo siguiente se adopta el enfoque de middleware.

UserServiceMiddleware.cs:

public class UserServiceMiddleware
{
    private readonly RequestDelegate next;

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

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

Inmediatamente antes de la llamada a app.MapRazorComponents<App>() en el archivo Program, llame al middleware:

Inmediatamente antes de la llamada a app.MapBlazorHub() en el archivo Program, llame al middleware:

Inmediatamente antes de la llamada a app.MapBlazorHub() en Startup.Configure de Startup.cs, llame al middleware:

app.UseMiddleware<UserServiceMiddleware>();

Acceso a AuthenticationStateProvider en el middleware de solicitud de salida

Se puede acceder a AuthenticationStateProvider de un DelegatingHandler para HttpClient creado con IHttpClientFactory en el middleware de solicitud de salida utilizando un controlador de actividad de circuito.

Nota:

Para obtener instrucciones generales sobre cómo definir controladores de delegación para solicitudes HTTP por HttpClient instancias creadas con IHttpClientFactory en aplicaciones ASP.NET Core, consulte las secciones siguientes de Realización de solicitudes HTTP mediante IHttpClientFactory en ASP.NET Core:

En el ejemplo siguiente se usa AuthenticationStateProvider para adjuntar un encabezado de nombre de usuario personalizado para los usuarios autenticados a las solicitudes de salida.

En primer lugar, implemente la clase CircuitServicesAccessor en la sección siguiente del artículo de inserción de dependencias (DI) Blazor:

Acceder a los servicios del Blazor servidor desde un ámbito DI diferente

Use CircuitServicesAccessor para acceder a AuthenticationStateProvider en la implementación de DelegatingHandler.

AuthenticationStateHandler.cs:

public class AuthenticationStateHandler : DelegatingHandler
{
    readonly CircuitServicesAccessor circuitServicesAccessor;

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

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

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

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

En el archivo Program, registre AuthenticationStateHandler y agregue el controlador a IHttpClientFactory que crea instancias de HttpClient:

builder.Services.AddTransient<AuthenticationStateHandler>();

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