Memvalidasi permintaan Places API dengan Firebase AppCheck dan reCAPTCHA

1. Sebelum memulai

Untuk memastikan legitimasi pengguna yang berinteraksi dengan aplikasi web Anda, Anda akan menerapkan Firebase App Check, yang memanfaatkan token JWT reCAPTCHA untuk memverifikasi sesi pengguna. Penyiapan ini akan memungkinkan Anda menangani permintaan dari aplikasi klien ke Places API (Baru) dengan aman.

b40cfddb731786fa.png

Link Live

Yang akan Anda build.

Untuk mendemonstrasikan hal ini, Anda akan membuat aplikasi web yang menampilkan peta saat dimuat. Tindakan ini juga akan secara diam-diam membuat token reCAPTCHA menggunakan Firebase SDK. Token ini kemudian dikirim ke server Node.js Anda, tempat Firebase memvalidasinya sebelum memenuhi permintaan apa pun ke Places API.

Jika token valid, Firebase App Check akan menyimpannya hingga masa berlakunya habis, sehingga Anda tidak perlu membuat token baru untuk setiap permintaan klien. Jika token tidak valid, pengguna akan diminta untuk menyelesaikan verifikasi reCAPTCHA lagi guna mendapatkan token baru.

2. Prasyarat

Anda harus mempelajari item di bawah untuk menyelesaikan Codelab ini. daea823b6bc38b67.png

Produk Google Cloud yang Diperlukan

  • Google Cloud Firebase App Check: database untuk pengelolaan token
  • Google reCAPTCHA: pembuatan dan verifikasi token. CAPTCHA adalah alat yang digunakan untuk membedakan manusia dari bot di situs. Fitur ini berfungsi dengan menganalisis perilaku pengguna, atribut browser, dan informasi jaringan untuk menghasilkan skor yang menunjukkan kemungkinan pengguna tersebut adalah bot. Jika skornya cukup tinggi, pengguna dianggap manusia, dan Anda tidak perlu melakukan tindakan lebih lanjut. Jika skornya rendah, teka-teki CAPTCHA dapat ditampilkan untuk mengonfirmasi identitas pengguna. Pendekatan ini kurang mengganggu dibandingkan metode CAPTCHA tradisional, sehingga pengalaman pengguna menjadi lebih lancar.
  • (Opsional) Google Cloud App Engine: lingkungan deployment.

Produk Google Maps Platform yang Diperlukan

Dalam Codelab ini, Anda akan menggunakan produk Google Maps Platform berikut:

Persyaratan Lainnya untuk Codelab ini

Untuk menyelesaikan Codelab ini, Anda memerlukan akun, layanan, dan alat berikut:

  • Akun Google Cloud Platform dengan penagihan diaktifkan
  • Kunci API Google Maps Platform dengan Maps JavaScript API dan Places diaktifkan
  • Pengetahuan dasar tentang JavaScript, HTML, dan CSS
  • Pengetahuan dasar tentang Node.js
  • Editor teks atau IDE pilihan Anda

3. Memulai Persiapan

Menyiapkan Google Maps Platform

Jika Anda belum memiliki akun Google Cloud Platform dan project yang mengaktifkan penagihan, lihat panduan Memulai Google Maps Platform untuk membuat akun penagihan dan project.

  1. Di Cloud Console, klik menu drop-down project, lalu pilih project yang ingin Anda gunakan untuk codelab ini.

e7ffad81d93745cd.png

  1. Aktifkan API dan SDK Google Maps Platform yang diperlukan untuk codelab ini di Google Cloud Marketplace. Untuk melakukannya, ikuti langkah-langkah dalam video ini atau dokumentasi ini.
  2. Buat kunci API di halaman Kredensial di Cloud Console. Anda dapat mengikuti langkah-langkah dalam video ini atau dokumentasi ini. Semua permintaan ke Google Maps Platform memerlukan kunci API.

Kredensial Default Aplikasi

Anda akan menggunakan Firebase Admin SDK untuk berinteraksi dengan project Firebase serta membuat permintaan ke Places API dan harus memberikan kredensial yang valid agar dapat berfungsi.

Kami akan menggunakan Autentikasi ADC (Kredensial Default Otomatis) untuk mengautentikasi server Anda guna membuat permintaan. Atau (tidak direkomendasikan), Anda dapat membuat akun layanan dan menyimpan kredensial dalam kode.

