ASP.NET Core 3.1 - Autenticando sua API com Google via Firebase

O Firebase Authentication permite que sua aplicação faça login com o Google e que uma API ASP.NET Core 3.1 valide esse acesso com poucas linhas de código. Neste artigo, vamos configurar o projeto no Firebase, autenticar o usuário no frontend e proteger a API com tokens JWT.

Praticamente toda aplicação precisa de autenticação e, hoje em dia, entrar com uma rede social traz muita praticidade para o usuário.

Mas como fica a API nesse cenário? Como validamos o token e como recuperamos os dados do usuário?

Por que não usar o ASP.NET Identity

Quem me conhece sabe que não uso o ASP.NET Identity para quase nada. Acho ele bem útil em aplicações com Razor, porém, no cenário de APIs, criar autenticação e autorização é bem simples.

Por incrível que pareça, fazer autenticação com Google, Facebook, GitHub ou mesmo com um Identity Server próprio também é simples. Afinal, tudo de que precisamos é o padrão OAuth combinado com tokens JWT.

Nesse modelo, a API não guarda senhas nem gerencia contas. Em vez disso, ela confia em um provedor de identidade e apenas verifica se o token recebido foi emitido por ele.

Por que usar o Firebase Authentication

O primeiro ponto é a criação dos usuários e o setup das contas. O Firebase Authentication já traz tudo isso pronto, com uma facilidade incrível, então ele é uma ótima saída para quem não quer perder tempo com uma configuração complexa.

Além disso, o Firebase fornece SDKs para JavaScript, Node, Java, .NET e várias outras linguagens. Ou seja, ele já entrega tudo pronto também para o nosso frontend.

Outro ponto positivo é que o mesmo projeto suporta vários provedores ao mesmo tempo. Portanto, adicionar Facebook, GitHub ou e-mail e senha depois exige apenas habilitá-los no console.

Criando o projeto no console do Firebase

O primeiro passo é acessar o console do Firebase, fazer login e criar um novo projeto. Em seguida, vá até Authentication, abra a aba de métodos de login e habilite o Google.

https://console.firebase.google.com/project/YOUR_PROJECT/authentication/providers

Por fim, abra as configurações do projeto e adicione uma nova aplicação. Para esta demonstração, vamos registrar uma aplicação web.

https://console.firebase.google.com/project/YOUR_PROJECT/settings/general/

Ao registrar a aplicação, o console exibe um trecho de código com as configurações do projeto. Guarde esse conteúdo, pois vamos utilizá-lo em breve.

<!-- O SDK principal do Firebase é sempre obrigatório e deve vir primeiro -->
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>

<script>
  // Configuração da aplicação web no Firebase
  var firebaseConfig = {
    apiKey: "YOUR_API_KEY",
    authDomain: "your-project.firebaseapp.com",
    databaseURL: "https://your-project.firebaseio.com",
    projectId: "your-project",
    storageBucket: "your-project.appspot.com",
    messagingSenderId: "000000000000",
    appId: "1:000000000000:web:0000000000000000"
  };
  // Inicializa o Firebase
  firebase.initializeApp(firebaseConfig);
</script>

O campo mais importante para a API é o projectId. Ele identifica o projeto e será usado mais adiante na validação do token.

Autenticando no frontend com o Firebase Authentication

O primeiro passo é autenticar o usuário no frontend, seja ele HTML puro, Angular, React, Vue ou qualquer outro. Esse processo é o mesmo que realizamos em qualquer aplicação SPA.

Nele, o frontend vai até o servidor de autenticação, que em alguns casos pode ser a nossa própria API, e recebe um token no formato JWT. Com o token em mãos, enviamos esse valor no cabeçalho de cada requisição para a API.

Nunca confie nos dados do frontend

Antes de seguir, um alerta importante: nunca, em hipótese alguma, confie nos dados vindos do frontend. Talvez você esteja se perguntando por que não enviar apenas o e-mail do usuário, já que recebemos nome, e-mail e foto do Google.

O problema é que qualquer pessoa poderia se passar por você nesse caso. Basta abrir o Postman, por exemplo, e fazer uma requisição para a API informando um e-mail diferente.

Por isso, o processo será sempre enviar o token, que é assinado pelos servidores do Google. Em seguida, a API valida essa assinatura e só então lê os dados do usuário.

É assim que todo servidor de identidade funciona: ele emite e valida os tokens. Isso vale para Google, Facebook, GitHub, Azure AD, Identity Server e até para um servidor de identidade próprio.

