Validar solicitações da API Places com o Firebase App Check e a reCAPTCHA

1. Antes de começar

Para garantir a legitimidade dos usuários que interagem com seu aplicativo da Web, implemente o Firebase App Check, usando tokens JWT do reCAPTCHA para verificar as sessões do usuário. Essa configuração vai permitir que você processe com segurança as solicitações do aplicativo cliente para a API Places (nova).

b40cfddb731786fa.png

Link ao vivo

O que você vai criar.

Para demonstrar isso, você vai criar um app da Web que mostra um mapa ao carregar. Ele também vai gerar discretamente um token reCAPTCHA usando o SDK do Firebase. Esse token é enviado para seu servidor Node.js, onde o Firebase o valida antes de atender a qualquer solicitação da API Places.

Se o token for válido, o Firebase App Check vai armazená-lo até que ele expire, eliminando a necessidade de criar um novo token para cada solicitação do cliente. Se o token for inválido, o usuário vai precisar concluir a verificação reCAPTCHA novamente para receber um novo token.

2. Pré-requisitos

Você precisa se familiarizar com os itens abaixo para concluir este codelab. daea823b6bc38b67.png

Produtos obrigatórios do Google Cloud

  • Firebase App Check do Google Cloud: banco de dados para gerenciamento de tokens
  • Google reCAPTCHA: criação e verificação de tokens. É uma ferramenta usada para distinguir humanos de bots em sites. Ele funciona analisando o comportamento do usuário, os atributos do navegador e as informações de rede para gerar uma pontuação que indica a probabilidade de o usuário ser um bot. Se a pontuação for alta o suficiente, o usuário será considerado humano e nenhuma outra ação será necessária. Se a pontuação for baixa, um quebra-cabeças de CAPTCHA poderá ser apresentado para confirmar a identidade do usuário. Essa abordagem é menos intrusiva do que os métodos tradicionais de CAPTCHA, o que torna a experiência do usuário mais tranquila.
  • (Opcional) Google Cloud App Engine: ambiente de implantação.

Produtos obrigatórios da Plataforma Google Maps

Neste codelab, você usará os seguintes produtos da Plataforma Google Maps:

Outros requisitos para este codelab

Para concluir este codelab, você vai precisar das seguintes contas, serviços e ferramentas:

  • Uma conta do Google Cloud Platform com o faturamento ativado
  • Uma chave de API da Plataforma Google Maps com a API Maps JavaScript e a API Places ativadas
  • Conhecimento básico de JavaScript, HTML e CSS
  • Conhecimento básico de Node.js
  • Um editor de texto ou ambiente de desenvolvimento integrado de sua escolha

3. Começar a configuração

Configurar a Plataforma Google Maps

Caso você ainda não tenha uma conta do Google Cloud Platform e um projeto com faturamento ativado, consulte o guia Primeiros passos com a Plataforma Google Maps para criar uma conta de faturamento e um projeto.

  1. No Console do Cloud, clique no menu suspenso do projeto e selecione a opção que você quer usar neste codelab.

e7ffad81d93745cd.png

  1. Ative as APIs e os SDKs da Plataforma Google Maps necessários para este codelab no Google Cloud Marketplace. Caso precise de ajuda, siga as etapas neste vídeo ou nesta documentação.
  2. Gere uma chave de API na página Credenciais do Console do Cloud. Você pode seguir as etapas neste vídeo ou nesta documentação. Todas as solicitações à Plataforma Google Maps exigem uma chave de API.

Application Default Credentials

Você vai usar o SDK Admin do Firebase para interagir com seu projeto do Firebase e fazer solicitações à API Places. Para que ele funcione, é necessário fornecer credenciais válidas.

Usaremos a autenticação ADC (credenciais padrão automáticas) para autenticar seu servidor e fazer solicitações. Como alternativa (não recomendada), você pode criar uma conta de serviço e armazenar credenciais no código.

Definição: o Application Default Credentials (ADC) é um mecanismo fornecido pelo Google Cloud para autenticar automaticamente seus aplicativos sem gerenciar credenciais explicitamente. Ele procura credenciais em vários locais (como variáveis de ambiente, arquivos de conta de serviço ou servidor de metadados do Google Cloud) e usa a primeira que encontrar.

  • No Terminal, use o comando abaixo, que permite que seus aplicativos acessem com segurança os recursos do Google Cloud em nome do usuário conectado:
