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 (nouvelle).

b40cfddb731786fa.png

Lien vers le direct

Ce que vous allez créer

Pour illustrer cela, vous allez créer une application Web qui affiche une carte au chargement. Il génère é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 répondre aux requêtes envoyées à l'API Places.

Si le jeton est valide, Firebase App Check le stocke jusqu'à son expiration, ce qui élimine la nécessité de créer un nouveau jeton pour chaque requête client. Si le jeton n'est pas valide, l'utilisateur est invité à effectuer à 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 permettant de distinguer les humains des robots sur les sites Web. Il analyse le comportement des utilisateurs, les attributs du navigateur et les informations 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 humain et aucune autre action n'est requise. Si le score est faible, un puzzle 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 améliore l'expérience utilisateur.
  • (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 Cloud Console, 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 afin qu'il puisse envoyer des requêtes. Vous pouvez également (non 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 des identifiants dans différents emplacements (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, qui permet à 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 "CLÉS", cliquez sur "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

SELECT le projet Google Cloud déjà 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 (engrenage)

18e5a7993ad9ea53.png 4632158304652118.png

Code d'initialisation Firebase

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

f10dcf6f5027e9f0.png

  • Enregistrer 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 domaines appropriés configurés: localhost pour le développement d'applications)

b47eab131617467.png e6bddef9d5cf5460.png

  • Coller la clé secrète reCAPTCHA dans Firebase AppCheck

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:

  • Parcourir: 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 d'un 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.
  • Répondez aux 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 à sept jours, est configurée dans la console Firebase et ne peut pas être modifiée en essayant de forcer un actualisation du jeton.

  • Active App Check:permet à Firebase App Check de vérifier l'authenticité des requêtes entrantes.
  • Charge l'API Google Maps:charge de manière dynamique la bibliothèque JavaScript Google Maps pour afficher la carte.
  • Initialise la carte:crée une carte Google Maps 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) à proximité de l'emplacement cliqué, à l'aide d'une autorisation Firebase App Check.
  • Affichage des repères:affiche les données récupérées sur la carte sous forme de repères, avec leur nom et leur icône.
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,qui é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, envoie une requête POST à 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

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

npm start 

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

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

Dépannage :

Assurez-vous que l'ID de 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 de reCAPTCHA ;

e6bddef9d5cf5460.png

  • dans la configuration Firebase ;

7e17bfbcb8007763.png

Autre

  • Créez un jeton de débogage pouvant ê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 essayez trop d'authentifications infructueuses (par exemple, en utilisant une fausse clé de site reCAPTCHA), vous risquez de déclencher un débit limité 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
  • Usurpation d'identité: identifiants ADC enregistrés
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
  • Testez finalement l'ADC localement, 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. C'est tout, bravo !

Étapes de suivi

Déploiement dans 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éliorez Firebase Authentication:

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

Intégrez des produits Firebase:

  • Realtime Database ou Firestore:si votre application a besoin de synchroniser 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 l'application sur mobile :

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