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

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 descreve outros cenários de segurança para aplicativos Blazor WebAssembly.

Anexar tokens a solicitações de saída

AuthorizationMessageHandler é um DelegatingHandler usado para processar tokens de acesso. Os tokens são adquiridos usando o serviço IAccessTokenProvider, que é registrado pela estrutura. Se um token não puder ser adquirido, um AccessTokenNotAvailableException será gerado. AccessTokenNotAvailableException tem um método Redirect que navega para AccessTokenResult.InteractiveRequestUrl usando o AccessTokenResult.InteractionOptions fornecido para permitir a atualização do token de acesso.

Para sua conveniência, a estrutura fornece o BaseAddressAuthorizationMessageHandler pré-configurado com o endereço básico do aplicativo como uma URL autorizada. Os tokens de acesso só são adicionados quando o URI de solicitação está dentro do URI base do aplicativo. Quando os URIs de solicitação de saída não estiverem dentro do URI base do aplicativo, use uma classe personalizada AuthorizationMessageHandler (recomendado) ou configure o AuthorizationMessageHandler.

Observação

Além da configuração do aplicativo cliente para acesso à API do servidor, a API do servidor também deve permitir solicitações entre origens (CORS) quando o cliente e o servidor não residem no mesmo endereço básico. Para obter mais informações sobre a configuração do CORS do lado do servidor, confira a seção CORS (Compartilhamento de Recursos entre origens) mais adiante neste artigo.

No exemplo a seguir:

No exemplo a seguir, HttpClientFactoryServiceCollectionExtensions.AddHttpClient é uma extensão em Microsoft.Extensions.Http. Adicione o pacote a um aplicativo que ainda não o referenciou.

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.

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

Em uma Blazorsolução do hospedada baseada no Blazor WebAssembly modelo de projeto do, os URIs de solicitação ficam dentro do URI base do aplicativo por padrão. Portanto, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) é atribuído ao HttpClient.BaseAddress em um aplicativo gerado do modelo de projeto.

O HttpClient configurado é usado para fazer solicitações autorizadas com o padrão 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();
    }
}

Cenários de solicitação de autenticação personalizada

Os cenários a seguir demonstram como personalizar solicitações de autenticação e como obter o caminho de logon das opções de autenticação.

Personalizar o processo de logon

Gerencie parâmetros adicionais para uma solicitação de logon com os seguintes métodos uma ou mais vezes em uma nova instância do InteractiveRequestOptions:

No exemplo de componente LoginDisplay a seguir, parâmetros adicionais são incluídos na solicitação de logon:

  • prompt é definido como login: força o usuário a inserir suas credenciais nessa solicitação, negando o logon único.
  • loginHint é definido como peter@contoso.com: preenche previamente o campo nome de usuário/endereço de email da página de entrada do usuário para peter@contoso.com. Os aplicativos geralmente usam esse parâmetro durante a nova autenticação, já tendo extraído o nome de usuário de uma entrada anterior usando a declaração 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 obter mais informações, consulte os seguintes recursos:

Personalizar opções antes de obter um token de modo interativo

Se ocorrer um AccessTokenNotAvailableException, gerencie parâmetros adicionais para uma nova solicitação de token de acesso do provedor de identidade com os seguintes métodos uma ou mais vezes em uma nova instância de InteractiveRequestOptions:

No exemplo a seguir que obtém dados JSON por meio da API Web, parâmetros adicionais são incluídos na solicitação de redirecionamento se um token de acesso não estiver disponível (AccessTokenNotAvailableException é gerado):

  • prompt é definido como login: força o usuário a inserir suas credenciais nessa solicitação, negando o logon único.
  • loginHint é definido como peter@contoso.com: preenche previamente o campo nome de usuário/endereço de email da página de entrada do usuário para peter@contoso.com. Os aplicativos geralmente usam esse parâmetro durante a nova autenticação, já tendo extraído o nome de usuário de uma entrada anterior usando a declaração 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");
    });
}

O exemplo anterior pressupõe:

Para obter mais informações, consulte os seguintes recursos:

Personalizar opções ao usar um IAccessTokenProvider