gcloud auth application-default login
  • Você vai criar um arquivo .env na raiz que especifica uma variável do projeto do Google Cloud:
GOOGLE_CLOUD_PROJECT="your-project-id"

Criar uma conta de serviço

Credenciais

  • Clique na conta de serviço criada.
  • Na guia "KEYS", selecione "Create a Key" > "JSON" > salve as credenciais JSON salvas. Mova o arquivo xxx.json que foi salvo automaticamente para a pasta raiz
  • (Próxima aula) Nomeie corretamente no arquivo nodejs server.js (firebase-credentials.json).

4. Integração do Firebase App Check

Você vai receber detalhes de configuração do Firebase e chaves secretas do reCAPTCHA.

Você vai colá-los no aplicativo de demonstração e iniciar o servidor.

Criar um aplicativo no Firebase

Selecione o projeto do Google Cloud que já foi criado. Talvez seja necessário especificar: "Selecionar o recurso pai".

a6d171c6d7e98087.png a16010ba102cc90b.png

  • Adicione um aplicativo no menu superior esquerdo (engrenagem)

18e5a7993ad9ea53.png 4632158304652118.png

Código de inicialização do Firebase

  • Salvar o código de inicialização do Firebase para colar no script.js (próximo capítulo) no lado do cliente

f10dcf6f5027e9f0.png

  • Registrar seu app para permitir que o Firebase use tokens do reCAPTCHA v3

https://console.firebase.google.com/u/0/project/YOUR_PROJECT/appcheck/apps

da7efe203ce4142c.png

  • Escolha reCAPTCHA → crie uma chave no site reCAPTCHA (com os domínios certos configurados: localhost para desenvolvimento de apps)

b47eab131617467.png e6bddef9d5cf5460.png

  • Cole o segredo do reCAPTCHA no Firebase App Check

a63bbd533a1b5437.png

  • O status do app vai ficar verde.

4f7962b527b78ee5.png

5. Aplicativo de demonstração

  • App da Web do cliente:arquivos HTML, JavaScript e CSS
  • Servidor:arquivo Node.js
  • Ambiente (.env): chaves de API
  • Configuração (app.yaml): configurações de implantação do Google App Engine

Configuração do Node.js:

  • Navegar: abra o terminal e navegue até o diretório raiz do projeto clonado.
  • Instale o Node.js (se necessário): versão 18 ou mais recente.
node -v  # Check installed version
  • Inicializar projeto:execute o comando a seguir para inicializar um novo projeto Node.js, deixando todas as configurações como padrão:
npm init 
  • Instalar dependências:use o comando abaixo para instalar as dependências do projeto necessárias:
npm install @googlemaps/places firebase-admin express axios dotenv

Configuração: variáveis de ambiente para o projeto do Google Cloud

  • Criação de arquivos de ambiente:no diretório raiz do projeto, crie um arquivo chamado .env. Esse arquivo vai armazenar dados de configuração sensíveis e não deve ser confirmado no controle de versão.
  • Preencher variáveis de ambiente:abra o arquivo .env e adicione as seguintes variáveis, substituindo os marcadores de posição pelos valores reais do seu projeto do Google Cloud:
# Google Cloud Project ID
GOOGLE_CLOUD_PROJECT="your-cloud-project-id"

# reCAPTCHA Keys (obtained in previous steps) 
RECAPTCHA_SITE_KEY="your-recaptcha-site-key"
RECAPTCHA_SECRET_KEY="your-recaptcha-secret-key"

# Maps Platform API Keys (obtained in previous steps)
PLACES_API_KEY="your-places-api-key"
MAPS_API_KEY="your-maps-api-key"

6. Visão geral do código

index.html

  • Carrega as bibliotecas do Firebase para criar o token no app
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Places API with AppCheck</title>
  <style></style>  </head>
<body>
  <div id="map"></div>

    <!-- Firebase services -->
  <script src="https://www.gstatic.com/firebasejs/9.15.0/firebase-app-compat.js"></script>
  <script src="https://www.gstatic.com/firebasejs/9.15.0/firebase-app-check-compat.js"></script>
  
  <script type="module" src="./script.js"></script> 
  <link rel="stylesheet" href="./style.css">
</body>
</html>

script.js

  • Busca chaves de API:recupera chaves de API do Google Maps e do Firebase App Check em um servidor de back-end.
  • Inicializa o Firebase:configura o Firebase para autenticação e segurança. (Substitua a configuração → consulte o Capítulo 4).

