Programação Por Equipe DevBras

Capa do artigo: Login com Facebook no Vue: como transformar o token em sessão no servidor

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.

  1. O Vue chama FB.login pedindo public_profile e email.
  2. Se a pessoa cancelar, o fluxo para ali, sem chamar a API.
  3. Se conectar, o Vue envia apenas accessToken para POST /api/auth/facebook.
  4. A API chama debug_token usando o token do app (APP_ID|APP_SECRET), guardado em configuração do servidor.
  5. A API exige is_valid verdadeiro e app_id igual ao seu.
  6. A API busca id, name e email em /me, com appsecret_proof.
  7. 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 /me sem conferir o app_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 .env do 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 authResponse nulo 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/facebook contém só o access token.
  • O servidor chama debug_token e confere is_valid e app_id.
  • O app secret está apenas na configuração do servidor, fora do repositório do front.
  • A chamada a /me envia appsecret_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?

Avalie este artigo

Referência: https://fullstackmark.com/post/16/user-authentication-with-vuejs-aspnet-core-2-and-facebook-login