Cenários de segurança adicionais do Blazor do ASP.NET Core do lado do servidor

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Este artigo explica como configurar o Blazor do lado do servidor para cenários de segurança adicionais, incluindo como passar tokens a um aplicativo Blazor.

Observação

Os exemplos de código neste artigo adotam tipos de referência anuláveis (NRTs) e análise estática de estado nulo do compilador .NET, que são compatíveis com ASP.NET Core no .NET 6 ou posterior. Ao direcionar ASP.NET Core 5.0 ou anterior, remova a designação de tipo nulo (?) dos tipos string?, TodoItem[]?, WeatherForecast[]? e IEnumerable<GitHubBranch>? nos exemplos do artigo.

Passar tokens para um aplicativo Blazor do lado do servidor

A atualização desta seção para Blazor Aplicativos Web está pendente Atualizar a seção sobre a passagem de tokens em Blazor Aplicativos Web (dotnet/AspNetCore.Docs #31691). Para obter mais informações, consulte Problema ao fornecer o Token de Acesso ao HttpClient no modo de servidor interativo (dotnet/aspnetcore nº52390).

Para Blazor Server, consulte a versão 7.0 da seção deste artigo.

Os tokens disponíveis fora dos componentes Razor em um aplicativo Blazor do lado do servidor podem ser passados aos componentes com a abordagem descrita nesta seção. O exemplo nesta seção se concentra na passagem de tokens de acesso, de atualização e token de prevenção de solicitação de falsificação (XSRF) do aplicativo Blazor, mas a abordagem é válida para outro estado de contexto HTTP.

Observação

Passar o token XSRF para componentes Razor é útil em cenários onde os componentes POST para Identity ou outros pontos de extremidade exigem validação. Se seu aplicativo exigir apenas tokens de acesso e de atualização, você poderá remover o código do token XSRF do exemplo a seguir.

Autentique o aplicativo como faria com um aplicativo Razor Pages ou MVC comum. Provisione e salve os tokens na autenticação cookie.

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

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

Em 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, escopos adicionais são adicionados com options.Scope.Add("{SCOPE}");, em que o espaço reservado {SCOPE} é o escopo adicional a ser adicionado.

Defina um serviço de provedor de token com escopo que pode ser usado dentro do Blazor aplicativo para resolver os tokens da DI (injeção de dependência).

TokenProvider.cs:

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

No arquivo Program, adicione serviços para:

  • IHttpClientFactory: usado em uma classe WeatherForecastService que obtém dados meteorológicos de uma API de servidor com um token de acesso.
  • TokenProvider: mantém os tokens de acesso e atualização.
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

Em Startup.ConfigureServices de Startup.cs, adicione serviços para:

  • IHttpClientFactory: usado em uma classe WeatherForecastService que obtém dados meteorológicos de uma API de servidor com um token de acesso.
  • TokenProvider: mantém os tokens de acesso e atualização.
services.AddHttpClient();
services.AddScoped<TokenProvider>();

Defina uma classe para passar o estado inicial do aplicativo com os tokens de acesso e atualização.

InitialApplicationState.cs:

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

No arquivo Pages/_Host.cshtml, crie e a instância de InitialApplicationStatee passe-o como um parâmetro para o aplicativo:

No arquivo Pages/_Layout.cshtml, crie e a instância de InitialApplicationStatee passe-o como um parâmetro para o aplicativo:

No arquivo Pages/_Host.cshtml, crie e a instância de InitialApplicationStatee passe-o como um parâmetro para o aplicativo:

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

No componente App (App.razor), resolva o serviço e inicialize-o com os dados do 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();
    }
}

Observação

Uma alternativa para atribuir o estado inicial ao TokenProvider no exemplo anterior é copiar os dados para um serviço com escopo no OnInitializedAsync para uso em todo o aplicativo.

Adicione uma referência de pacote ao aplicativo para o pacote NuGet Microsoft.AspNet.WebApi.Client.

Observação

Para obter diretrizes sobre como adicionar pacotes a aplicativos .NET, consulte os artigos em Instalar e gerenciar pacotes no Fluxo de trabalho de consumo de pacotes (documentação do NuGet). Confirme as versões corretas de pacote em NuGet.org.

No serviço que faz uma solicitação de API segura, injete o provedor de token e recupere o token para a solicitação 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 um token XSRF passado para um componente, injete o TokenProvider e adicione o token XSRF à solicitação POST. O exemplo a seguir adiciona o token a um ponto de extremidade de logoff POST. O cenário do exemplo a seguir é que o ponto de extremidade de logoff (Areas/Identity/Pages/Account/Logout.cshtml, scaffolded no aplicativo) não especifica um IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]) porque ele executa alguma ação além de uma operação de logoff normal que deve ser protegida. O ponto de extremidade requer um token XSRF válido para processar a solicitação com êxito.

Em um componente que apresenta um botão Logoff para usuários 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>

Definir o esquema de autenticação

Para um aplicativo que usa mais de um Middleware de Autenticação e, portanto, tem mais de um esquema de autenticação, o esquema que o Blazor usa pode ser explicitamente definido na configuração do ponto de extremidade do arquivo Program. O exemplo a seguir define o esquema OIDC (OpenID Connect):

Para um aplicativo que usa mais de um Middleware de Autenticação e, portanto, tem mais de um esquema de autenticação, o esquema que Blazor usa pode ser definido explicitamente na configuração do ponto de extremidade do Startup.cs. O exemplo a seguir define o esquema OIDC (OpenID Connect):

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