Se ocorrer falha na obtenção de um token ao usar um IAccessTokenProvider, gerencie parâmetros adicionais para uma nova solicitação de token de acesso do provedor de identidade com os seguintes métodos uma ou mais vezes em uma nova instância de InteractiveRequestOptions:

No exemplo a seguir que tenta obter um token de acesso para o usuário, parâmetros adicionais são incluídos na solicitação de logon se a tentativa de obter um token falhar quando TryGetToken for chamado:

  • prompt é definido como login: força o usuário a inserir suas credenciais nessa solicitação, negando o logon único.
  • loginHint é definido como peter@contoso.com: preenche previamente o campo nome de usuário/endereço de email da página de entrada do usuário para peter@contoso.com. Os aplicativos geralmente usam esse parâmetro durante a nova autenticação, já tendo extraído o nome de usuário de uma entrada anterior usando a declaração 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);
}

O exemplo anterior pressupõe:

Para obter mais informações, consulte os seguintes recursos:

Logoff com uma URL de retorno personalizada

O exemplo a seguir faz logoff e retorna o usuário para o ponto de extremidade /goodbye:

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

Obter o caminho de logon das opções de autenticação

Obtenha o caminho de logon configurado de RemoteAuthenticationOptions:

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

O exemplo anterior pressupõe:

Classe AuthorizationMessageHandler personalizada

Estas diretrizes nesta seção são recomendadas para aplicativos cliente que fazem solicitações de saída para URIs que não estão dentro do URI base do aplicativo.

No exemplo a seguir, uma classe personalizada estende AuthorizationMessageHandler para uso como DelegatingHandler para um HttpClient. ConfigureHandler configura esse manipulador para autorizar solicitações HTTP de saída usando um token de acesso. O token de acesso só será anexado se pelo menos uma das URLs autorizadas for uma base do URI de solicitação (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" });
    }
}

No código anterior, os escopos example.read e example.write são exemplos genéricos que não se destinam a refletir escopos válidos para qualquer provedor específico.

No arquivo Program, CustomAuthorizationMessageHandler é registrado como um serviço transitório e é configurado como o DelegatingHandler para instâncias HttpResponseMessage de saída feitas por um serviço denominado HttpClient.

No exemplo a seguir, HttpClientFactoryServiceCollectionExtensions.AddHttpClient é uma extensão em Microsoft.Extensions.Http. Adicione o pacote a um aplicativo que ainda não o referenciou.

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.

builder.Services.AddTransient<CustomAuthorizationMessageHandler>();

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

Observação

No exemplo anterior, o CustomAuthorizationMessageHandlerDelegatingHandler é registrado como um serviço transitório para AddHttpMessageHandler. O registro transitório é recomendado para IHttpClientFactory, que gerencia seus próprios escopos de DI. Para obter mais informações, consulte os seguintes recursos:

Para uma solução hospedada do Blazor com base no Blazor WebAssemblymodelo de projeto, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) é atribuído ao HttpClient.BaseAddress por padrão.

O HttpClient configurado é usado para fazer solicitações autorizadas com o padrão try-catch. Onde o cliente é criado com CreateClient (pacote Microsoft.Extensions.Http), o HttpClient recebe instâncias que incluem tokens de acesso ao fazer solicitações para a API do servidor. Se o URI de solicitação for um URI relativo, como está no exemplo a seguir (ExampleAPIMethod), será combinado com o BaseAddress quando o aplicativo cliente fizer a solicitação:

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

Configurar AuthorizationMessageHandler

AuthorizationMessageHandler pode ser configurado com URLs autorizadas, escopos e uma URL de retorno usando o método ConfigureHandler. ConfigureHandler configura o manipulador para autorizar solicitações HTTP de saída usando um token de acesso. O token de acesso só será anexado se pelo menos uma das URLs autorizadas for uma base do URI de solicitação (HttpRequestMessage.RequestUri). Se o URI da solicitação for um URI relativo, será combinado com o BaseAddress.

No exemplo a seguir, AuthorizationMessageHandler configura um HttpClient no arquivo 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")
    });

No código anterior, os escopos example.read e example.write são exemplos genéricos que não se destinam a refletir escopos válidos para qualquer provedor específico.

Para uma Blazor solução hospedada do com base no Blazor WebAssemblymodelo de projeto, IWebAssemblyHostEnvironment.BaseAddress é atribuído ao seguinte por padrão:

