Blazor Hybrid não herda o login do site: como autenticar o app de verdade
O app híbrido não enxerga o cookie do navegador. Entenda por que, como autenticar o app com token guardado no cofre do aparelho e como fazer o sair apagar o acesso de verdade.
A equipe reaproveitou os componentes Razor do portal web num app .NET MAUI com Blazor Hybrid. No site, o técnico de campo entra uma vez e fica logado por dias. No app, abre a tela de chamados e vê "não autorizado". A primeira ideia de alguém foi gravar usuário e senha num arquivo de preferências para "logar sozinho". A segunda foi copiar o cookie do site para dentro do WebView.
A resposta curta: o app híbrido é outro cliente, com outro armazenamento, e não herda a sessão do navegador. Ele precisa do próprio fluxo de login, feito pelo navegador do sistema com OIDC e PKCE, e o token que volta fica no cofre seguro do aparelho, nunca a senha. Os componentes Razor são os mesmos; quem diz quem está logado agora é o código nativo do app.
Por que o app não vê o cookie do site
No Blazor Hybrid, os componentes rodam dentro do processo .NET do app e desenham a interface num WebView embutido: WebView2 no Windows, o WebView do Android e o WKWebView no iOS e no macOS. Esse WebView tem o próprio armazenamento de cookies, separado do Chrome, do Edge ou do Safari que a pessoa usa no dia a dia. O cookie de sessão que o portal gravou no navegador simplesmente não existe ali.
Há uma segunda diferença, mais importante. No Blazor Server ou no WebAssembly, a autenticação é da aplicação web: o servidor emite o cookie e o navegador o reenvia. No Hybrid, a documentação da Microsoft trata a autenticação como responsabilidade do app nativo. É o código .NET que obtém um token, guarda, renova e informa aos componentes quem é o usuário por meio de um AuthenticationStateProvider. O WebView é só a tela.
Por isso copiar o cookie é uma gambiarra frágil: ele expira, depende de domínio e de atributos como SameSite, e obriga o app a manipular uma credencial do navegador que não foi emitida para ele. Gravar a senha é pior: ela fica no disco do aparelho, sobrevive à troca de senha no servidor só até a próxima falha e pode vazar em backup.
O desenho certo: o app como cliente OIDC
O app de chamados passa a ser um cliente registrado no provedor de identidade (Entra ID, Keycloak, Duende, Auth0, o que a empresa usar), do tipo cliente público, sem segredo embutido. O fluxo fica assim:
- O técnico toca em "Entrar". O app abre o navegador do sistema na página de login do provedor, com PKCE.
- Depois do login, o provedor redireciona para um esquema registrado pelo app, como
chamados://callback. - O app troca o código pelos tokens: um access token de vida curta e, se a política permitir, um refresh token.
- O refresh token vai para o
SecureStorage, que usa o Keychain no iOS e armazenamento protegido pelo sistema nas outras plataformas. - Um
AuthenticationStateProviderpróprio informa aos componentes quem entrou. As chamadas à API levam o access token no cabeçalhoAuthorization. - Sair apaga o token local e, quando o provedor oferece, revoga o refresh token no servidor.
Não escreva a troca de código e o PKCE na mão. Use uma biblioteca de cliente nativo mantida, como a MSAL.NET para Entra ID ou a IdentityModel.OidcClient para provedores OIDC em geral. O motivo de usar o navegador do sistema, e não um formulário de login dentro do WebView, está explicado no guia de login no app MAUI, e o de não embutir segredo está em app nativo não carrega o segredo do cliente.
O provedor de estado que os componentes enxergam
O exemplo abaixo mostra a parte que costuma faltar: a ponte entre o token que o app obteve e o [Authorize] e o AuthorizeView dos componentes. O ILoginNativo representa a sua integração com a biblioteca OIDC escolhida.
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
public interface ILoginNativo
{
Task<ResultadoLogin?> EntrarAsync();
Task<ResultadoLogin?> RenovarAsync(string refreshToken);
Task RevogarAsync(string refreshToken);
}
public record ResultadoLogin(string AccessToken, string? RefreshToken, ClaimsPrincipal Usuario);
public class EstadoDeLogin : AuthenticationStateProvider
{
private const string ChaveRefresh = "chamados_refresh_token";
private static readonly ClaimsPrincipal Anonimo = new(new ClaimsIdentity());
private readonly ILoginNativo _login;
private ClaimsPrincipal _usuario = Anonimo;
public string? AccessToken { get; private set; }
public EstadoDeLogin(ILoginNativo login) => _login = login;
public override Task<AuthenticationState> GetAuthenticationStateAsync()
=> Task.FromResult(new AuthenticationState(_usuario));
public async Task RestaurarAsync()
{
var refresh = await SecureStorage.Default.GetAsync(ChaveRefresh);
if (refresh is null) return;
var r = await _login.RenovarAsync(refresh);
if (r is null)
{
SecureStorage.Default.Remove(ChaveRefresh);
return;
}
await AplicarAsync(r);
}
public async Task EntrarAsync()
{
var r = await _login.EntrarAsync();
if (r is not null) await AplicarAsync(r);
}
public async Task SairAsync()
{
var refresh = await SecureStorage.Default.GetAsync(ChaveRefresh);
SecureStorage.Default.Remove(ChaveRefresh);
AccessToken = null;
_usuario = Anonimo;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
if (refresh is not null)
{
try { await _login.RevogarAsync(refresh); }
catch { /* sem rede: o token local já foi apagado */ }
}
}
private async Task AplicarAsync(ResultadoLogin r)
{
if (r.RefreshToken is not null)
await SecureStorage.Default.SetAsync(ChaveRefresh, r.RefreshToken);
AccessToken = r.AccessToken;
_usuario = r.Usuario;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
}
No MauiProgram.cs, registre o provedor uma vez e exponha a mesma instância como AuthenticationStateProvider:
builder.Services.AddAuthorizationCore();
builder.Services.AddSingleton<ILoginNativo, LoginOidc>();
builder.Services.AddSingleton<EstadoDeLogin>();
builder.Services.AddSingleton<AuthenticationStateProvider>(
sp => sp.GetRequiredService<EstadoDeLogin>());
Repare na ordem do SairAsync: primeiro apaga o token do disco e limpa o estado, depois tenta revogar no servidor. Se o aparelho estiver sem rede, o técnico ainda sai de verdade daquele aparelho. A revogação é o segundo passo, para que um token copiado antes também deixe de valer.
A API continua conferindo tudo
O AuthorizeView esconde botão; quem protege dado é a API. Cada chamada do app chega com o access token, e o backend valida assinatura, emissor, audiência e validade, exatamente como faria com qualquer outro cliente. Um app híbrido pode ser descompilado, e o código dos componentes está no pacote instalado. O guia sobre a identidade que o servidor ainda confere no Blazor Hybrid aprofunda esse lado.
Erros comuns
- Esperar que o login do site valha no app. O WebView tem armazenamento próprio, e a pessoa cai na tela de não autorizado sem entender por quê.
- Guardar a senha em
Preferencesou arquivo. A senha fica legível no aparelho e em backups, e a troca de senha no servidor não tira o acesso de quem copiou o arquivo. - Login com formulário próprio dentro do WebView. O app passa a ver a senha do provedor, o MFA e o gerenciador de senhas do sistema deixam de funcionar, e alguns provedores bloqueiam esse uso.
- Embutir o segredo do cliente no app. Qualquer um extrai o segredo do pacote; app nativo é cliente público e usa PKCE.
- Sair que só troca de tela. O refresh token continua no disco e o próximo que abrir o app entra com a conta anterior.
- Testar só no emulador já autenticado. O bug de primeiro acesso, de token expirado e de aparelho sem rede só aparece em instalação limpa.
Checklist
- O app é registrado no provedor como cliente público, com redirect para esquema próprio.
- O login abre o navegador do sistema, com PKCE, via biblioteca mantida.
- Nenhuma senha é gravada no aparelho; só o refresh token, no
SecureStorage. - Um
AuthenticationStateProviderpróprio alimenta[Authorize]eAuthorizeView. - A API valida o access token em toda chamada, sem confiar no que a tela esconde.
- Sair apaga o token local antes de qualquer chamada de rede e depois tenta revogar.
- Você testou instalação limpa, token vencido, conta desativada e aparelho sem rede.
Faça o teste agora no aparelho de homologação: entre no app, toque em sair, feche e abra de novo. A tela pede login, ou o refresh token continua no disco esperando o próximo que pegar o celular?
Referência: https://learn.microsoft.com/pt-br/aspnet/core/blazor/hybrid/security/