Programação Por Equipe DevBras

Capa do artigo: Blazor Hybrid com login: onde guardar o token e quem confere a identidade

Blazor Hybrid com login: onde guardar o token e quem confere a identidade

Monte a autenticação de um app Blazor Hybrid com o token no SecureStorage, um AuthenticationStateProvider próprio e a API validando cada chamada.

O app de pedidos da distribuidora abre, mostra a tela de login feita em Razor e, depois do clique, aparece o menu com o nome do vendedor. Parece seguro. Aí alguém chama a API de pedidos com o Postman, sem token, e recebe a lista inteira. A resposta curta: no Blazor Hybrid a interface roda dentro do app, mas a identidade continua sendo provada para a API em cada chamada. O app guarda o token num lugar protegido do aparelho, informa ao Blazor quem está logado para montar a tela, e a API valida o token como se o pedido viesse de qualquer lugar, porque pode vir.

O exemplo deste guia é esse app: .NET MAUI com Blazor Hybrid, chamado Vendas, consumindo https://api.distribuidora.exemplo/pedidos. O login em si (abrir o navegador do sistema e receber o código) está em login no app MAUI pelo navegador do sistema. Aqui o foco é o que acontece depois: onde o token mora, como o Blazor sabe quem entrou e o que a API confere.

Por que o login na tela não protege nada

No Blazor Server, existe um servidor que segura a sessão. No Blazor WebAssembly, existe um navegador com cookies e regras de origem. No Blazor Hybrid, não há nenhum dos dois: os componentes Razor rodam no processo do app, desenhados num WebView, e o "estado de autenticação" é só um objeto em memória que você mesmo preenche.

Isso traz duas consequências. Primeiro, [Authorize] e AuthorizeView no app só decidem o que mostrar; quem tem o pacote instalado pode, em tese, alterar o app ou chamar a API direto. Segundo, o cookie do site web não vem junto. Se a pessoa está logada no portal no navegador, o app não sabe disso, como explica o post sobre o cookie que não é herdado.

A documentação da Microsoft sobre segurança no Blazor Hybrid diz o mesmo com outras palavras: a autenticação é feita pelo app nativo, e o Blazor recebe o resultado por meio de um AuthenticationStateProvider.

Como montar a autenticação no app

  1. Obtenha o token pelo navegador do sistema, com PKCE, nunca com formulário de senha dentro do WebView.
  2. Guarde o token no SecureStorage, que usa o Keychain no iOS e armazenamento criptografado com chave do Keystore no Android. Nunca em Preferences, arquivo comum ou localStorage do WebView.
  3. Crie um AuthenticationStateProvider que lê o token, monta o usuário e avisa o Blazor quando a pessoa entra ou sai.
  4. Anexe o token a toda chamada com um DelegatingHandler, para nenhum componente esquecer.
  5. Valide na API assinatura, emissor, audiência e prazo, em todo endpoint que não seja público de propósito.

O provedor de estado do app fica assim:

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

public class EstadoDeLogin : AuthenticationStateProvider
{
    const string Chave = "access_token";
    static readonly AuthenticationState Anonimo =
        new(new ClaimsPrincipal(new ClaimsIdentity()));

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = await SecureStorage.Default.GetAsync(Chave);
        if (string.IsNullOrEmpty(token))
            return Anonimo;

        var nome = await SecureStorage.Default.GetAsync("nome") ?? "";
        var identidade = new ClaimsIdentity(
            new[] { new Claim(ClaimTypes.Name, nome) },
            authenticationType: "oidc");
        return new AuthenticationState(new ClaimsPrincipal(identidade));
    }

    public async Task EntrarAsync(string token, string nome)
    {
        await SecureStorage.Default.SetAsync(Chave, token);
        await SecureStorage.Default.SetAsync("nome", nome);
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public Task SairAsync()
    {
        SecureStorage.Default.Remove(Chave);
        SecureStorage.Default.Remove("nome");
        NotifyAuthenticationStateChanged(Task.FromResult(Anonimo));
        return Task.CompletedTask;
    }
}

