Valider les requêtes de l'API Places avec Firebase App Check et reCAPTCHA

1. Avant de commencer

Pour vous assurer de la légitimité des utilisateurs qui interagissent avec votre application Web, vous allez implémenter Firebase App Check en utilisant des jetons JWT reCAPTCHA pour valider les sessions utilisateur. Cette configuration vous permettra de gérer de manière sécurisée les requêtes de l'application cliente vers l'API Places (nouveau).

b40cfddb731786fa.png

Lien en direct

Objectifs de l'atelier

Pour illustrer cela, vous allez créer une application Web qui affiche une carte au chargement. Il générera également un jeton reCAPTCHA de manière discrète à l'aide du SDK Firebase. Ce jeton est ensuite envoyé à votre serveur Node.js, où Firebase le valide avant de traiter les requêtes adressées à l'API Places.

Si le jeton est valide, Firebase App Check le stocke jusqu'à son expiration, ce qui évite d'avoir à en créer un nouveau pour chaque requête client. Si le jeton n'est pas valide, l'utilisateur sera invité à effectuer de nouveau la validation reCAPTCHA pour obtenir un nouveau jeton.

2. Prérequis

Vous devez vous familiariser avec les éléments ci-dessous pour suivre cet atelier de programmation. daea823b6bc38b67.png

Produits Google Cloud requis

  • Google Cloud Firebase App Check : base de données pour la gestion des jetons
  • Google reCAPTCHA : création et validation de jetons. Il s'agit d'un outil utilisé pour distinguer les humains des robots sur les sites Web. Il fonctionne en analysant le comportement de l'utilisateur, les attributs du navigateur et les informations sur le réseau pour générer un score indiquant la probabilité que l'utilisateur soit un robot. Si le score est suffisamment élevé, l'utilisateur est considéré comme un humain et aucune autre action n'est requise. Si le score est faible, un CAPTCHA peut être présenté pour confirmer l'identité de l'utilisateur. Cette approche est moins intrusive que les méthodes CAPTCHA traditionnelles, ce qui rend l'expérience utilisateur plus fluide.
  • (Facultatif) Google Cloud App Engine : environnement de déploiement.

Produits Google Maps Platform requis

Dans cet atelier de programmation, vous utiliserez les produits Google Maps Platform suivants :

Autres conditions requises pour cet atelier de programmation

Pour suivre cet atelier de programmation, vous aurez besoin des comptes, des services et des outils suivants :

  • Compte Google Cloud Platform pour lequel la facturation est activée
  • Une clé API Google Maps Platform pour laquelle l'API Maps JavaScript et Places sont activées
  • Connaissances de base sur JavaScript, HTML et CSS
  • Connaissances de base de Node.js
  • Éditeur de texte ou IDE de votre choix

3. Préparer l'atelier

Configurer Google Maps Platform

Si vous ne disposez pas déjà d'un compte Google Cloud Platform et d'un projet sur lequel la facturation est activée, consultez le guide Premiers pas avec Google Maps Platform pour savoir comment créer un compte de facturation et un projet.

  1. Dans la console Cloud, cliquez sur le menu déroulant des projets, puis sélectionnez celui que vous souhaitez utiliser pour cet atelier de programmation.

e7ffad81d93745cd.png

  1. Activez les API et les SDK Google Maps Platform requis pour cet atelier de programmation depuis Google Cloud Marketplace. Pour ce faire, suivez les étapes indiquées dans cette vidéo ou cette documentation.
  2. Générez une clé API sur la page Identifiants de Cloud Console. Vous pouvez suivre la procédure décrite dans cette vidéo ou cette documentation. Toutes les requêtes envoyées à Google Maps Platform nécessitent une clé API.

Identifiants par défaut de l'application

Vous utiliserez le SDK Admin Firebase pour interagir avec votre projet Firebase et envoyer des requêtes à l'API Places. Vous devrez fournir des identifiants valides pour que cela fonctionne.

Nous utiliserons l'authentification ADC (Automatic Default Credentials) pour authentifier votre serveur et lui permettre d'envoyer des requêtes. Vous pouvez également (mais ce n'est pas recommandé) créer un compte de service et stocker les identifiants dans votre code.

Définition : les identifiants par défaut de l'application (ADC) sont un mécanisme fourni par Google Cloud pour authentifier automatiquement vos applications sans gérer explicitement les identifiants. Il recherche les identifiants à différents endroits (comme les variables d'environnement, les fichiers de compte de service ou le serveur de métadonnées Google Cloud) et utilise le premier qu'il trouve.

  • Dans votre terminal, utilisez la commande ci-dessous pour permettre à vos applications d'accéder de manière sécurisée aux ressources Google Cloud au nom de l'utilisateur actuellement connecté :