HttpClient tipado

Um cliente tipado pode ser definido como aquele que lida com todas as preocupações de aquisição de HTTP e token em uma única classe.

WeatherForecastClient.cs:

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

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

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

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

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

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

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

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

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

No exemplo anterior, o tipo WeatherForecast é uma classe estática que contém dados de previsão do tempo. O espaço reservado {ASSEMBLY NAME} é o nome do assembly do aplicativo (por exemplo, using static BlazorSample.Data;).

No exemplo a seguir, HttpClientFactoryServiceCollectionExtensions.AddHttpClient é uma extensão em Microsoft.Extensions.Http. Adicione o pacote a um aplicativo que ainda não o referenciou.

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

Para uma solução hospedada do Blazor com base no Blazor WebAssemblymodelo de projeto, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) é atribuído ao HttpClient.BaseAddress por padrão.

Em um componente que busca dados meteorológicos:

@inject WeatherForecastClient Client

...

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

Configurar o manipulador HttpClient

O manipulador pode ser configurado ainda mais com ConfigureHandler para solicitações HTTP de saída.

No exemplo a seguir, HttpClientFactoryServiceCollectionExtensions.AddHttpClient é uma extensão em Microsoft.Extensions.Http. Adicione o pacote a um aplicativo que ainda não o referenciou.

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

No código anterior, os escopos example.read e example.write são exemplos genéricos que não se destinam a refletir escopos válidos para qualquer provedor específico.

Para uma Blazor solução hospedada do com base no Blazor WebAssemblymodelo de projeto, IWebAssemblyHostEnvironment.BaseAddress é atribuído ao seguinte por padrão:

Solicitações de API Web não autenticadas ou não autorizadas em um aplicativo com um cliente padrão seguro

Um aplicativo que normalmente usa um padrão HttpClient seguro também pode fazer solicitações de API Web não autenticadas ou não autorizadas configurando um HttpClient nomeado.

No exemplo a seguir, HttpClientFactoryServiceCollectionExtensions.AddHttpClient é uma extensão em Microsoft.Extensions.Http. Adicione o pacote a um aplicativo que ainda não o referenciou.

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

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

Para uma solução hospedada do Blazor com base no Blazor WebAssemblymodelo de projeto, IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress)) é atribuído ao HttpClient.BaseAddress por padrão.

O registro anterior é além do registro padrão HttpClient seguro existente.

Um componente cria o HttpClient do IHttpClientFactory (pacote Microsoft.Extensions.Http) para fazer solicitações não autenticadas ou não autorizadas:

@inject IHttpClientFactory ClientFactory

...

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

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

        ...
    }
}

Observação

O controlador na API do servidor, ExampleNoAuthenticationController no exemplo anterior, não está marcado com o [Authorize] atributo.

A decisão de usar um cliente seguro ou não como instância HttpClient padrão depende do desenvolvedor. Uma maneira de tomar essa decisão é considerar o número de pontos de extremidade autenticados e não autenticados que o aplicativo contata. Se a maioria das solicitações do aplicativo for para pontos de extremidade de API seguros, use a instância HttpClient autenticada como padrão. Caso contrário, registre a instância não autenticada HttpClient como o padrão.

Uma abordagem alternativa para usar o IHttpClientFactory é criar um cliente tipado para acesso não autenticado a pontos de extremidade anônimos.

Solicitar tokens de acesso adicionais

Os tokens de acesso podem ser obtidos manualmente chamando IAccessTokenProvider.RequestAccessToken. No exemplo a seguir, um escopo adicional é exigido por um aplicativo para o padrão HttpClient. O exemplo da MSAL (Biblioteca de Autenticação da Microsoft) configura o escopo com MsalProviderOptions:

No arquivo Program:

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

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

Os espaços reservados {CUSTOM SCOPE 1} e {CUSTOM SCOPE 2} no exemplo anterior são escopos personalizados.

Observação

A AdditionalScopesToConsent não consegue provisionar permissões de usuário delegadas para o Microsoft Graph por meio da interface de consentimento do Microsoft Entra ID quando um usuário usa pela primeira vez um aplicativo registrado no Microsoft Azure. Para obter mais informações, consulte Usar a API do Graph com o ASP.NET Core Blazor WebAssembly.

