Convalida le richieste dell'API Places con Firebase App Check e reCAPTCHA

1. Prima di iniziare

Per garantire la legittimità degli utenti che interagiscono con la tua applicazione web, dovrai implementare Firebase App Check, sfruttando i token JWT di reCAPTCHA per verificare le sessioni utente. Questa configurazione ti consentirà di gestire in modo sicuro le richieste dall'applicazione client all'API Places (nuova).

b40cfddb731786fa.png

Link live

Cosa creerai.

Per dimostrarlo, creerai un'app web che mostra una mappa al caricamento. Inoltre, genererà discretamente un token reCAPTCHA utilizzando l'SDK Firebase. Questo token viene poi inviato al server Node.js, dove Firebase lo convalida prima di soddisfare eventuali richieste all'API Places.

Se il token è valido, Firebase App Check lo memorizza fino alla scadenza, eliminando la necessità di creare un nuovo token per ogni richiesta del client. Se il token non è valido, all'utente verrà chiesto di completare di nuovo la verifica reCAPTCHA per ottenere un nuovo token.

2. Prerequisiti

Per completare questo codelab, devi acquisire familiarità con gli elementi riportati di seguito. daea823b6bc38b67.png

Prodotti Google Cloud obbligatori

  • Firebase App Check di Google Cloud: database per la gestione dei token
  • Google reCAPTCHA: creazione e verifica del token. È uno strumento utilizzato per distinguere le persone dai bot sui siti web. Funziona analizzando il comportamento dell'utente, gli attributi del browser e le informazioni di rete per generare un punteggio che indica la probabilità che l'utente sia un bot. Se il punteggio è sufficientemente alto, l'utente viene considerato umano e non sono necessarie ulteriori azioni. Se il punteggio è basso, potrebbe essere visualizzato un puzzle CAPTCHA per confermare l'identità dell'utente. Questo approccio è meno invasivo dei metodi CAPTCHA tradizionali, il che rende l'esperienza utente più fluida.
  • (Facoltativo) Google Cloud App Engine: ambiente di deployment.

Prodotti Google Maps Platform obbligatori

In questo Codelab utilizzerai i seguenti prodotti Google Maps Platform:

Altri requisiti per questo codelab

Per completare questo codelab, ti serviranno i seguenti account, servizi e strumenti:

  • Un account Google Cloud Platform con la fatturazione attivata
  • Una chiave API Google Maps Platform con l'API Maps JavaScript e Places abilitate
  • Conoscenza di base di JavaScript, HTML e CSS
  • Conoscenza di base di Node.js
  • Un editor di testo o un IDE a tua scelta

3. Configurazione iniziale

Configurare Google Maps Platform

Se non hai ancora un account Google Cloud e un progetto con la fatturazione abilitata, consulta la guida Introduzione a Google Maps Platform per creare un account di fatturazione e un progetto.

  1. Nella console Cloud, fai clic sul menu a discesa del progetto e seleziona il progetto che vuoi utilizzare per questo codelab.

e7ffad81d93745cd.png

  1. Abilita le API e gli SDK di Google Maps Platform richiesti per questo codelab nel Google Cloud Marketplace. Per farlo, segui i passaggi descritti in questo video o in questa documentazione.
  2. Genera una chiave API nella pagina Credenziali di Cloud Console. Puoi seguire la procedura descritta in questo video o in questa documentazione. Tutte le richieste a Google Maps Platform richiedono una chiave API.

Credenziali predefinite dell'applicazione

Utilizzerai l'SDK Firebase Admin per interagire con il tuo progetto Firebase e per inviare richieste all'API Places e dovrai fornire credenziali valide per il funzionamento.

Utilizzeremo l'autenticazione ADC (Credenziali predefinite automatiche) per autenticare il tuo server in modo che possa effettuare richieste. In alternativa (non consigliato), puoi creare un account di servizio e memorizzare le credenziali all'interno del codice.

Definizione: le credenziali predefinite dell'applicazione (ADC) sono un meccanismo fornito da Google Cloud per autenticare automaticamente le applicazioni senza gestire esplicitamente le credenziali. Cerca le credenziali in varie posizioni (ad esempio variabili di ambiente, file dell'account di servizio o server metadati Google Cloud) e utilizza la prima che trova.

  • Nel terminale, utilizza il comando seguente che consente alle tue applicazioni di accedere in modo sicuro alle risorse Google Cloud per conto dell'utente attualmente connesso:
