Login com Facebook no Vue: como transformar o token em sessão no servidor
O botão do Facebook só entrega um token ao navegador. Veja como o backend confere esse token, cria o usuário pelo ID certo e abre uma sessão própria com cookie.
O componente de login do Facebook terminou, a foto apareceu no canto da tela e o Vue mandou para a API um JSON com name, email e id. A API criou a conta e devolveu um cookie. Funciona na demo e está aberto para qualquer pessoa: basta mandar o mesmo JSON com o e-mail de outra pessoa e você entra como ela. O botão não cria sessão no seu servidor. Ele entrega um access token ao navegador. Quem cria a sessão é o backend, depois de conferir esse token direto com o Facebook.
A resposta curta: o front manda só o access token. O servidor chama o endpoint debug_token da Graph API com o token do app, confere se o token é válido e foi emitido para o seu app_id, busca o perfil com esse token e só então grava o usuário pelo ID do Facebook e emite o cookie da sua aplicação. O app secret nunca sai do servidor.
Por que o perfil que o Vue recebeu não prova nada
Tudo o que roda no navegador está nas mãos de quem usa o navegador. O objeto que o SDK JavaScript devolve pode ser trocado no console, interceptado por uma extensão ou simplesmente forjado com curl. A sua API não tem como distinguir um JSON que veio do SDK de um JSON digitado à mão.
O access token é diferente porque o Facebook consegue dizer se ele é legítimo. Mas há uma pegadinha: um token válido não basta, ele precisa ter sido emitido para o seu app. Se você só chamar /me com o token recebido, um atacante pode usar um token que ele obteve legitimamente em outro app qualquer, e a chamada funciona. É por isso que a checagem do app_id no debug_token é o coração do fluxo.
Esse padrão é o mesmo de qualquer provedor que entrega token ao front. No Google, o equivalente é conferir o ID token, como mostramos em conferir o token do Google no servidor. Se você estiver começando do zero e o provedor suportar, o fluxo de Authorization Code com PKCE descrito em login no Vue com PKCE evita boa parte desse trabalho. Aqui o foco é o cenário comum de quem já usa o SDK do Facebook.
Como o fluxo fica do botão até o cookie
O exemplo que atravessa o texto é uma loja chamada Bazar Azul: front em Vue, API em ASP.NET Core, login por Facebook ou por e-mail e senha.
- O Vue chama
FB.loginpedindopublic_profileeemail. - Se a pessoa cancelar, o fluxo para ali, sem chamar a API.
- Se conectar, o Vue envia apenas
accessTokenparaPOST /api/auth/facebook. - A API chama
debug_tokenusando o token do app (APP_ID|APP_SECRET), guardado em configuração do servidor. - A API exige
is_validverdadeiro eapp_idigual ao seu. - A API busca
id,nameeemailem/me, comappsecret_proof. - A API grava ou atualiza o usuário pelo ID do Facebook e emite o cookie de sessão próprio, com HttpOnly.
O lado do Vue: só o token sai do navegador
// src/auth/facebook.js
function loginComFacebook() {
return new Promise((resolve, reject) => {
window.FB.login((response) => {
if (response.status !== 'connected' || !response.authResponse) {
reject(new Error('login-cancelado'));
return;
}
resolve(response.authResponse.accessToken);
}, { scope: 'public_profile,email' });
});
}
export async function entrarComFacebook() {
const accessToken = await loginComFacebook();
const resp = await fetch('/api/auth/facebook', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessToken })
});
if (!resp.ok) {
throw new Error('falha-no-login');
}
return resp.json();
}
Repare no que não está ali: nome, e-mail e foto não vão para a API. O componente pode mostrar o que quiser na tela, mas o servidor vai buscar esses dados por conta própria. O token também não vai para o localStorage; depois do login ele não serve para mais nada no front, porque a sessão passa a ser o cookie.
O lado do servidor: conferir, buscar e abrir a sessão
// Program.cs (trecho)
builder.Services.AddHttpClient();
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.Cookie.HttpOnly = true;
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
o.Cookie.SameSite = SameSiteMode.Lax;
});
app.MapPost("/api/auth/facebook", async (
FacebookLoginRequest req,
IHttpClientFactory httpFactory,
IConfiguration cfg,
UsuarioService usuarios,
HttpContext ctx) =>
{
if (string.IsNullOrWhiteSpace(req.AccessToken)) return Results.BadRequest();
var appId = cfg["Facebook:AppId"]!;
var appSecret = cfg["Facebook:AppSecret"]!;
var versao = cfg["Facebook:GraphVersion"]!;
var http = httpFactory.CreateClient();
var debugUrl = "https://graph.facebook.com/debug_token"
+ "?input_token=" + Uri.EscapeDataString(req.AccessToken)
+ "&access_token=" + Uri.EscapeDataString(appId + "|" + appSecret);
using var debugResp = await http.GetAsync(debugUrl);
if (!debugResp.IsSuccessStatusCode) return Results.Unauthorized();
var debug = await debugResp.Content.ReadFromJsonAsync<DebugTokenEnvelope>();
if (debug?.Data is not { IsValid: true } dados || dados.AppId != appId)
return Results.Unauthorized();
var meUrl = "https://graph.facebook.com/" + versao + "/me?fields=id,name,email"
+ "&access_token=" + Uri.EscapeDataString(req.AccessToken)
+ "&appsecret_proof=" + AppSecretProof(req.AccessToken, appSecret);
using var meResp = await http.GetAsync(meUrl);
if (!meResp.IsSuccessStatusCode) return Results.Unauthorized();
var perfil = await meResp.Content.ReadFromJsonAsync<FacebookPerfil>();
if (perfil is null || perfil.Id != dados.UserId) return Results.Unauthorized();
var usuario = await usuarios.CriarOuAtualizarPorFacebookAsync(perfil.Id, perfil.Name, perfil.Email);
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, usuario.Id.ToString()),
new Claim(ClaimTypes.Name, usuario.Nome)
}, CookieAuthenticationDefaults.AuthenticationScheme);
await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity));
return Results.Ok(new { usuario.Nome });
});
static string AppSecretProof(string token, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
return Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(token))).ToLowerInvariant();
}
record FacebookLoginRequest(string AccessToken);
record DebugTokenEnvelope([property: JsonPropertyName("data")] DebugTokenData? Data);
record DebugTokenData(
[property: JsonPropertyName("app_id")] string AppId,
[property: JsonPropertyName("is_valid")] bool IsValid,
[property: JsonPropertyName("user_id")] string UserId);
record FacebookPerfil(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("email")] string? Email);
A versão da Graph API fica em configuração (Facebook:GraphVersion) e deve ser a mesma que o painel do seu app usa; assim você troca de versão sem recompilar. O appsecret_proof é um HMAC-SHA256 do token com o app secret. Com a opção correspondente ligada no painel do app, chamadas sem essa prova são recusadas, o que impede que um token vazado seja usado fora do seu servidor.
O ID certo para gravar o usuário
O ID que o Facebook devolve é específico do seu app: a mesma pessoa tem IDs diferentes em apps diferentes. Ele é estável dentro do seu app, e é por ele que você deve procurar o usuário. O e-mail não serve como chave: a pessoa pode negar a permissão de e-mail, a conta pode não ter e-mail confirmado e o campo pode simplesmente não vir. Por isso Email é anulável no record acima.
No Bazar Azul, a tabela de usuários tem uma tabela filha de logins externos com provedor e id_externo, com índice único nos dois. Se um dia a loja aceitar Google também, entra outra linha, sem gambiarra.
Quando o login do Facebook encontra uma conta com senha
Aqui mora o erro mais caro. Alguém já tem conta com senha no e-mail ana@exemplo.com. Outra pessoa entra com Facebook e o perfil vem com o mesmo e-mail. Se você vincular automaticamente, entregou a conta da Ana para quem controla aquele login do Facebook. A regra segura é não vincular em silêncio: peça que a pessoa entre com a senha primeiro e, já autenticada, conecte o Facebook nas configurações. Decida essa regra antes de escrever o código, e escreva-a no README.
Sair de verdade
Sair do sistema é derrubar o cookie do servidor com SignOutAsync e, se você guarda sessões no banco, invalidar a linha correspondente. Limpar o estado do Vue não basta: o cookie continua válido até expirar. Se quiser que o botão do Facebook também volte ao estado deslogado, trate isso no front, mas o essencial é o servidor parar de aceitar o cookie. O post cookie de sessão com HttpOnly, Secure e SameSite detalha os atributos.
Erros comuns
- Criar o usuário a partir do JSON que o front mandou. Qualquer pessoa forja o corpo e entra como outra conta.
- Chamar
/mesem conferir oapp_id. Um token válido de outro app passa, e o atacante usa um app dele para gerar tokens que o seu servidor aceita. - Colocar o app secret no Vue ou no
.envdo front. Tudo o que vai para o bundle é público, e com o secret alguém fala com a Graph API em nome do seu app. - Usar o e-mail como chave do usuário. O e-mail falta, muda ou coincide com uma conta local, e você cria duplicatas ou vincula a conta errada.
- Ignorar o cancelamento. O fluxo segue com
authResponsenulo e a tela mostra um erro genérico, ou pior, manda um token vazio para a API. - Guardar o access token do Facebook como sessão. Ele expira no ritmo do Facebook, não no seu, e fica exposto a qualquer script da página.
Checklist
- O corpo de
POST /api/auth/facebookcontém só o access token. - O servidor chama
debug_tokene confereis_valideapp_id. - O app secret está apenas na configuração do servidor, fora do repositório do front.
- A chamada a
/meenviaappsecret_proof. - O usuário é localizado pelo ID do Facebook, e o e-mail é tratado como opcional.
- Existe regra escrita para conta local com o mesmo e-mail, sem vínculo automático.
- A sessão é um cookie HttpOnly e Secure emitido pela sua API.
- Sair invalida a sessão no servidor, não só o estado do Vue.
Faça o teste agora: mande para o seu endpoint de login um JSON com nome e e-mail inventados e um token qualquer. A sua API cria a conta ou responde 401?
Referência: https://fullstackmark.com/post/16/user-authentication-with-vuejs-aspnet-core-2-and-facebook-login