O método IAccessTokenProvider.RequestAccessToken cria uma sobrecarga que permite que um aplicativo provisione um token de acesso com um determinado conjunto de escopos.

Em um 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))
{
    ...
}

Os espaços reservados {CUSTOM SCOPE 1} e {CUSTOM SCOPE 2} no exemplo anterior são escopos personalizados.

AccessTokenResult.TryGetToken retorna:

  • true com o token para uso.
  • false se o token não for recuperado.

Compartilhamento de Recursos entre Origens (CORS)

Ao enviar credenciais (cabeçalhos e cookies de autorização) em solicitações CORS, o cabeçalho Authorization deve ser permitido pela política CORS.

A política a seguir inclui a configuração para:

  • Origens da solicitação (http://localhost:5000, https://localhost:5001).
  • Qualquer método (verbo).
  • Cabeçalhos Content-Type e Authorization. Para permitir um cabeçalho personalizado (por exemplo, x-custom-header), liste o cabeçalho ao chamar WithHeaders.
  • Credenciais definidas pelo código JavaScript do lado do cliente (propriedade credentials definida como include).
app.UseCors(policy => 
    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
        .AllowAnyMethod()
        .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, 
            "x-custom-header")
        .AllowCredentials());

Uma solução hospedada do Blazor com base no Blazor WebAssemblymodelo de projeto usa o mesmo endereço básico para os aplicativos cliente e servidor. O HttpClient.BaseAddress do aplicativo cliente é definido como um URI de builder.HostEnvironment.BaseAddress por padrão. A configuração do CORS não é necessária na configuração padrão de uma solução hospedada do Blazor. Aplicativos cliente adicionais que não são hospedados pelo projeto do servidor e não compartilham o endereço básico do aplicativo do servidor precisam de configuração CORS no projeto do servidor.

Para mais informações, confira Habilitar solicitações CORS (entre origens) no ASP.NET Core e o componente testador de solicitação HTTP do aplicativo de exemplo (Components/HTTPRequestTester.razor).

Tratar erros de solicitação de token

Quando um SPA (aplicativo de página única) autentica um usuário com o OIDC (OpenID Connect), o estado de autenticação é mantido localmente dentro do SPA e no IP (provedor Identity) na forma de uma sessão de cookie definida como resultado do usuário fornecer suas credenciais.

Os tokens que o IP emite para o usuário normalmente são válidos por curtos períodos de tempo, cerca de uma hora. Portanto, o aplicativo cliente sempre deve buscar novos tokens. Caso contrário, o usuário será desconectado depois que os tokens concedidos expirarem. Na maioria dos casos, os clientes OIDC são capazes de provisionar novos tokens sem exigir que o usuário se autentique novamente graças ao estado de autenticação ou à "sessão" que é mantida dentro do IP.

Há alguns casos em que o cliente não pode obter um token sem interação do usuário; por exemplo, quando, por algum motivo, o usuário faz logoff explicitamente do IP. Esse cenário ocorre se um usuário visita https://login.microsoftonline.com e faz logout. Nesses cenários, o aplicativo não sabe imediatamente que o usuário efetuou o logout. É possível que qualquer token que o cliente possua não seja mais válido. Além disso, o cliente não pode provisionar um novo token sem interação do usuário após a expiração do token atual.

Esses cenários não são específicos para a autenticação baseada em token, são a natureza dos SPAs. Um SPA que usa cookies também falhará ao chamar uma API de servidor se a autenticação do cookie for removida.

Quando um aplicativo executa chamadas de API para recursos protegidos, você deve estar ciente do seguinte:

  • Para provisionar um novo token de acesso para chamar a API, o usuário pode precisar se autenticar novamente.
  • Mesmo que o cliente tenha um token que parece ser válido, a chamada para o servidor pode falhar porque o token foi revogado pelo usuário.

Quando o aplicativo solicita um token, há dois resultados possíveis:

  • A solicitação é bem-sucedida e o aplicativo tem um token válido.
  • A solicitação falha e o aplicativo deve autenticar o usuário mais uma vez para obter um novo token.

