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, implementerai Firebase App Check, sfruttando i token JWT reCAPTCHA per verificare le sessioni utente. Questa configurazione ti consentirà di gestire in modo sicuro le richieste dall'applicazione client all'API Places (New).

b40cfddb731786fa.png

Live Link

Cosa creerai.

Per dimostrarlo, creerai un'app web che visualizza una mappa al caricamento. Inoltre, genererà in modo discreto un token reCAPTCHA utilizzando l'SDK Firebase. Questo token viene quindi inviato al tuo server Node.js, dove Firebase lo convalida prima di soddisfare qualsiasi richiesta all'API Places.

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

2. Prerequisiti

Per completare questo codelab, devi familiarizzare con gli elementi riportati di seguito. daea823b6bc38b67.png

Prodotti Google Cloud richiesti

  • Google Cloud Firebase App Check: database per la gestione dei token
  • Google reCAPTCHA: creazione e verifica dei token. È uno strumento utilizzato per distinguere gli esseri umani dai bot sui siti web. Analizza il comportamento degli utenti, 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 rispetto ai metodi CAPTCHA tradizionali, il che rende l'esperienza utente più fluida.
  • (Facoltativo) Google Cloud App Engine: ambiente di deployment.

Prodotti Google Maps Platform richiesti

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

Configurare Google Maps Platform

Se non hai ancora un account Google Cloud Platform e un progetto con la fatturazione abilitata, consulta la guida Guida introduttiva 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 in 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 i passaggi descritti 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 effettuare richieste all'API Places e dovrai fornire credenziali valide per il suo funzionamento.

Utilizzeremo l'autenticazione ADC (credenziali predefinite automatiche) per autenticare il tuo server per effettuare richieste. In alternativa (non consigliato), puoi creare un service account e archiviare le credenziali nel 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 (come variabili di ambiente, file di service account o server di metadati Google Cloud) e utilizza le prime che trova.

  • Nel terminale, utilizza il comando riportato di seguito 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) Assegna il nome corretto al 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.

Li incollerai nell'applicazione demo e avvierai il server.

Crea un'applicazione in Firebase

SELEZIONA il progetto Google Cloud già creato (potresti dover specificare "Selezione della risorsa padre")"

a6d171c6d7e98087.png a16010ba102cc90b.png

  • Aggiungi un'applicazione dal menu in alto a sinistra (icona a forma di 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 giusti configurati: localhost per lo sviluppo di app)

b47eab131617467.png e6bddef9d5cf5460.png

  • Incolla il 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 questo comando per inizializzare un nuovo progetto Node.js, lasciando tutte le impostazioni predefinite:
npm init 
  • Installa le 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 memorizzerà dati di configurazione sensibili e non deve essere sottoposto al 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

  • Recupera 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 il capitolo 4).

La durata di validità del token Firebase App Check, compresa tra 30 minuti e 7 giorni, viene 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 di Google Maps:carica dinamicamente la libreria JavaScript di Google Maps per visualizzare la mappa.
  • Inizializza la mappa:crea una mappa Google centrata su una località predefinita.
  • Gestisce i clic sulla mappa:ascolta i clic sulla mappa e aggiorna il punto centrale di conseguenza.
  • Esegue query sull'API Places:invia richieste a un'API di backend (/api/data) per recuperare informazioni sui luoghi (ristoranti, parchi, bar) vicino alla posizione selezionata, utilizzando Firebase App Check per l'autorizzazione.
  • Mostra indicatori:traccia i dati recuperati sulla mappa come 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 delle 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, invia una richiesta POST all'API Google Places con i parametri di ricerca inclusi.
  • Elabora e restituisce la risposta dell'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

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

npm start 

Un token viene creato come variabile globale, nascosto dalla 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 del server e sulla risposta alla richiesta di Places API Nearby Search 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 del sito reCAPTCHA all'interno di 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);
}
  • Troppi tentativi di autenticazione non riusciti, ad esempio l'utilizzo di una chiave di sito reCAPTCHA falsa, possono attivare una limitazione temporanea.
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
  • Impersonate: ADC credentials saved
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
  • Infine, testa localmente ADC salvando lo script seguente 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. È tutto, 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.

Migliorare Firebase Authentication:

  • Token predefiniti e personalizzati:implementa i token personalizzati Firebase per un utilizzo più approfondito dei servizi Firebase.
  • Durata del token:imposta durate del 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 alternative a reCAPTCHA:verifica 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 della sincronizzazione dei dati in tempo reale o di 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 i reimpostazioni delle password.

Espandersi nel settore dei dispositivi mobili:

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