gcloud auth application-default login
  • Creerai un file .env nella directory principale che specifica una variabile del progetto Google Cloud:
GOOGLE_CLOUD_PROJECT="your-project-id"

Crea un account di servizio

Credenziali

  • Fai clic sull'account di servizio creato
  • Vai alla scheda CHIAVI per creare una chiave > JSON > salva le credenziali JSON scaricate. Sposta il file xxx.json scaricato automaticamente nella cartella principale
  • (Capitolo successivo) Assegnagli il nome corretto nel file nodejs server.js (​​firebase-credentials.json)

4. Integrazione di Firebase App Check

Otterrai i dettagli di configurazione di Firebase e le chiavi secret reCAPTCHA.

Dovrai incollarli nell'applicazione di dimostrazione e avviare il server.

Creare un'applicazione in Firebase

SELECCIONA il progetto Google Cloud già creato (potrebbe essere necessario specificare: "Selezione della risorsa principale")"

a6d171c6d7e98087.png a16010ba102cc90b.png

  • Aggiungi un'applicazione dal menu in alto a sinistra (ingranaggio)

18e5a7993ad9ea53.png 4632158304652118.png

Codice di inizializzazione di Firebase

  • Salva il codice di inizializzazione di Firebase da incollare in script.js (capitolo successivo) per il lato client

f10dcf6f5027e9f0.png

  • Registra la tua app per consentire a Firebase di utilizzare i token reCAPTCHA v3

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

da7efe203ce4142c.png

  • Scegli reCAPTCHA → crea una chiave nel sito web reCAPTCHA (con i domini corretti configurati: localhost per lo sviluppo di app)

b47eab131617467.png e6bddef9d5cf5460.png

  • Incolla la chiave secret reCAPTCHA in Firebase App Check

a63bbd533a1b5437.png

  • Lo stato dell'app dovrebbe diventare verde

4f7962b527b78ee5.png

5. Applicazione demo

  • App web client: file HTML, JavaScript e CSS
  • Server: file Node.js
  • Ambiente (.env): chiavi API
  • Configurazione (app.yaml): impostazioni di deployment di Google App Engine

Configurazione di Node.js:

  • Naviga: apri il terminale e vai alla directory principale del progetto clonato.
  • Installa Node.js (se necessario): versione 18 o successive.
node -v  # Check installed version
  • Inizializza progetto:esegui il seguente comando per inizializzare un nuovo progetto Node.js, lasciando tutte le impostazioni predefinite:
npm init 
  • Installa dipendenze:utilizza il seguente comando per installare le dipendenze del progetto richieste:
npm install @googlemaps/places firebase-admin express axios dotenv

Configurazione: variabili di ambiente per il progetto Google Cloud

  • Creazione del file di ambiente:nella directory principale del progetto, crea un file denominato .env. Questo file memorizza dati di configurazione sensibili e non deve essere sottoposto a commit nel controllo della versione.
  • Compila le variabili di ambiente:apri il file .env e aggiungi le seguenti variabili, sostituendo i segnaposto con i valori effettivi del tuo progetto 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. Panoramica del codice

index.html

  • Carica le librerie Firebase per creare il token nell'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

  • Recupero delle chiavi API:recupera le chiavi API per Google Maps e Firebase App Check da un server di backend.
  • Inizializza Firebase:configura Firebase per l'autenticazione e la sicurezza. (Sostituisci configurazione → vedi Capitolo 4).

La durata di validità del token Firebase App Check, che può variare da 30 minuti a 7 giorni, è configurata nella console Firebase e non può essere modificata tentando di forzare un aggiornamento del token.

  • Attiva App Check:consente a Firebase App Check di verificare l'autenticità delle richieste in entrata.
  • Carica l'API Google Maps: carica dinamicamente la libreria JavaScript di Google Maps per visualizzare la mappa.
  • Inizializza la mappa:crea una mappa di Google centrata su una posizione predefinita.
  • Gestisce i clic sulla mappa: rileva i clic sulla mappa e aggiorna di conseguenza il punto centrale.
  • Esegue query sull'API Places:invia richieste a un'API di backend (/api/data) per recuperare informazioni su luoghi (ristoranti, parchi, bar) nelle vicinanze della posizione su cui è stato fatto clic, utilizzando Firebase App Check per l'autorizzazione.
  • Visualizza indicatori:traccia i dati recuperati sulla mappa sotto forma di indicatori, mostrando i relativi nomi e icone.
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

  • Carica le variabili di ambiente (chiavi API, ID progetto Google) da un file .env.
  • Avvia il server,in attesa di richieste su http://localhost:3000.
  • Inizializza l'SDK Firebase Admin utilizzando le credenziali predefinite dell'applicazione (ADC).
  • Riceve un token reCAPTCHA da script.js.
  • Verifica la validità del token ricevuto.
  • Se il token è valido, esegue una richiesta POST all'API Google Places con i parametri di ricerca inclusi.
  • Elabora e restituisce la risposta dall'API Places al client.
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. Eseguire l'applicazione