Quando uma solicitação de token falha, você precisa decidir se deseja salvar qualquer estado atual antes de fazer um redirecionamento. Existem várias abordagens para armazenar o estado com níveis crescentes de complexidade:

  • Armazene o estado da página atual no armazenamento de sessão. Durante o OnInitializedAsyncmétodo de ciclo de vida (OnInitializedAsync), verifique se o estado pode ser restaurado antes de continuar.
  • Adicione um parâmetro de cadeia de caracteres de consulta e use-o como uma maneira de sinalizar ao aplicativo que ele precisa restaurar novamente o estado salvo antes.
  • Adicione um parâmetro de cadeia de caracteres de consulta com um identificador exclusivo para armazenar dados no armazenamento de sessão sem arriscar colisões com outros itens.

Salvar o estado do aplicativo antes de uma operação de autenticação com armazenamento de sessão

O exemplo a seguir mostra como:

  • Preserve o estado antes de redirecionar para a página de logon.
  • Recupere o estado anterior após a autenticação usando um parâmetro de cadeia de caracteres 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; }
    }
}

Salvar o estado do aplicativo antes de uma operação de autenticação com armazenamento de sessão e um contêiner de estado

Durante uma operação de autenticação, há casos em que você deseja salvar o estado do aplicativo antes que o navegador seja redirecionado para o IP. Esse pode ser o caso quando você está usando um contêiner de estado e deseja restaurar o estado depois da autenticação. Você pode usar um objeto de estado de autenticação personalizada para preservar o estado específico do aplicativo ou uma referência a ele e restaurar esse estado após a conclusão bem-sucedida da operação de autenticação. O exemplo a seguir demonstra a abordagem.

Uma classe de contêiner de estado é criada no aplicativo com propriedades para manter os valores de estado do aplicativo. No exemplo a seguir, o contêiner é usado para manter o valor do contador do modelo de projeto Blazor no componenteCounter (Counter.razor). Os métodos para serializar e desserializar o contêiner são baseados em 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;
    }
}

O componente Counter usa o contêiner de estado para manter o valor currentCount fora do 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;
    }
}

Crie um ApplicationAuthenticationState com base em RemoteAuthenticationState. Forneça uma propriedade Id, que serve como um identificador para o estado armazenado 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; }
}

O componente Authentication (Authentication.razor) salva e recupera o estado do aplicativo usando o armazenamento de sessão local com os métodos StateContainer de serialização e desserialização, GetStateForLocalStorage e 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);
            }
        }
    }
}

Este exemplo usa o Microsoft Entra (ME-ID) para a autenticação. No arquivo Program:

  • O ApplicationAuthenticationState é configurado como o tipo RemoteAuthenticationState da MSAL (Biblioteca de Autenticação da Microsoft).
  • O contêiner de estado é registrado no contêiner de serviço.
builder.Services.AddMsalAuthentication<ApplicationAuthenticationState>(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});

builder.Services.AddSingleton<StateContainer>();

Personalizar rotas de aplicativo

Por padrão, a biblioteca Microsoft.AspNetCore.Components.WebAssembly.Authentication usa as rotas mostradas na tabela a seguir para representar diferentes estados de autenticação.

Rota Finalidade
authentication/login Aciona uma operação de entrada.
authentication/login-callback Processa o resultado de qualquer operação de entrada.
authentication/login-failed Exibe mensagens de erro quando a operação de entrada falha por algum motivo.
authentication/logout Aciona uma operação de saída.
authentication/logout-callback Processa o resultado de uma operação de saída.
authentication/logout-failed Exibe mensagens de erro quando a operação de saída falha por algum motivo.
authentication/logged-out Indica que o usuário fez logoff com êxito.
authentication/profile Aciona uma operação para editar o perfil do usuário.
authentication/register Aciona uma operação para registrar um novo usuário.

As rotas mostradas na tabela anterior são configuráveis por meio de RemoteAuthenticationOptions<TRemoteAuthenticationProviderOptions>.AuthenticationPaths. Ao definir opções para fornecer rotas personalizadas, confirme se o aplicativo tem uma rota que manipula cada caminho.

No exemplo a seguir, todos os caminhos são prefixados com /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; }
}

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

