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, você vai implementar o Firebase App Check, aproveitando os tokens JWT do reCAPTCHA para verificar as sessões dos usuários. Essa configuração permite processar com segurança 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 web app que mostra um mapa ao ser carregado. Ele também vai gerar discretamente um token do reCAPTCHA usando o SDK do Firebase. Esse token é enviado ao seu servidor Node.js, onde o Firebase o valida antes de atender a qualquer solicitação para a 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 analisa o comportamento do usuário, os atributos do navegador e as informações da 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 teste CAPTCHA poderá ser apresentado para confirmar a identidade do usuário. Essa abordagem é menos intrusiva do que os métodos tradicionais de CAPTCHA, tornando 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 o 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 o Places ativados
  • 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 o projeto 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 isso, é necessário fornecer credenciais válidas.

Vamos usar a autenticação ADC (credenciais padrão automáticas) para autenticar seu servidor e fazer solicitações. Como alternativa (não recomendada), crie uma conta de serviço e armazene as credenciais no seu código.

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

  • 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 no momento:
gcloud auth application-default login
  • Você vai criar um arquivo .env na raiz que especifica uma variável de projeto do Google Cloud:
GOOGLE_CLOUD_PROJECT="your-project-id"

Criar uma conta de serviço

Credenciais

  • Clique na conta de serviço criada
  • Acesse a guia CHAVES para criar uma chave > JSON > salve as credenciais JSON baixadas. Mova o arquivo xxx.json baixado automaticamente para a pasta raiz.
  • (Próximo capítulo) Nomeie corretamente no arquivo nodejs server.js (firebase-credentials.json)

4. Integração do Firebase AppCheck

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

Cole-os no aplicativo de demonstração e inicie o servidor.

Criar um aplicativo no Firebase

SELECIONE o projeto na nuvem 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 (engrenagem) no canto superior esquerdo.

18e5a7993ad9ea53.png 4632158304652118.png

Código de inicialização do Firebase

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

f10dcf6f5027e9f0.png

  • Registre 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 secret do reCAPTCHA no Firebase AppCheck

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 a seguir para instalar as dependências necessárias do projeto:
npm install @googlemaps/places firebase-admin express axios dotenv

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

  • Criação do arquivo de ambiente:no diretório raiz do projeto, crie um arquivo chamado .env. Esse arquivo armazena dados de configuração sensíveis e não deve ser enviado ao controle de versões.
  • Preencher variáveis de ambiente:abra o arquivo .env e adicione as seguintes variáveis, substituindo os marcadores 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 de um servidor de back-end.
  • Inicializa o Firebase:configura o Firebase para autenticação e segurança. (Substituir configuração → consulte o Capítulo 4).

A duração da 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 forçando uma 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 com eles.
  • Consulta a API Places:envia solicitações a 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.
  • Mostra marcadores:mostra os dados buscados no mapa como marcadores, exibindo 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 Application Default Credentials (ADC).
  • Recebe um token reCAPTCHA de script.js.
  • Verifica a validade do token recebido.
  • Se o token for válido, fará uma solicitação POST para a API Google Places com parâmetros de pesquisa incluídos.
  • Processa e retorna a resposta da API Places ao 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, oculto da janela do navegador do usuário e transmitido 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 Nearby Search da API Places podem ser encontrados nos registros do servidor.

Solução de problemas:

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

  • no arquivo .env (variável GOOGLE_CLOUD_PROJECT)
  • na configuração da gcloud no 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, usando uma chave de site reCAPTCHA falsa, pode acionar uma limitação temporária.
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 certa do gcloud
gcloud auth login 
  • Verifique se as bibliotecas necessárias estão instaladas.
npm install @googlemaps/places firebase-admin
  • Verifique se a biblioteca do Firebase está carregada em server.js.
const {GoogleAuth} = require('google-auth-library');
gcloud auth application-default login
  • Representação: credenciais do ADC salvas
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
  • Por fim, teste o ADC localmente. Salve o script a seguir como test.js e execute 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, tudo certo!

Próximas etapas

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 o Firebase Authentication:

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

Integrar produtos do Firebase:

  • Realtime Database ou Firestore:se o aplicativo 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 disponibilizar 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 e integre recursos do Firebase aos seus apps para dispositivos móveis.