Definisi: Kredensial Default Aplikasi (ADC) adalah mekanisme yang disediakan Google Cloud untuk mengautentikasi aplikasi Anda secara otomatis tanpa mengelola kredensial secara eksplisit. ADC mencari kredensial di berbagai lokasi (seperti variabel lingkungan, file akun layanan, atau server metadata Google Cloud) dan menggunakan kredensial pertama yang ditemukan.

  • Di Terminal, gunakan perintah di bawah ini yang memungkinkan aplikasi Anda mengakses resource Google Cloud dengan aman atas nama pengguna yang saat ini login:
gcloud auth application-default login
  • Anda akan membuat file .env di root yang menentukan variabel Project Google Cloud:
GOOGLE_CLOUD_PROJECT="your-project-id"

Membuat akun layanan

Kredensial

  • Klik akun layanan yang dibuat
  • Buka tab KUNCI untuk Membuat Kunci > JSON > simpan kredensial json yang didownload. Pindahkan file xxx.json yang otomatis didownload ke folder root Anda
  • (Bab Berikutnya) Beri nama dengan benar ke dalam file server.js nodejs (​​firebase-credentials.json)

4. Integrasi Firebase AppCheck

Anda akan mendapatkan detail konfigurasi Firebase dan kunci rahasia reCAPTCHA.

Anda akan menempelkannya ke aplikasi demo dan memulai server.

Membuat aplikasi di Firebase

SELECT project Google Cloud yang sudah dibuat (Anda mungkin harus menentukan: "Memilih resource induk")"

a6d171c6d7e98087.png a16010ba102cc90b.png

  • Menambahkan Aplikasi dari Menu kiri atas (roda gigi)

18e5a7993ad9ea53.png 4632158304652118.png

Kode inisialisasi Firebase

  • Menyimpan kode inisialisasi Firebase untuk ditempelkan di script.js (bab berikutnya) untuk sisi klien

f10dcf6f5027e9f0.png

  • Daftarkan aplikasi Anda untuk mengizinkan Firebase menggunakan token reCAPTCHA v3

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

da7efe203ce4142c.png

  • Pilih reCAPTCHA → buat kunci di situs reCAPTCHA (dengan domain yang tepat yang dikonfigurasi: localhost untuk developer aplikasi)

b47eab131617467.png e6bddef9d5cf5460.png

  • Menempelkan Secret reCAPTCHA di Firebase AppCheck

a63bbd533a1b5437.png

  • Status aplikasi akan berubah menjadi hijau

4f7962b527b78ee5.png

5. Aplikasi demo

  • Aplikasi Web Klien: File HTML, JavaScript, CSS
  • Server: File Node.js
  • Lingkungan (.env): Kunci API
  • Konfigurasi (app.yaml): Setelan deployment Google App Engine

Penyiapan Node.js:

  • Menavigasi: Buka terminal dan buka direktori utama project yang di-clone.
  • Instal Node.js (jika diperlukan): versi 18 atau yang lebih baru.
node -v  # Check installed version
  • Melakukan Inisialisasi Project: Jalankan perintah berikut untuk melakukan inisialisasi project Node.js baru, dengan membiarkan semua setelan sebagai default:
npm init 
  • Menginstal Dependensi: Gunakan perintah berikut untuk menginstal dependensi project yang diperlukan:
npm install @googlemaps/places firebase-admin express axios dotenv

Konfigurasi: Variabel Lingkungan untuk Project Google Cloud

  • Pembuatan File Lingkungan: Di direktori utama project Anda, buat file bernama .env. File ini akan menyimpan data konfigurasi sensitif dan tidak boleh di-commit ke kontrol versi.
  • Isi Variabel Lingkungan: Buka file .env dan tambahkan variabel berikut, dengan mengganti placeholder dengan nilai sebenarnya dari Project Google Cloud Anda:
# 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. Ringkasan kode

index.html

  • Memuat library firebase untuk membuat token di aplikasi
<!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

  • Mengambil Kunci API: Mengambil kunci API untuk Google Maps dan Firebase App Check dari server backend.
  • Melakukan inisialisasi Firebase: Menyiapkan Firebase untuk autentikasi dan keamanan. (Ganti konfigurasi → lihat Bab 4).