Incluindo o SDK do Firebase

No meu caso, criei uma nova API com ASP.NET Core usando o comando dotnet new webapi -o Todo. Dentro dessa API, criei uma pasta Client com um arquivo index.html.

O SDK do Firebase é modularizado, então precisamos incluir dois arquivos: um base e outro para autenticação.

<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-auth.js"></script>

Repare que os dois arquivos usam a mesma versão do SDK. Misturar versões diferentes entre o módulo base e o de autenticação pode gerar erros difíceis de diagnosticar.

O próximo passo é inicializar o Firebase com as configurações que recebemos ao criar o app no console.

var firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "your-project.firebaseapp.com",
  databaseURL: "https://your-project.firebaseio.com",
  projectId: "your-project",
  storageBucket: "your-project.appspot.com",
  messagingSenderId: "000000000000",
  appId: "1:000000000000:web:0000000000000000"
};

// Inicializa o Firebase
firebase.initializeApp(firebaseConfig);

A apiKey do Firebase identifica o projeto e não funciona como uma senha. Mesmo assim, configure os domínios autorizados no console, para que apenas os seus sites consigam iniciar o login.

Servindo os arquivos localmente

Pronto, o app já está registrado e inicializado. Neste momento, você já pode testar a página, e recomendo usar o HTTP Server para isso.

Abra um terminal, navegue até a pasta Client e execute os comandos abaixo.

npm install -g http-server
http-server -c1

O primeiro comando instala o HTTP Server na sua máquina, e o segundo sobe um servidor de arquivos estáticos na pasta atual. O parâmetro -c1 define o cache em apenas um segundo, ou seja, não queremos cachear nada.

Feito isso, o app fica disponível em http://localhost:8080. Precisamos desse servidor porque o Firebase não aceita login a partir de arquivos abertos direto do disco, apenas a partir de URLs.

Configurando o provedor do Google

Com o Firebase configurado, é hora de preparar a autenticação. O Firebase Authentication oferece diversos provedores, entre eles o do Google.

Além do provedor, podemos informar escopos, que definem quais informações do usuário queremos acessar. Sem nenhum escopo adicional, o login já retorna os dados básicos do perfil, como nome, e-mail e foto.

// Inicializa o Firebase
firebase.initializeApp(firebaseConfig);

var provider = new firebase.auth.GoogleAuthProvider();
provider.addScope("https://www.googleapis.com/auth/contacts.readonly");

Essa configuração é a mesma para qualquer provedor, mudando apenas a classe e a URL do escopo. Agora, basta executar o login.

function login() {
  firebase.auth().signInWithPopup(provider).then(function (result) {
    // ESTE NÃO É O JWT, É O TOKEN PARA OBTER INFORMAÇÕES DO GOOGLE
    var googleAccessToken = result.credential.accessToken;
    var user = result.user;

    firebase.auth()
      .currentUser
      .getIdToken()
      .then(function (idToken) {
        // ESTE É O TOKEN JWT QUE VAMOS ENVIAR PARA A API
        console.log(idToken);
      })
      .catch(function (error) {
        console.log(error);
      });
  }).catch(function (error) {
    // Algo deu errado no login
    console.log(error);
  });
}

Os dois tokens do login com Firebase

Deixei alguns comentários no código para destacar que existem dois tokens durante o login. O primeiro é o access token do Google, usado para chamar as APIs do Google com os escopos solicitados.

O segundo é o ID token do Firebase, obtido com getIdToken, que é o JWT que de fato queremos enviar para a nossa API. Ele é assinado pelo Firebase e expira em uma hora, mas o SDK renova esse valor automaticamente enquanto o usuário estiver logado.

Nesse exemplo, usamos o signInWithPopup, que abre aquela famosa janela do Google para o usuário se autenticar. Em dispositivos móveis, porém, o signInWithRedirect costuma oferecer uma experiência melhor.

Por fim, deixei o token sendo exibido no console do navegador, para facilitar a cópia e o teste da API.

Página completa do cliente

Falta apenas o botão de login, que ficou assim.

<button onclick="login()">SIGN IN WITH GOOGLE</button>

Pronto, temos o frontend se autenticando com o Google e escrevendo o token no console após o login. O resultado final do arquivo Client/index.html é este aqui.

<html>

<head>
  <title>Fire</title>
</head>