A duração de validade do token do Firebase App Check, que varia de 30 minutos a 7 dias, é configurada no Console do Firebase e não pode ser alterada ao forçar a atualização do token.

  • Ativa o App Check:permite que o Firebase App Check verifique a autenticidade das solicitações recebidas.
  • Carrega a API Google Maps:carrega dinamicamente a biblioteca JavaScript do Google Maps para mostrar o mapa.
  • Inicializa o mapa:cria um mapa do Google centralizado em um local padrão.
  • Processa cliques no mapa:detecta cliques no mapa e atualiza o ponto central de acordo.
  • Consultas da API Places:envia solicitações para uma API de back-end (/api/data) para buscar informações sobre lugares (restaurantes, parques, bares) perto do local clicado, usando o Firebase App Check para autorização.
  • Mostrar marcadores:representa os dados buscados no mapa como marcadores, mostrando os nomes e ícones deles.
let mapsApiKey, recaptchaKey; // API keys
let currentAppCheckToken = null; // AppCheck token

async function init() {
  try {
    await fetchConfig(); // Load API keys from .env variable

    /////////// REPLACE with your Firebase configuration details
    const firebaseConfig = {
      apiKey: "AIza.......",
      authDomain: "places.......",
      projectId: "places.......",
      storageBucket: "places.......",
      messagingSenderId: "17.......",
      appId: "1:175.......",
      measurementId: "G-CPQ.......",
    };
    /////////// REPLACE 

    // Initialize Firebase and App Check
    await firebase.initializeApp(firebaseConfig);
    await firebase.appCheck().activate(recaptchaKey);

    // Get the initial App Check token
    currentAppCheckToken = await firebase.appCheck().getToken();

    // Load the Maps JavaScript API dynamically
    const scriptMaps = document.createElement("script");
    scriptMaps.src = `https://maps.googleapis.com/maps/api/js?key=${mapsApiKey}&libraries=marker,places&v=beta`;
    scriptMaps.async = true;
    scriptMaps.defer = true;
    scriptMaps.onload = initMap; // Create the map after the script loads
    document.head.appendChild(scriptMaps);
  } catch (error) {
    console.error("Firebase initialization error:", error);
    // Handle the error appropriately (e.g., display an error message)
  }
}
window.onload = init()

// Fetch configuration data from the backend API
async function fetchConfig() {
  const url = "/api/config";

  try {
    const response = await fetch(url);
    const config = await response.json();
    mapsApiKey = config.mapsApiKey;
    recaptchaKey = config.recaptchaKey;
  } catch (error) {
    console.error("Error fetching configuration:", error);
    // Handle the error (e.g., show a user-friendly message)
  }
}

// Initialize the map when the Maps API script loads
let map; // Dynamic Map
let center = { lat: 48.85557501, lng: 2.34565006 };
function initMap() {
  map = new google.maps.Map(document.getElementById("map"), {
    center: center,
    zoom: 13,
    mapId: "b93f5cef6674c1ff",
    zoomControlOptions: {
      position: google.maps.ControlPosition.RIGHT_TOP,
    },
    streetViewControl: false,
    mapTypeControl: false,
    clickableIcons: false,
    fullscreenControlOptions: {
      position: google.maps.ControlPosition.LEFT_TOP,
    },
  });

  // Initialize the info window for markers
  infoWindow = new google.maps.InfoWindow({});

  // Add a click listener to the map
  map.addListener("click", async (event) => {
    try {
      // Get a fresh App Check token on each click
      const appCheckToken = await firebase.appCheck().getToken();
      currentAppCheckToken = appCheckToken;

      // Update the center for the Places API query
      center.lat = event.latLng.lat();
      center.lng = event.latLng.lng();

      // Query for places with the new token and center
      queryPlaces();
    } catch (error) {
      console.error("Error getting App Check token:", error);
    }
  });
}

function queryPlaces() {
  const url = '/api/data'; // "http://localhost:3000/api/data"

  const body = {
    request: {
      includedTypes: ['restaurant', 'park', 'bar'],
      excludedTypes: [],
      maxResultCount: 20,
      locationRestriction: {
        circle: {
          center: {
            latitude: center.lat,
            longitude: center.lng,
          },
          radius: 4000,
        },
      },
    },
  };

  // Provides token to the backend using header: X-Firebase-AppCheck

  fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Firebase-AppCheck': currentAppCheckToken.token,
    },
    body: JSON.stringify(body),
  })
    .then((response) => response.json())
    .then((data) => {
      // display if response successful
      displayMarkers(data.places);
    })
    .catch((error) => {
      alert('No places');
      // eslint-disable-next-line no-console
      console.error('Error:', error);
    });
}