Durasi validitas token Firebase App Check, yang berkisar dari 30 menit hingga 7 hari, dikonfigurasi dalam Firebase console dan tidak dapat diubah dengan mencoba memaksa pembaruan token.

  • Mengaktifkan App Check: Mengaktifkan Firebase App Check untuk memverifikasi keaslian permintaan masuk.
  • Memuat Google Maps API: Memuat library JavaScript Google Maps secara dinamis untuk menampilkan peta.
  • Melakukan inisialisasi Peta: Membuat Google Maps yang berpusat di lokasi default.
  • Menangani Klik Peta: Mendeteksi klik pada peta dan memperbarui titik tengah yang sesuai.
  • Mengkueri Places API: Mengirim permintaan ke API backend (/api/data) untuk mengambil informasi tentang tempat (restoran, taman, bar) di dekat lokasi yang diklik, menggunakan Firebase App Check untuk otorisasi.
  • Menampilkan Penanda: Memetakan data yang diambil di peta sebagai penanda, yang menampilkan nama dan ikonnya.
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

  • Memuat variabel lingkungan (kunci API, project ID Google) dari file .env.
  • Memulai server, memproses permintaan di http://localhost:3000.
  • Melakukan inisialisasi Firebase Admin SDK menggunakan Kredensial Default Aplikasi (ADC).
  • Menerima token reCAPTCHA dari script.js.
  • Memverifikasi validitas token yang diterima.
  • Jika token valid, buat permintaan POST ke Google Places API dengan menyertakan parameter penelusuran.
  • Memproses dan menampilkan respons dari Places API ke klien.
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. Menjalankan aplikasi

Dari lingkungan yang Anda pilih, jalankan server dari terminal dan buka http://localhost:3000

npm start 

Token dibuat sebagai variabel global, disembunyikan dari jendela browser pengguna, dan dikirim ke server untuk diproses. Detail token dapat ditemukan di log server.

Detail tentang fungsi server dan respons terhadap permintaan Places API Nearby Search dapat ditemukan di log server.

Pemecahan masalah:

Pastikan Project ID Google konsisten dalam penyiapan:

  • dalam file .env (variabel GOOGLE_CLOUD_PROJECT)
  • di konfigurasi gcloud terminal:
gcloud config set project your-project-id
  • di penyiapan reCaptcha

e6bddef9d5cf5460.png

  • di penyiapan Firebase

7e17bfbcb8007763.png

Lainnya

  • Buat token debug yang dapat digunakan sebagai pengganti kunci situs reCAPTCHA dalam script.js untuk tujuan pengujian dan pemecahan masalah.

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);
}
  • Mencoba terlalu banyak autentikasi yang gagal, misalnya: menggunakan kunci situs recaptcha palsu, dapat memicu throttling sementara.
FirebaseError: AppCheck: Requests throttled due to 403 error. Attempts allowed again after 01d:00m:00s (appCheck/throttled).

Kredensial ADC

  • Pastikan Anda menggunakan akun gcloud yang benar
gcloud auth login 
  • Pastikan library yang diperlukan telah diinstal
npm install @googlemaps/places firebase-admin
  • Pastikan library Firebase dimuat di server.js
const {GoogleAuth} = require('google-auth-library');
gcloud auth application-default login
  • Meniru identitas: Kredensial ADC disimpan
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
  • Terakhir, uji ADC secara lokal, simpan skrip berikut sebagai test.js dan jalankan di 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. Selesai, bagus!

Langkah tindak lanjut

Deployment ke App Engine:

  • Siapkan project Anda untuk di-deploy ke Google App Engine, dengan melakukan perubahan konfigurasi yang diperlukan.
  • Gunakan alat command line gcloud atau konsol App Engine untuk men-deploy aplikasi Anda.

Meningkatkan Firebase Authentication:

  • Token Default vs. Kustom: Terapkan token kustom Firebase untuk penggunaan layanan Firebase yang lebih mendalam.
  • Masa Aktif Token: Tetapkan masa aktif token yang sesuai, lebih singkat untuk operasi sensitif (token Firebase kustom hingga satu jam), lebih lama untuk sesi umum (token reCAPTCHA: 30 menit hingga 7 jam).
  • Menjelajahi Alternatif reCAPTCHA: Selidiki apakah DeviceCheck (iOS), SafetyNet (Android), atau App Attest cocok untuk kebutuhan keamanan Anda.

Mengintegrasikan Produk Firebase:

  • Realtime Database atau Firestore: Jika aplikasi Anda memerlukan sinkronisasi data real-time atau kemampuan offline, integrasikan dengan Realtime Database atau Firestore.
  • Cloud Storage: Gunakan Cloud Storage untuk menyimpan dan menayangkan konten buatan pengguna seperti gambar atau video.
  • Autentikasi: Manfaatkan Firebase Authentication untuk membuat akun pengguna, mengelola sesi login, dan menangani reset sandi.

Luaskan ke Seluler:

  • Android dan iOS: Jika Anda berencana memiliki aplikasi seluler, buat versi untuk platform Android dan iOS.
  • Firebase SDK: Gunakan Firebase SDK untuk Android dan iOS guna mengintegrasikan fitur Firebase ke dalam aplikasi seluler Anda dengan lancar.