Se o requisito chamar caminhos completamente diferentes, defina as rotas conforme descrito antes e renderize o RemoteAuthenticatorView com um parâmetro de ação explícito:

@page "/register"

<RemoteAuthenticatorView Action="RemoteAuthenticationActions.Register" />

Você poderá dividir a interface do usuário em páginas diferentes se optar por fazer isso.

Personalizar a interface do usuário de autenticação

RemoteAuthenticatorView inclui um conjunto padrão de fragmentos de interface do usuário para cada estado de autenticação. Cada estado pode ser personalizado passando um RenderFragment personalizado. Para personalizar o texto exibido durante o processo de logon inicial, é possível alterar o RemoteAuthenticatorView da seguinte maneira.

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

O RemoteAuthenticatorView tem um fragmento que pode ser usado por rota de autenticação mostrada na tabela a seguir.

Rota 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 o usuário

Os usuários associados ao aplicativo podem ser personalizados.

Personalizar o usuário com uma declaração de conteúdo

No exemplo a seguir, os usuários autenticados do aplicativo recebem uma declaração amr para cada um dos métodos de autenticação. A declaração amr identifica como o sujeito do token foi autenticado no Microsoft Identity Platform v1.0 nas declarações de conteúdo. O exemplo usa uma classe de conta de usuário personalizada com base em RemoteUserAccount.

Crie uma classe que estenda a classe RemoteUserAccount. O exemplo a seguir define a propriedade AuthenticationMethod como a matriz do usuário de valores de propriedade amrJSON. AuthenticationMethod é preenchido automaticamente pela estrutura quando o usuário é autenticado.

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

Crie um alocador que estenda AccountClaimsPrincipalFactory<TAccount> para criar declarações dos métodos de autenticação do usuário armazenados em 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 o CustomAccountFactory para o provedor de autenticação em uso. Qualquer um dos seguintes registros é 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 de segurança e funções do ME-ID com uma classe de conta de usuário personalizada

Para visualizar outro exemplo que funciona com grupos de segurança e Funções de Administrador do ME-ID e uma classe de conta de usuário personalizada, confira ASP.NET Core Blazor WebAssembly com grupos e funções da ID do Microsoft Entra.

Pré-renderizar com autenticação

Não é possível fazer a pré-renderização de conteúdo que exija autenticação e autorização no momento. Depois de seguir as diretrizes em um dos tópicos do aplicativo de segurança do Blazor WebAssembly, siga estas instruções para criar um aplicativo que:

  • Pré-renderize caminhos para os quais a autorização não é necessária.
  • Não pré-renderize caminhos para os quais a autorização é necessária.

Para o arquivo Program do projeto Client, considere os registros de serviços comuns em um método separado (por exemplo, crie um método ConfigureCommonServices no projeto Client). Os serviços comuns são aqueles que o desenvolvedor registra para uso pelos projetos de cliente e servidor.

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

No arquivo Program:

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

builder.Services.AddScoped( ... );

ConfigureCommonServices(builder.Services);

await builder.Build().RunAsync();

No arquivo Program do projeto Server, registre os seguintes serviços adicionais e chame 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);

No projeto Server, no método Startup.ConfigureServices, registre os serviços adicionais a seguir e chame 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 mais informações sobre o provedor de autenticação do servidor de estrutura do Blazor (ServerAuthenticationStateProvider), confira Autenticação e autorizaçãoBlazor no ASP.NET Core.

No projeto Server, no arquivo Pages/_Host.cshtml, substitua o Component Tag Helper (<component ... />) pelo seguinte:

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

No exemplo anterior:

  • O espaço reservado {CLIENT APP ASSEMBLY NAME} é o nome do assembly do aplicativo cliente (por exemplo, BlazorSample.Client).
  • A verificação condicional para o segmento de linha /authentication:
    • Evita a pré-renderização (render-mode="WebAssembly") para caminhos de autenticação.
    • Pré-renderiza (render-mode="WebAssemblyPrerendered") para caminhos que não são de autenticação.

Opções para aplicativos hospedados e provedores de logon de terceiros

Ao autenticar e autorizar um aplicativo Blazor WebAssembly hospedado com um provedor de terceiros, há várias opções disponíveis para autenticar o usuário. Sua escolha depende do cenário.