Para um aplicativo que usa mais de um Middleware de Autenticação e, portanto, tem mais de um esquema de autenticação, o esquema que Blazor usa pode ser definido explicitamente na configuração do ponto de extremidade do Startup.Configure. O exemplo a seguir define o esquema do Microsoft Entra ID:

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

Usar pontos de extremidade do OIDC (OpenID Connect) v2.0

Em versões do ASP.NET Core anteriores à 5.0, a biblioteca de autenticação e os modelos Blazor usam pontos de extremidade do OpenID Connect (OIDC) v1.0. Para usar um ponto de extremidade v2.0 com versões de ASP.NET Core anteriores à 5.0, configure a opção OpenIdConnectOptions.Authority no OpenIdConnectOptions:

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

Como alternativa, a configuração pode ser feita no arquivo de configurações do aplicativo (appsettings.json):

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

Se anexar um segmento à autoridade não for apropriado para o provedor OIDC do aplicativo, como acontece com provedores que não são ME-ID, defina a propriedade Authority diretamente. Defina a propriedade em OpenIdConnectOptions ou no arquivo de configurações do aplicativo com a chave Authority.

Alterações de código

  • A lista de declarações no token de ID muda para pontos de extremidade v2.0. A documentação da Microsoft sobre as alterações foi desativada, mas as diretrizes sobre as declarações em um token de ID estão disponíveis na referência de declarações de token de ID.

  • Como os recursos são especificados em URIs de escopo para pontos de extremidade v2.0, remova a configuração da propriedade do OpenIdConnectOptions.Resource em OpenIdConnectOptions:

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

URI da ID do aplicativo

  • Ao usar pontos de extremidade v2.0, as APIs definem um App ID URI, que deve representar um identificador exclusivo para a API.
  • Todos os escopos incluem o URI da ID do Aplicativo como prefixo e os pontos de extremidade v2.0 emitem tokens de acesso com o URI da ID do Aplicativo como público-alvo.
  • Ao usar pontos de extremidade V2.0, a ID do cliente configurada na API do Servidor muda da ID do Aplicativo de API (ID do Cliente) para o URI da ID do Aplicativo.

appsettings.json:

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

Você pode encontrar o URI da ID do Aplicativo a ser usado na descrição do registro do aplicativo do provedor OIDC.

Manipulador de circuito para capturar usuários a serviços personalizados

Use um CircuitHandler para capturar um usuário do AuthenticationStateProvider e definir esse usuário em um serviço. Se você quiser atualizar o usuário, registre um retorno de chamada para AuthenticationStateChanged e enfileira um Task para obter o novo usuário e atualize o serviço. O exemplo a seguir demonstra a abordagem.

No exemplo a seguir:

  • OnConnectionUpAsync é chamado sempre que o circuito se reconecta, definindo o usuário para o tempo de vida da conexão. Somente o método OnConnectionUpAsync é necessário, a menos que você implemente atualizações por meio de um manipulador para alterações de autenticação (AuthenticationChanged no exemplo a seguir).
  • OnCircuitOpenedAsync é chamado para anexar o manipulador alterado de autenticação,AuthenticationChanged, para atualizar o usuário.
  • O bloco catch da tarefa UpdateAuthentication não toma nenhuma ação sobre exceções porque não há como denunciá-las neste momento na execução do código. Se uma exceção for gerada da tarefa, a exceção será relatada em outro lugar no aplicativo.

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

No arquivo Program:

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

...

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

No Startup.ConfigureServices do Startup.cs:

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

...

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

Use o serviço em um componente para obter o usuário:

@inject UserService UserService

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

Para definir o usuário no middleware para MVC, Razor Pages e em outros cenários ASP.NET Core, chame SetUser no UserService middleware personalizado após a execução do Middleware de Autenticação ou defina o usuário com uma implementação IClaimsTransformation. O exemplo a seguir adota a abordagem 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);
    }
}

Imediatamente antes da chamada para app.MapRazorComponents<App>() no arquivo Program, chame o middleware:

Imediatamente antes da chamada para app.MapBlazorHub() no arquivo Program, chame o middleware:

Imediatamente antes da chamada para app.MapBlazorHub() no Startup.Configure do Startup.cs, chame o middleware:

app.UseMiddleware<UserServiceMiddleware>();

Acessar AuthenticationStateProvider no middleware de solicitação de saída

O AuthenticationStateProvider de um DelegatingHandler para HttpClient criado com IHttpClientFactory pode ser acessado no middleware de solicitação de saída usando um manipulador de atividade de circuito.

Observação

Para obter diretrizes gerais sobre como definir manipuladores de delegação para solicitações HTTP por instâncias HttpClient criadas usando IHttpClientFactory em aplicativos ASP.NET Core, confira as seguintes seções Fazer solicitações HTTP usando IHttpClientFactory no ASP.NET Core:

O exemplo a seguir usa AuthenticationStateProvider para anexar um cabeçalho de nome de usuário personalizado de usuários autenticados a solicitações de saída.

Primeiro, implemente a classe CircuitServicesAccessor na seção a seguir do artigo de injeção de dependência (DI) Blazor:

Acessar serviços Blazor do lado do servidor de um escopo de DI diferente

Use o CircuitServicesAccessor para acessar o AuthenticationStateProvider na implementação 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);
    }
}

No arquivo Program, registre o AuthenticationStateHandler e adicione o manipulador ao IHttpClientFactory que cria instâncias HttpClient:

builder.Services.AddTransient<AuthenticationStateHandler>();

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