//// display places markers on map
...

server.js

  • Carrega variáveis de ambiente (chaves de API, ID do projeto do Google) de um arquivo .env.
  • Inicia o servidor,detectando solicitações em http://localhost:3000.
  • Inicializa o SDK Admin do Firebase usando as credenciais padrão do aplicativo (ADC).
  • Recebe um token reCAPTCHA de script.js.
  • Verifica a validade do token recebido.
  • Se o token for válido, faça uma solicitação POST para a API Google Places com os parâmetros de pesquisa incluídos.
  • Processa e retorna a resposta da API Places para o cliente.
const express = require('express');
const axios = require('axios');

const admin = require('firebase-admin');

// .env variables
require('dotenv').config();

// Store sensitive API keys in environment variables
const recaptchaSite = process.env.RECAPTCHA_SITE_KEY;
const recaptchaSecret = process.env.RECAPTCHA_SECRET_KEY;
const placesApiKey = process.env.PLACES_API_KEY;
const mapsApiKey = process.env.MAPS_API_KEY;

// Verify environment variables loaded (only during development)
console.log('recaptchaSite:', recaptchaSite, '\n');
console.log('recaptchaSecret:', recaptchaSecret, '\n');

const app = express();
app.use(express.json());

// Firebase Admin SDK setup with Application Default Credentials (ADC)
const { GoogleAuth } = require('google-auth-library');
admin.initializeApp({
  // credential: admin.credential.applicationDefault(), // optional: explicit ADC
});

// Main API Endpoint 
app.post('/api/data', async (req, res) => {
  const appCheckToken = req.headers['x-firebase-appcheck'];

  console.log("\n", "Token", "\n", "\n", appCheckToken, "\n")

  try {
    // Verify Firebase App Check token for security
    const appCheckResult = await admin.appCheck().verifyToken(appCheckToken);

    if (appCheckResult.appId) {
      console.log('App Check verification successful!');
      placesQuery(req, res);
    } else {
      console.error('App Check verification failed.');
      res.status(403).json({ error: 'App Check verification failed.' });
    }
  } catch (error) {
    console.error('Error verifying App Check token:', error);
    res.status(500).json({ error: 'Error verifying App Check token.' });
  }
});

// Function to query Google Places API
async function placesQuery(req, res) {
  console.log('#################################');
  console.log('\n', 'placesApiKey:', placesApiKey, '\n');

  const queryObject = req.body.request;
  console.log('\n','Request','\n','\n', queryObject, '\n')

  const headers = {
    'Content-Type': 'application/json',
    'X-Goog-FieldMask': '*',
    'X-Goog-Api-Key': placesApiKey,
    'Referer': 'http://localhost:3000',  // Update for production(ie.: req.hostname)
  };

  const myUrl = 'https://places.googleapis.com/v1/places:searchNearby';

  try {
    // Authenticate with ADC
    const auth = new GoogleAuth();
    const { credential } = await auth.getApplicationDefault();

    const response = await axios.post(myUrl, queryObject, { headers, auth: credential });
    
    console.log('############### SUCCESS','\n','\n','Response','\n','\n', );
    const myBody = response.data;
    myBody.places.forEach(place => {
      console.log(place.displayName); 
    });
    res.json(myBody); // Use res.json for JSON data
  } catch (error) {
    console.log('############### ERROR');
    // console.error(error); // Log the detailed error for debugging
    res.status(error.response.status).json(error.response.data); // Use res.json for errors too
  }
}

// Configuration endpoint (send safe config data to the client)
app.get('/api/config', (req, res) => {
  res.json({
    mapsApiKey: process.env.MAPS_API_KEY, 
    recaptchaKey: process.env.RECAPTCHA_SITE_KEY, 
  });
});

// Serve static files
app.use(express.static('static'));