Para saber mais, confira Persistir declarações e tokens adicionais de provedores externos no ASP.NET Core.

Autenticar usuários para chamar apenas APIs de terceiros protegidas

Autentique o usuário com um fluxo OAuth do lado do cliente no provedor de API de terceiros:

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

Neste cenário:

  • O servidor que hospeda o aplicativo não desempenha uma função.
  • As APIs no servidor não podem ser protegidas.
  • O aplicativo só pode chamar APIs de terceiros protegidas.

Autenticar usuários com um provedor de terceiros e chamar APIs protegidas no servidor host e em terceiros

Configure Identity com um provedor de logon de terceiros. Obtenha os tokens necessários para acesso à API de terceiros e armazene-os.

Quando um usuário faz logon, Identity coleta e atualiza tokens de acesso como parte do processo de autenticação. Nesse caso, há algumas abordagens disponíveis para fazer chamadas a APIs de terceiros.

Usar um token de acesso do servidor para recuperar o token de acesso de terceiros

Use o token de acesso gerado no servidor para recuperar o token de acesso de terceiros de um ponto de extremidade da API do servidor. Em seguida, use o token de acesso de terceiros para chamar recursos de API de terceiros direto em Identity no cliente.

Não recomendamos essa abordagem. Essa abordagem requer o tratamento do token de acesso de terceiros como se tivesse sido gerado para um cliente público. Em termos de OAuth, o aplicativo público não tem um segredo do cliente porque não é confiável armazenar segredos com segurança e o token de acesso é produzido para um cliente confidencial. Um cliente confidencial é um que tem um segredo do cliente e é considerado capaz de armazenar segredos com segurança.

  • O token de acesso de terceiros pode receber escopos adicionais para executar operações confidenciais com base no fato de que o terceiro emitiu o token para um cliente mais confiável.
  • Da mesma forma, os tokens de atualização não devem ser emitidos para um cliente que não seja confiável, pois isso dá ao cliente acesso ilimitado, a menos que outras restrições sejam colocadas em prática.

Fazer chamadas à API do cliente para a API do servidor para chamar APIs de terceiros

Faça uma chamada à API do cliente para a API do servidor. No servidor, recupere o token de acesso para o recurso de API de terceiros e emita qualquer chamada necessária.

Recomendamos essa abordagem. Embora essa abordagem exija um salto de rede extra pelo servidor para chamar uma API de terceiros, isso resulta em uma experiência mais segura:

  • O servidor pode armazenar tokens de atualização e garantir que o aplicativo não perca o acesso a recursos de terceiros.
  • O aplicativo não pode vazar tokens de acesso do servidor que podem conter permissões mais confidenciais.

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

A biblioteca de autenticação e os modelos de projeto do Blazor usam pontos de extremidade do OIDC (OpenID Connect) v1.0. Para usar um ponto de extremidade v2.0, configure a opção JwtBearerOptions.Authority do portador JWT. No exemplo a seguir, o ME-ID está configurado para v2.0 acrescentando um segmento v2.0 à propriedade Authority:

using Microsoft.AspNetCore.Authentication.JwtBearer;

...

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

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

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

Caso anexar um segmento à autoridade não seja apropriado para o provedor OIDC do aplicativo, como acontece com os provedores que não são ME-ID, defina a propriedade Authority diretamente. Defina a propriedade em JwtBearerOptions ou no arquivo de configurações do aplicativo (appsettings.json) com a chave Authority.

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.

Configurar e usar gRPC em componentes

Para configurar um aplicativo Blazor WebAssembly e usar a estrutura gRPC do ASP.NET Core:

Observação

A pré-renderização é habilitada por padrão em Aplicativos Web Blazor, portanto, você precisa levar em conta a renderização do componente primeiro do servidor e, em seguida, do cliente. Qualquer estado pré-renderizado deve fluir para o cliente para que possa ser reutilizado. Para mais informações, confira Pré-renderizar componentes Razor do ASP.NET Core.

Observação

A pré-renderização é habilitada por padrão em aplicativos Blazor WebAssembly hospedados, portanto, você precisa levar em conta a renderização do componente primeiro do servidor e, em seguida, do cliente. Qualquer estado pré-renderizado deve fluir para o cliente para que possa ser reutilizado. Para obter mais informações, confira Renderizar previamente e integrar os componentes Razor do 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);
});