<body>
  <button onclick="login()">SIGN IN WITH GOOGLE</button>

  <script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-auth.js"></script>

  <script>
    var firebaseConfig = {
      apiKey: "YOUR_API_KEY",
      authDomain: "your-project.firebaseapp.com",
      databaseURL: "https://your-project.firebaseio.com",
      projectId: "your-project",
      storageBucket: "your-project.appspot.com",
      messagingSenderId: "000000000000",
      appId: "1:000000000000:web:0000000000000000"
    };

    // Inicializa o Firebase
    firebase.initializeApp(firebaseConfig);

    var provider = new firebase.auth.GoogleAuthProvider();
    provider.addScope("https://www.googleapis.com/auth/contacts.readonly");

    function login() {
      firebase.auth().signInWithPopup(provider).then(function (result) {
        // ESTE NÃO É O JWT, É O TOKEN PARA OBTER INFORMAÇÕES DO GOOGLE
        var googleAccessToken = result.credential.accessToken;
        var user = result.user;

        firebase.auth()
          .currentUser
          .getIdToken()
          .then(function (idToken) {
            // ESTE É O TOKEN JWT QUE VAMOS ENVIAR PARA A API
            console.log(idToken);
          })
          .catch(function (error) {
            console.log(error);
          });
      }).catch(function (error) {
        // Algo deu errado no login
        console.log(error);
      });
    }
  </script>
</body>

</html>

Validando o Firebase Authentication na API

Agora vem a parte mais legal: autenticar no backend. A API precisa receber o token no cabeçalho da requisição e validá-lo com as chaves públicas do Google para liberar o acesso.

O processo é o mesmo que fazemos em uma autenticação JWT convencional. A única diferença é que quem emite o token é o Firebase, e não a nossa própria API.

Instalando o pacote JwtBearer

Vamos começar adicionando o pacote de autenticação JWT à API. Como o projeto usa o ASP.NET Core 3.1, escolha a versão 3.1 do pacote.

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 3.1.0

Feito isso, precisamos garantir que a API exige autenticação e autorização. Para isso, vá até o método Configure do Startup.cs e verifique se estas duas linhas existem.

app.UseAuthentication();
app.UseAuthorization();

A ordem importa: a autenticação identifica o usuário, e só depois a autorização decide se ele pode acessar o recurso. Além disso, as duas chamadas precisam ficar entre o UseRouting e o UseEndpoints.

Se quiser entender melhor como essa ordem funciona no pipeline, veja o artigo sobre middleware no ASP.NET e como criar o seu.

Configurando Authority, Issuer e Audience

Agora precisamos informar à API que ela usa autenticação do tipo JWT Bearer. Isso é feito no método ConfigureServices, com a linha abaixo.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);

Com isso, a API passa a esperar tokens JWT no cabeçalho Authorization, com o esquema Bearer. Contudo, ela ainda não sabe como verificar esses tokens.

Para isso, informamos a URL que a API deve usar para obter as chaves de verificação, chamada de Authority. A partir dela, o handler baixa o documento de metadados do OpenID Connect e as chaves públicas do Google.

O provedor de identidade também não aceita qualquer token como válido para qualquer aplicação. Afinal, a mesma conta do Google pode ter vários apps com login social, então precisamos validar o Issuer, que é quem emitiu o token, e a Audience, que é o ID do nosso projeto.

services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://securetoken.google.com/YOUR_PROJECT_ID";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "https://securetoken.google.com/YOUR_PROJECT_ID",
            ValidateAudience = true,
            ValidAudience = "YOUR_PROJECT_ID",
            ValidateLifetime = true
        };
    });

Pronto, a API já valida o login com Google feito pelo Firebase Authentication. Substitua YOUR_PROJECT_ID pelo projectId que aparece nas configurações do console.

O que a API verifica no token

A documentação de verificação de ID tokens do Firebase lista as regras que um token precisa cumprir. A configuração acima cobre todas elas com os validadores padrão do ASP.NET Core.

  • Assinatura: o algoritmo deve ser RS256, com uma chave pública publicada pelo Google.
  • exp: a data de expiração precisa estar no futuro.
  • iat: a data de emissão precisa estar no passado.
  • aud: precisa ser igual ao ID do projeto no Firebase.
  • iss: precisa ser https://securetoken.google.com/ seguido do ID do projeto.
  • sub: precisa ser uma string não vazia, igual ao identificador do usuário.

Em outras palavras, um token emitido para outro projeto, vencido ou adulterado é rejeitado automaticamente. Dessa forma, a API não precisa escrever nenhuma linha de validação manual.

