Programação Por Equipe DevBras

Capa do artigo: Blazor WebAssembly sem saber quem está logado: passe o estado sem abrir o cookie

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.

  1. No servidor, configure o cookie com HttpOnly, Secure e SameSite, e faça as rotas de API responderem 401 em vez de redirecionar para o login.
  2. Ative a serialização do estado de autenticação no registro dos componentes Razor.
  3. No cliente, ative a desserialização, a autorização e o estado de autenticação em cascata.
  4. Proteja as APIs no servidor com RequireAuthorization, inclusive as que só o componente chama.
  5. Trate o 401 no cliente mandando a pessoa para o login com recarga completa.
  6. 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 HttpOnly do 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, Secure e com SameSite definido.
  • O servidor usa AddAuthenticationStateSerialization e o cliente, AddAuthenticationStateDeserialization (ou o equivalente do template no .NET 8).
  • Toda rota de API usada pelo componente tem RequireAuthorization e filtra pelo usuário no servidor.
  • Requisições a /api sem 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?

Avalie este artigo

Referência: https://jonhilton.net/blazor-share-auth-state/