Um componente no aplicativo cliente pode fazer chamadas gRPC usando o cliente 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 a propriedade, use Status.DebugException a Grpc.Net.Client, versão 2.30.0 ou posterior.

Para mais informações, confira gRPC Web em aplicativos gRPC do ASP.NET Core.

Substituir a implementação de AuthenticationService

As subseções a seguir explicam como substituir:

  • Qualquer implementação do JavaScript de AuthenticationService.
  • A Biblioteca de Autenticação da Microsoft para JavaScript (MSAL.js).

Substituir qualquer implementação de AuthenticationService em JavaScript

Crie uma biblioteca JavaScript para lidar com os detalhes da autenticação personalizada.

Aviso

As diretrizes nesta seção são um detalhe de implementação do padrão RemoteAuthenticationService<TRemoteAuthenticationState,TAccount,TProviderOptions>. O código TypeScript desta seção se aplica especificamente ao ASP.NET Core no .NET 7 e está sujeito a alterações sem aviso prévio nas versões futuras do 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 };
};

É possível importar a biblioteca removendo a tag <script> original e adicionando uma tag <script> que carrega a biblioteca personalizada. O exemplo a seguir demonstra a substituição da tag padrão <script> por uma que carrega uma biblioteca chamada CustomAuthenticationService.js da pasta wwwroot/js.

Em wwwroot/index.html, antes do script do Blazor (_framework/blazor.webassembly.js) e dentro da tag de fechamento </body>:

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

Para mais informações, confira AuthenticationService.ts no repositório do GitHub dotnet/aspnetcore.

Observação

Os links de documentação para a fonte de referência do .NET geralmente carregam o branch padrão do repositório, que representa o desenvolvimento atual da próxima versão do .NET. Para selecionar uma marca para uma versão específica, use a lista suspensa para Alternar branches ou marcas. Para saber mais, confira Como selecionar uma marca de versão do código-fonte do ASP.NET Core (dotnet/AspNetCore.Docs #26205).

Substituir a Biblioteca de Autenticação da Microsoft para JavaScript (MSAL.js)

Se um aplicativo exigir uma versão personalizada da Biblioteca de Autenticação da Microsoft para JavaScript (MSAL.js), siga estas etapas:

  1. Confirme se o sistema tem o SDK do .NET do desenvolvedor mais recente ou obtenha e instale o último SDK do desenvolvedor em SDK do .NET Core: instaladores e binários. A configuração de feeds NuGet internos não é necessária para esse cenário.
  2. Configure o repositório do GitHub dotnet/aspnetcore para desenvolvimento seguindo as instruções na documentação em Compilar ASP.NET Core da fonte. Crie e clone ou baixe um arquivo ZIP do dotnet/aspnetcorerepositório do GitHub.
  3. Abra o arquivo src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json e defina a versão desejada de @azure/msal-browser. Para obter uma lista de versões lançadas, visite o @azure/msal-browsersite do npm e selecione a guia Versões.
  4. Crie o projeto Authentication.Msal na pasta src/Components/WebAssembly/Authentication.Msal/src com o comando yarn build em um shell de comando.
  5. Se o aplicativo usa ativos compactados (Brotli/Gzip), compacte o arquivo Interop/dist/Release/AuthenticationService.js.
  6. Copie o arquivo AuthenticationService.js e versões compactadas (.br/.gz) do arquivo, se produzidas, da pasta Interop/dist/Release para a pasta publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal nos ativos publicados do aplicativo.

Passar opções de provedor personalizado

Defina uma classe para passar os dados para a biblioteca JavaScript subjacente.

Importante

A estrutura da classe deve corresponder ao que a biblioteca espera quando o JSON é serializado com System.Text.Json.

O exemplo a seguir demonstra uma classe ProviderOptions com atributos JsonPropertyName que correspondem às expectativas de uma biblioteca de provedores personalizados 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 as opções do provedor no sistema DI e configure os valores apropriados:

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

O exemplo anterior define URIs de redirecionamento com literais de cadeia de caracteres regulares. As seguintes alternativas estão disponíveis:

Recursos adicionais