gcloud auth application-default login
  • Vous allez créer un fichier .env à la racine qui spécifie une variable de projet Google Cloud :
GOOGLE_CLOUD_PROJECT="your-project-id"

Créer un compte de service

Identifiants

  • Cliquez sur le compte de service créé.
  • Dans l'onglet "KEYS" (CLÉS), cliquez sur "Create a Key" (Créer une clé) > "JSON" > enregistrez les identifiants JSON téléchargés. Déplacez le fichier xxx.json téléchargé automatiquement dans votre dossier racine.
  • (Chapitre suivant) Nommez-le correctement dans le fichier nodejs server.js (​​firebase-credentials.json)

4. Intégration de Firebase App Check

Vous obtiendrez les détails de configuration Firebase et les clés secrètes reCAPTCHA.

Vous les collerez dans l'application de démonstration et démarrerez le serveur.

Créer une application dans Firebase

Sélectionnez le projet Google Cloud qui a déjà été créé (vous devrez peut-être spécifier "Sélectionner la ressource parente").

a6d171c6d7e98087.png a16010ba102cc90b.png

  • Ajouter une application depuis le menu en haut à gauche (icône en forme de roue dentée)

18e5a7993ad9ea53.png 4632158304652118.png

Code d'initialisation Firebase

  • Enregistrez le code d'initialisation Firebase pour le coller dans script.js (chapitre suivant) pour le côté client.

f10dcf6f5027e9f0.png

  • Enregistrez votre application pour autoriser Firebase à utiliser les jetons reCAPTCHA v3.

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

da7efe203ce4142c.png

  • Choisissez reCAPTCHA → créez une clé sur le site Web reCAPTCHA (avec les bons domaines configurés : localhost pour le développement d'applications).

b47eab131617467.png e6bddef9d5cf5460.png

  • Coller la clé secrète reCAPTCHA dans Firebase App Check

a63bbd533a1b5437.png

  • L'état de l'application doit passer au vert.

4f7962b527b78ee5.png

5. Application de démonstration

  • Application Web cliente : fichiers HTML, JavaScript et CSS
  • Serveur : fichier Node.js
  • Environnement (.env) : clés API
  • Configuration (app.yaml) : paramètres de déploiement Google App Engine

Configuration de Node.js :

  • Accédez : ouvrez votre terminal et accédez au répertoire racine de votre projet cloné.
  • Installez Node.js (si nécessaire) : version 18 ou ultérieure.
node -v  # Check installed version
  • Initialiser le projet : exécutez la commande suivante pour initialiser un projet Node.js, en laissant tous les paramètres par défaut :
npm init 
  • Installer les dépendances : utilisez la commande suivante pour installer les dépendances de projet requises :
npm install @googlemaps/places firebase-admin express axios dotenv

Configuration : variables d'environnement pour le projet Google Cloud

  • Création du fichier d'environnement : dans le répertoire racine de votre projet, créez un fichier nommé .env. Ce fichier stocke des données de configuration sensibles et ne doit pas être ajouté au contrôle des versions.
  • Renseignez les variables d'environnement : ouvrez le fichier .env et ajoutez les variables suivantes en remplaçant les espaces réservés par les valeurs réelles de votre projet 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. Présentation du code

index.html

  • Charge les bibliothèques Firebase pour créer le jeton dans l'application
<!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

  • Récupère les clés API : récupère les clés API pour Google Maps et Firebase App Check à partir d'un serveur backend.
  • Initialise Firebase : configure Firebase pour l'authentification et la sécurité. (Remplacer la configuration → voir le chapitre 4).

La durée de validité du jeton Firebase App Check, qui varie de 30 minutes à 7 jours, est configurée dans la console Firebase et ne peut pas être modifiée en tentant de forcer l'actualisation d'un jeton.

  • Active App Check : permet à Firebase App Check de vérifier l'authenticité des requêtes entrantes.
  • Charge l'API Google Maps : charge dynamiquement la bibliothèque JavaScript Google Maps pour afficher la carte.
  • Initialise la carte : crée une carte Google centrée sur un emplacement par défaut.
  • Gère les clics sur la carte : écoute les clics sur la carte et met à jour le point central en conséquence.
  • Interroge l'API Places : envoie des requêtes à une API backend (/api/data) pour récupérer des informations sur les lieux (restaurants, parcs, bars) situés à proximité de l'emplacement sélectionné, en utilisant Firebase App Check pour l'autorisation.
  • Afficher les repères : trace les données récupérées sur la carte sous forme de repères, en affichant leurs noms et leurs icônes.
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

  • Charge les variables d'environnement (clés API, ID de projet Google) à partir d'un fichier .env.
  • Démarre le serveur et écoute les requêtes sur http://localhost:3000.
  • Initialise le SDK Firebase Admin à l'aide des identifiants par défaut de l'application (ADC).
  • Reçoit un jeton reCAPTCHA de script.js.
  • Vérifie la validité du jeton reçu.
  • Si le jeton est valide, une requête POST est envoyée à l'API Google Places avec les paramètres de recherche inclus.
  • Traite et renvoie la réponse de l'API Places au 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. Exécuter l'application

Depuis l'environnement de votre choix, exécutez le serveur à partir du terminal et accédez à http://localhost:3000.

npm start 

Un jeton est créé en tant que variable globale, masqué dans la fenêtre du navigateur de l'utilisateur et transmis au serveur pour traitement. Vous trouverez des informations sur le jeton dans les journaux du serveur.

Vous trouverez des informations sur les fonctions du serveur et la réponse à la requête Nearby Search de l'API Places dans les journaux du serveur.

Dépannage :

Assurez-vous que l'ID du projet Google est cohérent dans la configuration :

  • dans le fichier .env (variable GOOGLE_CLOUD_PROJECT)
  • dans la configuration gcloud du terminal :
gcloud config set project your-project-id
  • dans la configuration reCAPTCHA.

e6bddef9d5cf5460.png

  • dans la configuration Firebase.

7e17bfbcb8007763.png

Autre

  • Créez un jeton de débogage qui peut être utilisé à la place de la clé de site reCAPTCHA dans script.js à des fins de test et de dépannage.

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);
}
  • Si vous effectuez trop de tentatives d'authentification infructueuses (par exemple, en utilisant une fausse clé de site reCAPTCHA), vous risquez de déclencher une limitation temporaire.
