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
- Obtenha o token pelo navegador do sistema, com PKCE, nunca com formulário de senha dentro do WebView.
- Guarde o token no
SecureStorage, que usa o Keychain no iOS e armazenamento criptografado com chave do Keystore no Android. Nunca emPreferences, arquivo comum oulocalStoragedo WebView. - Crie um
AuthenticationStateProviderque lê o token, monta o usuário e avisa o Blazor quando a pessoa entra ou sai. - Anexe o token a toda chamada com um
DelegatingHandler, para nenhum componente esquecer. - 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
localStoragedo 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
Preferencespara "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
AuthorizeViewcomo proteção. Ele esconde o botão. A rota da API continua aberta se não tiverRequireAuthorization. - 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=12por13mostra 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". AuthenticationStateProvidernotifica a mudança ao entrar e ao sair.- Todas as chamadas passam pelo
HttpClientcom oTokenHandler. - API com
AuthorityeAudienceconfigurados eRequireAuthorizationnas 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?
Referência: https://learn.microsoft.com/pt-br/aspnet/core/blazor/hybrid/security/