Repare que o nome exibido serve só para a tela. Nenhuma decisão de permissão do app deveria depender de claims lidas localmente sem validação; quem decide é a API.

O handler que anexa o token e o registro no MauiProgram.cs:

using System.Net.Http.Headers;

public class TokenHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken ct)
    {
        var token = await SecureStorage.Default.GetAsync("access_token");
        if (!string.IsNullOrEmpty(token))
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, ct);
    }
}

// MauiProgram.cs
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<EstadoDeLogin>();
builder.Services.AddScoped<AuthenticationStateProvider>(
    sp => sp.GetRequiredService<EstadoDeLogin>());
builder.Services.AddTransient<TokenHandler>();
builder.Services.AddHttpClient("api", c =>
        c.BaseAddress = new Uri("https://api.distribuidora.exemplo/"))
    .AddHttpMessageHandler<TokenHandler>();

O AuthorizeRouteView e o CascadingAuthenticationState nas rotas funcionam como no Blazor web, agora alimentados pelo seu provedor.

Um detalhe que pega muita gente: quando a API responde 401, o token venceu ou foi revogado. O app deve chamar SairAsync (ou tentar a renovação, se você usa refresh token) e mandar a pessoa para o login, em vez de mostrar uma tela vazia com o nome dela no topo.

O que a API precisa conferir

Do lado do servidor, a API ASP.NET Core não sabe nem quer saber que o pedido veio de um app Hybrid. Ela valida o JWT:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.Authority = "https://login.distribuidora.exemplo/";
        o.Audience = "api-pedidos";
    });
builder.Services.AddAuthorization();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/pedidos", (ClaimsPrincipal user) => ListarDoVendedor(user))
   .RequireAuthorization();

Com Authority, o middleware baixa as chaves públicas do provedor e confere assinatura, emissor e validade; com Audience, recusa token emitido para outra aplicação. E o filtro de dados usa a identidade do token (ListarDoVendedor), não um vendedorId mandado pelo app na URL. Se o prazo do token for longo, veja JWT que expira para o desenho com renovação.

Erros comuns

  • Guardar o token no localStorage do WebView. Qualquer script carregado na página lê, e o armazenamento pode ser limpo pelo sistema. O token some ou vaza, às vezes os dois.
  • Usar Preferences para "simplificar". No Android, isso vira um arquivo XML em texto. Em aparelho com root ou backup sem criptografia, o token sai inteiro.
  • Embutir client secret ou senha de banco no app. O pacote é descompilável. Tudo que está nele é público para quem se der ao trabalho.
  • Confiar no AuthorizeView como proteção. Ele esconde o botão. A rota da API continua aberta se não tiver RequireAuthorization.
  • Sair sem apagar o token. O botão "sair" troca a tela, mas o próximo pedido ainda leva o token antigo, e a próxima pessoa no aparelho compartilhado entra como a anterior.
  • Aceitar o vendedor pela URL. Trocar ?vendedor=12 por 13 mostra os pedidos de outra pessoa, mesmo com token válido.

Checklist

  • Login feito no navegador do sistema, sem senha digitada em componente Razor.
  • Token no SecureStorage, removido no "sair".
  • AuthenticationStateProvider notifica a mudança ao entrar e ao sair.
  • Todas as chamadas passam pelo HttpClient com o TokenHandler.
  • API com Authority e Audience configurados e RequireAuthorization nas rotas privadas.
  • Filtro de dados pela identidade do token, nunca por parâmetro do cliente.
  • Pacote do app sem segredo: procure por "secret" e "password" no código e nos arquivos de configuração.

Faça o teste que separa tela de segurança: entre no app, copie o token de um log de depuração, saia da conta e chame /pedidos com ele pelo terminal. Se a API ainda responde, o "sair" só mudou a tela. E a pergunta para responder olhando o seu código: em que arquivo do aparelho esse token está gravado agora?

Avalie este artigo

Referência: https://learn.microsoft.com/pt-br/aspnet/core/blazor/hybrid/security/