// Start the server
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server listening on port ${port}`, '\n');
});

7. Execute o aplicativo

No ambiente escolhido, execute o servidor no terminal e acesse http://localhost:3000.

npm start 

Um token é criado como uma variável global, oculta da janela do navegador do usuário e transmitida ao servidor para processamento. Os detalhes do token podem ser encontrados nos registros do servidor.

Os detalhes sobre as funções do servidor e a resposta à solicitação de pesquisa por proximidade da API Places estão nos registros do servidor.

Solução de problemas:

Verifique se o ID do projeto do Google está consistente na configuração:

  • no arquivo .env (variável GOOGLE_CLOUD_PROJECT)
  • na configuração da gcloud do terminal:
gcloud config set project your-project-id
  • na configuração do reCAPTCHA

e6bddef9d5cf5460.png

  • na configuração do Firebase

7e17bfbcb8007763.png

Outro

  • Crie um token de depuração que possa ser usado no lugar da chave do site reCAPTCHA em script.js para fins de teste e solução de problemas.

9c0beb760d13faef.png

try {
 // Initialize Firebase first
 await firebase.initializeApp(firebaseConfig);
  // Set the debug token
  if (window.location.hostname === 'localhost') { // Only in development
    await firebase.appCheck().activate(
      'YOUR_DEBUG_FIREBASE_TOKEN', // Replace with the token from the console
      true // Set to true to indicate it's a debug token
      );
  } else {
      // Activate App Check
      await firebase.appCheck().activate(recaptchaKey);
}
  • Tentar muitas autenticações sem sucesso, por exemplo, usar uma chave de site falsa do reCAPTCHA, pode acionar um limite temporário.
FirebaseError: AppCheck: Requests throttled due to 403 error. Attempts allowed again after 01d:00m:00s (appCheck/throttled).

Credenciais do ADC

  • Verifique se você está na conta do gcloud certa
gcloud auth login 
  • Verifique se as bibliotecas necessárias estão instaladas
npm install @googlemaps/places firebase-admin
  • Verifique se a biblioteca do Firebase foi carregada em server.js.
const {GoogleAuth} = require('google-auth-library');
gcloud auth application-default login
  • Impersonate: as credenciais do ADC foram salvas
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
  • Por fim, teste o ADC localmente, salvando o script a seguir como "test.js" e executando no terminal: node test.js
const {GoogleAuth} = require('google-auth-library');

async function requestTestADC() {
 try {
   // Authenticate using Application Default Credentials (ADC)
   const auth = new GoogleAuth();
   const {credential} = await auth.getApplicationDefault();

   // Check if the credential is successfully obtained
   if (credential) {
     console.log('Application Default Credentials (ADC) loaded successfully!');
     console.log('Credential:', credential); // Log the credential object
   } else {
     console.error('Error: Could not load Application Default Credentials (ADC).');
   }

   // ... rest of your code ...

 } catch (error) {
   console.error('Error:', error);
 }
}

requestTestADC();

8. Pronto, parabéns!

Etapas de acompanhamento

Implantação no App Engine:

  • Prepare seu projeto para implantação no Google App Engine, fazendo as mudanças de configuração necessárias.
  • Use a ferramenta de linha de comando gcloud ou o console do App Engine para implantar seu aplicativo.

Melhorar a autenticação do Firebase:

  • Tokens padrão x tokens personalizados:implemente tokens personalizados do Firebase para usar melhor os serviços do Firebase.
  • Tempo de vida do token:defina o tempo de vida adequado para operações sensíveis (token personalizado do Firebase até uma hora) e sessões gerais (token do reCAPTCHA: 30 minutos a 7 horas).
  • Conheça as alternativas ao reCAPTCHA:verifique se o DeviceCheck (iOS), o SafetyNet (Android) ou o App Attest são adequados para suas necessidades de segurança.

Integrar produtos do Firebase:

  • Realtime Database ou Firestore:se o app precisar de sincronização de dados em tempo real ou recursos off-line, faça a integração com o Realtime Database ou o Firestore.
  • Cloud Storage:use o Cloud Storage para armazenar e exibir conteúdo gerado pelo usuário, como imagens ou vídeos.
  • Autenticação:use o Firebase Authentication para criar contas de usuário, gerenciar sessões de login e processar redefinições de senha.

Expandir para dispositivos móveis:

  • Android e iOS:se você planeja ter um app para dispositivos móveis, crie versões para as plataformas Android e iOS.
  • SDKs do Firebase:use os SDKs do Firebase para Android e iOS para integrar perfeitamente os recursos do Firebase aos seus apps para dispositivos móveis.