Testando a autenticação com Firebase

Com tudo configurado, abra qualquer método do controller padrão e adicione o atributo [Authorize]. Em seguida, tente chamar esse método pelo Postman, por exemplo, e você vai receber o erro 401 Unauthorized.

Agora copie o token gerado no console do navegador e faça uma nova requisição para a mesma URL. Desta vez, informe o cabeçalho abaixo.

Authorization: Bearer YOUR_TOKEN

Como mágica, a API validou o token com as chaves do Google e liberou o acesso ao método. Incrível, não é?

Com pouquíssimas linhas de código, integramos a API ao Google e ainda usamos os atributos padrão do ASP.NET Core. E o melhor: sem ASP.NET Identity.

Lendo os dados do usuário

Para obter os dados do usuário, basta ler as claims disponíveis em User.Claims. Ali fica uma lista com tudo o que o token carrega, como nome, foto e e-mail.

O código final do controller ficou assim.

using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Todo.Controllers
{
    [ApiController]
    [Route("")]
    public class HomeController : ControllerBase
    {
        [HttpGet]
        [Authorize]
        public dynamic Get()
        {
            return new
            {
                name = User.Claims.FirstOrDefault(x => x.Type == "name")?.Value,
                // email = User.Claims.FirstOrDefault(x => x.Type == "email")?.Value,
                picture = User.Claims.FirstOrDefault(x => x.Type == "picture")?.Value
            };
        }
    }
}

Note que o operador ?. evita uma exceção caso alguma claim não exista no token. Assim, o endpoint continua respondendo mesmo para contas sem foto, por exemplo.

Atenção ao mapeamento de claims

Por padrão, o handler JWT do ASP.NET Core 3.1 renomeia algumas claims conhecidas para os tipos longos do .NET. É o caso de email e sub, que passam a usar os tipos de ClaimTypes.

Por isso, buscar a claim email pelo nome curto não encontra o valor. Nesse caso, use ClaimTypes.Email ou limpe o mapeamento com JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() antes de configurar a autenticação.

O identificador único do usuário no Firebase fica na claim user_id e também em sub. Portanto, use esse valor como chave para relacionar o usuário com os dados do seu banco.

Boas práticas com Firebase Authentication

Guarde o ID do projeto nas configurações da aplicação, em vez de deixá-lo fixo no código. Dessa forma, fica fácil usar projetos diferentes para desenvolvimento e produção.

No frontend, chame getIdToken antes de cada requisição, em vez de guardar o token manualmente. Assim, o SDK entrega sempre um token válido e cuida da renovação para você.

Além disso, sirva a API apenas por HTTPS, porque o token dá acesso total à conta do usuário enquanto for válido. Para conhecer outras medidas de proteção, confira o artigo sobre segurança em APIs ASP.NET Core.

Por fim, lembre-se de que autenticação não é autorização. O token diz quem é o usuário, mas as regras de quem pode fazer o quê continuam sendo responsabilidade da sua API.

Conclusão

Com o Firebase Authentication, o login com Google fica pronto em poucos minutos no frontend. Do lado da API, o ASP.NET Core 3.1 valida o token com apenas a configuração do JwtBearer.

O resultado é uma autenticação segura, sem senhas guardadas no seu banco e sem depender do ASP.NET Identity. Além disso, a mesma estrutura funciona para outros provedores que o Firebase suporta.

O código-fonte completo deste exemplo está no meu GitHub, no repositório aspnetcore-firebase-google-login.

FAQ

Preciso do ASP.NET Identity para usar login com Google?

Não, a API só precisa validar o JWT emitido pelo Firebase. O pacote JwtBearer faz esse trabalho com a configuração de Authority, Issuer e Audience.

Qual token devo enviar para a API?

Envie o ID token obtido com getIdToken. O access token do Google serve apenas para chamar as APIs do próprio Google.

Por quanto tempo o ID token do Firebase é válido?

Ele expira em uma hora, mas o SDK do Firebase renova o valor automaticamente enquanto o usuário continua logado.

Por que a claim de e-mail não aparece pelo nome curto?

O handler JWT do ASP.NET Core 3.1 mapeia algumas claims para os tipos de ClaimTypes. Use ClaimTypes.Email ou limpe o mapeamento padrão.

Posso usar outros provedores além do Google?

Sim, basta habilitar o provedor no console e trocar a classe no frontend. A configuração da API continua exatamente a mesma.