Login com Google no Blazor Web App: a sessão nasce num endpoint HTTP, não no circuito
Configure o login com Google num Blazor Web App com validação no servidor, cookie de sessão criado num endpoint HTTP e nenhum segredo no projeto WebAssembly.
O botão do Google aparece no Blazor, a pessoa escolhe a conta, um componente recebe o e-mail via JavaScript interop e o código grava "usuário logado: ana@exemplo.com" no estado do circuito. Funciona na demo. Só que o e-mail veio do navegador, e qualquer pessoa com o console aberto pode chamar o mesmo método do .NET com o e-mail que quiser.
A resposta curta: no Blazor, quem cria a sessão é um endpoint HTTP do servidor, depois de validar a resposta do Google. O caminho mais simples é o handler AddGoogle do ASP.NET Core, que faz o fluxo de código no servidor e grava um cookie. Se você usa o botão do Google Identity Services, envie o ID token para um endpoint que confere assinatura, emissor, audiência e expiração, e só então chame SignInAsync. O componente nunca decide sozinho quem está logado.
Por que o Blazor engana: parece servidor, mas o navegador está no meio
Num componente com renderização interativa no servidor, o código C# roda no servidor, e isso dá a sensação de que tudo que chega ali é confiável. Não é. Os eventos e os argumentos de interop vêm do navegador por uma conexão SignalR. Um valor passado por JavaScript tem a mesma confiabilidade de um campo de formulário: nenhuma.
Há um segundo detalhe que pega muita gente. Dentro de um circuito interativo não existe uma requisição HTTP em andamento, então não há resposta onde gravar um cookie. Chamar HttpContext.SignInAsync de dentro de um componente interativo não funciona como esperado. E chamar um endpoint de login com HttpClient a partir do componente server é pior: quem recebe o cookie é o HttpClient no servidor, não o navegador da pessoa.
No modo WebAssembly, o código C# roda no navegador. Tudo que está no projeto cliente, inclusive o appsettings.json dentro de wwwroot, é baixado por qualquer visitante. Client secret ali é público. Se você usa o modo Auto, entenda como o estado de autenticação passa de um lado para o outro em estado de autenticação no Blazor Auto.
Como configurar, passo a passo
O exemplo: o portal de clientes da Loja Exemplo, um Blazor Web App em .NET 8 com renderização interativa no servidor, em https://portal.lojaexemplo.com.br.
- Crie um client OAuth do tipo aplicativo web no console do Google e registre o redirect
https://portal.lojaexemplo.com.br/signin-google, que é o caminho padrão do handler. Para desenvolvimento, registre também o endereço HTTPS local com a porta exata. - Guarde o segredo fora do código:
dotnet user-secretsno desenvolvimento e variável de ambienteAuthentication__Google__ClientSecretem produção. - Configure cookie e Google no
Program.csdo projeto servidor. - Crie endpoints de login e logout como rotas HTTP comuns, não como métodos de componente.
- Proteja páginas com
[Authorize]e useAuthorizeViewsó para mostrar ou esconder elementos. - Identifique o usuário pelo
subdo Google, não pelo e-mail, que pode mudar.
O Program.cs com o handler do Google
using System.Security.Claims;
using Google.Apis.Auth;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.ExpireTimeSpan = TimeSpan.FromHours(8);
})
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["Authentication:Google:ClientId"]!;
options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]!;
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapGet("/login", () => Results.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
[GoogleDefaults.AuthenticationScheme]));
app.MapPost("/logout", async (HttpContext ctx) =>
{
await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Results.LocalRedirect("/");
});
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.Run();
O pacote do handler é o Microsoft.AspNetCore.Authentication.Google. O botão "Entrar" do layout é um link comum para /login, com recarga completa da página. O handler redireciona para o Google, recebe o code em /signin-google, troca pelo token no servidor e grava o cookie. Quando a página volta, o circuito novo nasce já autenticado. O RedirectUri fixo evita redirecionamento aberto; se quiser voltar para a página de origem, valide o destino como em URL aberta não é redirecionar.
Se você usa o botão do Google Identity Services
O botão entrega ao JavaScript um ID token (o campo credential). Esse token é só uma alegação até o servidor conferir. Crie um endpoint que valida com a biblioteca oficial, do pacote Google.Apis.Auth:
app.MapPost("/auth/google-token", async (GoogleTokenRequest body, HttpContext ctx, IConfiguration config) =>
{
GoogleJsonWebSignature.Payload payload;
try
{
payload = await GoogleJsonWebSignature.ValidateAsync(body.Credential,
new GoogleJsonWebSignature.ValidationSettings
{
Audience = [config["Authentication:Google:ClientId"]!]
});
}
catch (InvalidJwtException)
{
return Results.Unauthorized();
}
if (!payload.EmailVerified) return Results.Unauthorized();
var identity = new ClaimsIdentity(
[
new Claim(ClaimTypes.NameIdentifier, payload.Subject),
new Claim(ClaimTypes.Email, payload.Email),
new Claim(ClaimTypes.Name, payload.Name ?? payload.Email)
], CookieAuthenticationDefaults.AuthenticationScheme);
await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
return Results.NoContent();
});
// no fim do Program.cs, depois de app.Run()
record GoogleTokenRequest(string Credential);
O ValidateAsync confere a assinatura com as chaves públicas do Google, o emissor e a expiração; a audiência é conferida porque você informou o client id. Sem a audiência, um token emitido para outro site seria aceito no seu. O endpoint exige corpo JSON, o que impede um formulário de outro site de fazer o login por você.
A chamada parte do navegador, nunca do C#, para que o cookie fique no navegador:
// wwwroot/js/login-google.js, usado como callback do botão do Google
window.entrarComGoogle = async (resposta) => {
const r = await fetch('/auth/google-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: resposta.credential }),
credentials: 'same-origin'
});
if (!r.ok) {
alert('Não foi possível entrar. Tente de novo.');
return;
}
window.location.assign('/');
};
O location.assign recarrega a página, e o circuito novo enxerga o cookie. Os detalhes de validação do ID token, independentes de linguagem, estão em conferir o token do Google no servidor.
Erros comuns
- Gravar o usuário a partir do e-mail que veio por interop. Qualquer pessoa chama o mesmo método com outro e-mail e entra na conta de outro cliente.
- Chamar o endpoint de login com
HttpClientnum componente server. O cookie é gravado no servidor, o navegador continua deslogado, e a equipe perde horas procurando o motivo. - Colocar o client secret no projeto cliente. O arquivo vai para
wwwroote é baixado por qualquer visitante. - Validar o ID token sem audiência. Tokens emitidos para outros aplicativos passam a abrir sessão no seu.
- Usar o e-mail como chave do usuário. Se a pessoa trocar o e-mail da conta, vira outro cadastro; use o
sub. - Proteger página só com
AuthorizeView. Ele esconde marcação; a rota continua acessível sem[Authorize].
Checklist
- A sessão só é criada em endpoint HTTP, depois de validação no servidor.
- Nenhum componente cria sessão a partir de valor recebido do navegador.
- O client secret está em user-secrets ou variável de ambiente, nunca no projeto cliente.
- O redirect
/signin-googleestá registrado com o domínio e a porta exatos. - A validação do ID token informa a audiência.
- O usuário é identificado pelo
sub. - Um token expirado ou de outro client id é recusado com 401.
Procure no seu projeto todas as chamadas a SignInAsync: alguma delas cria a sessão sem uma validação do Google no servidor logo antes?