Blazor WebAssembly sem saber quem está logado: passe o estado sem abrir o cookie
Configure a serialização do estado de autenticação do servidor para o WebAssembly, mantenha o cookie HttpOnly, faça a API responder 401 em vez de redirecionar e trate a sessão que expira.
A página /minha-conta é pré-renderizada no servidor e mostra "Olá, Ana". Um segundo depois, quando o componente vira interativo no navegador, o mesmo cabeçalho troca para "Entrar". Na tentativa de consertar, alguém sugere tirar o HttpOnly do cookie de sessão para o código no navegador conseguir ler. O sintoma some e a sessão passa a estar ao alcance de qualquer script injetado na página.
A resposta curta: o componente WebAssembly não precisa do cookie; precisa de uma cópia das informações do usuário. Nos Blazor Web Apps do .NET 9 em diante, o servidor serializa o estado de autenticação na página com AddAuthenticationStateSerialization(), e o cliente o lê com AddAuthenticationStateDeserialization(). O cookie continua HttpOnly, o navegador o envia sozinho nas chamadas à API do mesmo domínio, e quem autoriza de verdade continua sendo o servidor.
Por que o WebAssembly não enxerga o login
No servidor, o ASP.NET Core lê o cookie a cada requisição, valida e monta um ClaimsPrincipal. O AuthenticationStateProvider do servidor entrega esse usuário aos componentes. No navegador, o código .NET roda dentro do WebAssembly, isolado como qualquer JavaScript: ele não recebe o cookie como dado, e um cookie HttpOnly é invisível para scripts por definição. Sem configuração, o provedor de autenticação do lado cliente não tem de onde tirar o usuário e responde "anônimo".
O HttpOnly existe exatamente para isso. Se um script malicioso entrar na página por um XSS, ele não consegue ler o cookie e mandar para fora. Tirar a proteção para o componente "ver" o login troca um problema de interface por uma falha de segurança, como explica o post sobre cookie HttpOnly.
Esse é o caso de autenticação de um problema maior: no modo Auto ou WebAssembly, nenhum estado do servidor atravessa sozinho para o navegador. Se você também está vendo buscas duplicadas ou serviços que perdem dados, leia o componente troca de lado e o estado fica para trás.
Como passar o estado do jeito certo
O exemplo é uma área do cliente com cookie de autenticação do ASP.NET Core, um projeto servidor Conta e um projeto cliente Conta.Client.
- No servidor, configure o cookie com
HttpOnly,SecureeSameSite, e faça as rotas de API responderem 401 em vez de redirecionar para o login. - Ative a serialização do estado de autenticação no registro dos componentes Razor.
- No cliente, ative a desserialização, a autorização e o estado de autenticação em cascata.
- Proteja as APIs no servidor com
RequireAuthorization, inclusive as que só o componente chama. - Trate o 401 no cliente mandando a pessoa para o login com recarga completa.
- Teste com refresh, aba anônima e sessão expirada, não só com o debug que já estava logado.
O servidor
// Conta/Program.cs
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents()
.AddAuthenticationStateSerialization();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.LoginPath = "/entrar";
o.Cookie.HttpOnly = true;
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
o.Cookie.SameSite = SameSiteMode.Lax;
o.ExpireTimeSpan = TimeSpan.FromHours(8);
o.SlidingExpiration = true;
o.Events.OnRedirectToLogin = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api"))
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
}
ctx.Response.Redirect(ctx.RedirectUri);
return Task.CompletedTask;
};
});
builder.Services.AddAuthorization();
O evento OnRedirectToLogin resolve um problema que confunde muita gente: por padrão, o cookie de autenticação responde a uma requisição não autenticada com um redirecionamento 302 para a página de login. Para uma chamada de API feita pelo componente, isso significa receber o HTML da tela de login no lugar do JSON esperado, e o erro aparece como falha de desserialização, não como "sessão expirada".
Por padrão, a serialização envia apenas o nome e os papéis do usuário. Se a interface precisar de outras claims, existe a opção SerializeAllClaims, mas pense antes: tudo o que é serializado vai no HTML da página e fica legível no navegador. Envie o que a tela precisa mostrar, não o que o servidor sabe.
O cliente
// Conta.Client/Program.cs
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization();
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
Com isso, <AuthorizeView>, o atributo [Authorize] e o parâmetro em cascata Task<AuthenticationState> funcionam no componente WebAssembly com o mesmo usuário que o servidor viu. Se o seu projeto ainda está no .NET 8, o template com autenticação individual gera uma classe equivalente (um provedor que persiste o estado na página); a ideia é a mesma, com mais código para manter.
A API continua decidindo
// Conta/Program.cs (continuação)
app.MapGet("/api/conta/enderecos", async (ClaimsPrincipal user, EnderecoRepositorio repo) =>
{
var id = user.FindFirstValue(ClaimTypes.NameIdentifier);
return id is null ? Results.Unauthorized() : Results.Ok(await repo.DoClienteAsync(id));
}).RequireAuthorization();
O estado serializado serve para a interface: mostrar o nome, esconder o menu de administração de quem não é administrador. Ele não autoriza nada. Um usuário pode alterar a memória do próprio navegador e fazer o componente acreditar que tem o papel "Admin"; a API vai continuar recusando, porque ela lê o cookie, não o componente.
Quando a sessão expira com a página aberta
O estado serializado é uma foto do momento em que a página foi renderizada. Se a pessoa deixa a aba aberta além das 8 horas do cookie, o componente continua dizendo "Olá, Ana", mas a próxima chamada à API volta 401. Trate isso num ponto central:
try
{
enderecos = await Http.GetFromJsonAsync<List<Endereco>>("api/conta/enderecos");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
Navigation.NavigateTo("/entrar?returnUrl=/minha-conta", forceLoad: true);
}
O forceLoad: true faz o navegador recarregar a página de verdade, passando pelo servidor, que vai renderizar a tela de login e, depois do login, um estado de autenticação novo.
Erros comuns
- Tirar o
HttpOnlydo cookie para o componente ler. Qualquer XSS passa a conseguir roubar a sessão inteira. - Copiar o token para o
localStorage"só para o WASM usar". É o mesmo problema com outro nome: um script injetado lê e exfiltra. - Esconder o botão no cliente e não proteger a API. O menu some, mas a rota responde a quem chamar diretamente.
- Deixar a API redirecionar para o login. O componente recebe HTML no lugar de JSON e mostra um erro genérico em vez de pedir login.
- Serializar todas as claims sem olhar quais são. CPF, ids internos ou dados de perfil vão parar no HTML de cada página.
- Testar só com o circuito do debug. O desenvolvedor nunca vê o momento em que o WebAssembly assume e o estado não veio.
Checklist
- O cookie de sessão continua
HttpOnly,Securee comSameSitedefinido. - O servidor usa
AddAuthenticationStateSerializatione o cliente,AddAuthenticationStateDeserialization(ou o equivalente do template no .NET 8). - Toda rota de API usada pelo componente tem
RequireAuthorizatione filtra pelo usuário no servidor. - Requisições a
/apisem sessão recebem 401, não 302. - O cliente trata 401 mandando para o login com recarga completa.
- Só as claims que a interface usa são serializadas.
- Você testou refresh, aba anônima e sessão expirada.
Abra o seu app numa aba, apague o cookie de sessão pelo DevTools e clique em algo que chama a API: o componente manda você para o login ou continua dizendo que você está logado e mostra um erro genérico?
Referência: https://jonhilton.net/blazor-share-auth-state/