FirebaseError: AppCheck: Requests throttled due to 403 error. Attempts allowed again after 01d:00m:00s (appCheck/throttled).

Identifiants ADC

  • Vérifiez que vous êtes connecté au bon compte gcloud.
gcloud auth login 
  • Assurez-vous que les bibliothèques nécessaires sont installées.
npm install @googlemaps/places firebase-admin
  • Assurez-vous que la bibliothèque Firebase est chargée dans server.js.
const {GoogleAuth} = require('google-auth-library');
gcloud auth application-default login
  • Emprunter l'identité : identifiants ADC enregistrés
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
  • Enfin, testez localement ADC en enregistrant le script suivant sous le nom test.js et en l'exécutant dans le 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. Et voilà, bravo !

Étapes suivantes

Déploiement sur App Engine

  • Préparez votre projet pour le déploiement sur Google App Engine en apportant les modifications de configuration nécessaires.
  • Utilisez l'outil de ligne de commande gcloud ou la console App Engine pour déployer votre application.

Améliorer Firebase Authentication :

  • Jetons par défaut et jetons personnalisés : implémentez des jetons Firebase personnalisés pour une utilisation plus approfondie des services Firebase.
  • Durée de vie des jetons : définissez des durées de vie appropriées pour les jetons. Elles doivent être plus courtes pour les opérations sensibles (jeton Firebase personnalisé jusqu'à une heure) et plus longues pour les sessions générales (jeton reCAPTCHA : de 30 minutes à 7 heures).
  • Explorez des alternatives à reCAPTCHA : vérifiez si DeviceCheck (iOS), SafetyNet (Android) ou App Attest répondent à vos besoins en matière de sécurité.

Intégrer les produits Firebase :

  • Realtime Database ou Firestore : si votre application a besoin d'une synchronisation des données en temps réel ou de fonctionnalités hors connexion, intégrez-la à Realtime Database ou Firestore.
  • Cloud Storage : utilisez Cloud Storage pour stocker et diffuser du contenu généré par les utilisateurs, comme des images ou des vidéos.
  • Authentification : utilisez Firebase Authentication pour créer des comptes utilisateur, gérer les sessions de connexion et gérer la réinitialisation des mots de passe.

Développer votre activité sur les mobiles :

  • Android et iOS : si vous prévoyez de créer une application mobile, développez des versions pour les plates-formes Android et iOS.
  • SDK Firebase : utilisez les SDK Firebase pour Android et iOS afin d'intégrer facilement les fonctionnalités Firebase à vos applications mobiles.