Nell'ambiente scelto, esegui il server dal terminale e vai alla pagina http://localhost:3000

npm start 

Un token viene creato come variabile globale, nascosto nella finestra del browser dell'utente e trasmesso al server per l'elaborazione. I dettagli del token sono disponibili nei log del server.

I dettagli sulle funzioni e sulla risposta del server alla richiesta di ricerca nelle vicinanze dell'API Places sono disponibili nei log del server.

Risoluzione dei problemi:

Assicurati che l'ID progetto Google sia coerente nella configurazione:

  • nel file .env (variabile GOOGLE_CLOUD_PROJECT)
  • nella configurazione gcloud del terminale:
gcloud config set project your-project-id
  • nella configurazione di reCAPTCHA

e6bddef9d5cf5460.png

  • nella configurazione di Firebase

7e17bfbcb8007763.png

Altro

  • Crea un token di debug che può essere utilizzato al posto della chiave di sito reCAPTCHA in script.js per scopi di test e risoluzione dei problemi.

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);
}
  • Se provi troppe autenticazioni non riuscite, ad esempio utilizzando una chiave di sito reCAPTCHA falsa, potrebbe essere attivato un rallentamento temporaneo.
FirebaseError: AppCheck: Requests throttled due to 403 error. Attempts allowed again after 01d:00m:00s (appCheck/throttled).

Credenziali ADC

  • Assicurati di utilizzare l'account gcloud corretto
gcloud auth login 
  • Assicurati che le librerie necessarie siano installate
npm install @googlemaps/places firebase-admin
  • Assicurati che la libreria Firebase sia caricata in server.js
const {GoogleAuth} = require('google-auth-library');
gcloud auth application-default login
  • Rappresentazione: credenziali ADC salvate
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
  • Infine, testa localmente l'ADC salvando il seguente script come test.js ed eseguendolo nel terminale: 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. Ecco fatto, ottimo lavoro.

Passaggi successivi

Deployment in App Engine:

  • Prepara il progetto per il deployment su Google App Engine apportando le modifiche di configurazione necessarie.
  • Utilizza lo strumento a riga di comando gcloud o la console App Engine per eseguire il deployment dell'applicazione.

Migliora Firebase Authentication:

  • Token predefiniti e token personalizzati:implementa i token personalizzati di Firebase per un utilizzo più approfondito dei servizi Firebase.
  • Durata del token:imposta durate dei token appropriate, più brevi per le operazioni sensibili (token Firebase personalizzato fino a un'ora), più lunghe per le sessioni generali (token reCAPTCHA: da 30 minuti a 7 ore).
  • Esplora le alternative a reCAPTCHA: valuta se DeviceCheck (iOS), SafetyNet (Android) o App Attest sono adatti alle tue esigenze di sicurezza.

Integra i prodotti Firebase:

  • Realtime Database o Firestore:se la tua applicazione ha bisogno di sincronizzazione dei dati in tempo reale o funzionalità offline, esegui l'integrazione con Realtime Database o Firestore.
  • Cloud Storage:utilizza Cloud Storage per archiviare e pubblicare contenuti generati dagli utenti, come immagini o video.
  • Autenticazione:utilizza Firebase Authentication per creare account utente, gestire le sessioni di accesso e gestire la reimpostazione delle password.

Espansione ai dispositivi mobili:

  • Android e iOS:se prevedi di avere un'app mobile, crea versioni sia per le piattaforme Android sia per quelle iOS.
  • SDK Firebase:utilizza gli SDK Firebase per Android e iOS per integrare facilmente le funzionalità di Firebase nelle tue app mobile.