Проверка запросов Places API с помощью Firebase AppCheck и reCAPTCHA

1. Прежде чем начать

Для обеспечения легитимности взаимодействия пользователей с вашим веб-приложением вам потребуется реализовать Firebase App Check, используя токены reCAPTCHA JWT для проверки пользовательских сессий. Эта настройка позволит вам безопасно обрабатывать запросы от клиентского приложения к Places API (новое) .

b40cfddb731786fa.png

Прямая ссылка

Что вы построите.

Для демонстрации этого вы создадите веб-приложение, которое отображает карту при загрузке. Оно также незаметно сгенерирует токен reCAPTCHA с помощью Firebase SDK. Затем этот токен отправляется на ваш сервер Node.js, где Firebase проверяет его перед выполнением любых запросов к Places API.

Если токен действителен, Firebase App Check сохранит его до истечения срока действия, что избавит от необходимости создавать новый токен для каждого запроса клиента. Если токен недействителен, пользователю будет предложено повторно пройти проверку reCAPTCHA для получения нового токена.

2. Предварительные требования

Для выполнения этого практического задания вам необходимо ознакомиться с перечисленными ниже пунктами. daea823b6bc38b67.png

Необходимые продукты Google Cloud

  • Проверка приложения Google Cloud Firebase : база данных для управления токенами.
  • Google reCAPTCHA : создание и проверка токенов. Это инструмент, используемый для различения людей и ботов на веб-сайтах. Он работает путем анализа поведения пользователя, атрибутов браузера и сетевой информации для генерации оценки, указывающей на вероятность того, что пользователь является ботом. Если оценка достаточно высока, пользователь считается человеком, и никаких дальнейших действий не требуется. Если оценка низка, может быть показана головоломка CAPTCHA для подтверждения личности пользователя. Этот подход менее интрузичен, чем традиционные методы CAPTCHA, что делает взаимодействие с пользователем более удобным.
  • (Необязательно) Google Cloud App Engine : среда развертывания.

Необходимые продукты платформы Google Maps

В этом практическом занятии вы будете использовать следующие продукты платформы Google Maps:

Другие требования для участия в этом практическом занятии.

Для выполнения этого практического задания вам понадобятся следующие учетные записи, сервисы и инструменты:

  • Учетная запись Google Cloud Platform с включенной функцией выставления счетов.
  • Ключ API платформы Google Maps с включенными функциями Maps JavaScript API и Places.
  • Базовые знания JavaScript, HTML и CSS.
  • Базовые знания Node.js
  • Текстовый редактор или IDE на ваш выбор.

3. Настройка

Настройка платформы Google Maps

Если у вас еще нет учетной записи Google Cloud Platform и проекта с включенной функцией выставления счетов, пожалуйста, ознакомьтесь с руководством «Начало работы с Google Maps Platform», чтобы создать учетную запись для выставления счетов и проект.

  1. В консоли Cloud Console щелкните раскрывающееся меню «Проект» и выберите проект, который вы хотите использовать для этого практического занятия.

e7ffad81d93745cd.png

  1. Включите необходимые для этого практического занятия API и SDK платформы Google Maps в Google Cloud Marketplace . Для этого выполните действия, показанные в этом видео или в этой документации .
  2. Сгенерируйте ключ API на странице «Учетные данные» в Cloud Console. Вы можете следовать инструкциям в этом видео или в этой документации . Для всех запросов к Google Maps Platform требуется ключ API.

Учетные данные приложения по умолчанию

Для взаимодействия с вашим проектом Firebase, а также для отправки запросов к Places API вам потребуется использовать Firebase Admin SDK, при этом необходимо будет предоставить действительные учетные данные.

Для аутентификации вашего сервера при отправке запросов мы будем использовать аутентификацию ADC (автоматические учетные данные по умолчанию). В качестве альтернативы (не рекомендуется) вы можете создать учетную запись службы и хранить учетные данные в своем коде.

Определение : Application Default Credentials (ADC) — это механизм, предоставляемый Google Cloud для автоматической аутентификации ваших приложений без явного управления учетными данными. Он ищет учетные данные в различных местах (например, в переменных среды, файлах учетных записей служб или на сервере метаданных Google Cloud) и использует первые найденные.

  • В терминале используйте следующую команду, которая позволит вашим приложениям безопасно получать доступ к ресурсам Google Cloud от имени текущего пользователя:
gcloud auth application-default login
  • Вам потребуется создать в корневой директории файл .env, в котором будет указана переменная проекта Google Cloud:
GOOGLE_CLOUD_PROJECT="your-project-id"

Создайте учетную запись службы

  • Вкладка «Платформа Google Maps» > «+Создать учетные данные» > Сервисный аккаунт
  • Добавьте роль администратора Firebase AppCheck, затем введите имя учетной записи службы, которое вы только что ввели, например: firebase-appcheck-codelab@yourproject.iam.gserviceaccount.com

Реквизиты для входа

  • Нажмите на созданный сервисный аккаунт
  • Перейдите во вкладку КЛЮЧИ, создайте ключ > JSON > сохраните загруженные учетные данные в формате JSON. Переместите автоматически загруженный файл xxx.json в корневую папку.
  • (Следующая глава) Правильно укажите имя файла server.js в Node.js (firebase-credentials.json)

4. Интеграция Firebase AppCheck

Вы получите данные для настройки Firebase и секретные ключи reCAPTCHA.

Вам нужно будет вставить их в демонстрационное приложение и запустить сервер.

Создайте приложение в Firebase.

Выберите уже созданный проект Google Cloud (возможно, потребуется указать: «Выбор родительского ресурса»).

a6d171c6d7e98087.pnga16010ba102cc90b.png

  • Добавить приложение можно через меню в верхнем левом углу (значок шестеренки).

18e5a7993ad9ea53.png4632158304652118.png

Код инициализации Firebase

  • Сохраните код инициализации Firebase , чтобы вставить его в файл script.js (в следующей главе) для клиентской части.

f10dcf6f5027e9f0.png

  • Зарегистрируйте свое приложение, чтобы разрешить Firebase использовать токены reCAPTCHA v3.

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

da7efe203ce4142c.png

  • Выберите reCAPTCHA → создайте ключ на веб-сайте reCAPTCHA (с правильно настроенными доменами: localhost для разработки приложений).

b47eab131617467.pnge6bddef9d5cf5460.png

  • Вставьте секретный ключ reCAPTCHA в Firebase AppCheck.

a63bbd533a1b5437.png

  • Статус приложения должен стать зеленым.

4f7962b527b78ee5.png

5. Демонстрационное приложение

  • Клиентское веб-приложение: HTML, JavaScript, CSS файлы
  • Сервер: файл Node.js
  • Окружение (.env): Ключи API
  • Конфигурация (app.yaml): Настройки развертывания Google App Engine

Настройка Node.js:

  • Перейдите в меню : откройте терминал и перейдите в корневой каталог клонированного проекта.
  • Установите Node.js (при необходимости) : версия 18 или выше.
node -v  # Check installed version
  • Инициализация проекта: Выполните следующую команду, чтобы инициализировать новый проект Node.js, оставив все настройки по умолчанию:
npm init 
  • Установка зависимостей: Для установки необходимых зависимостей проекта используйте следующую команду:
npm install @googlemaps/places firebase-admin express axios dotenv

Настройка: Переменные среды для проекта Google Cloud

  • Создание файла окружения: В корневом каталоге вашего проекта создайте файл с именем .env . В этом файле будут храниться конфиденциальные данные конфигурации, и его не следует добавлять в систему контроля версий.
  • Заполнение переменных окружения: Откройте файл .env и добавьте следующие переменные, заменив заполнители фактическими значениями из вашего проекта 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. Обзор кода

index.html

  • Загружает библиотеки Firebase для создания токена в приложении.
<!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>

скрипт.js

  • Получение ключей API: Извлекает ключи API для Google Maps и Firebase App Check с бэкэнд-сервера.
  • Инициализация Firebase: Настройка Firebase для аутентификации и безопасности. (Замените конфигурацию → см. главу 4).

Срок действия токена Firebase App Check, составляющий от 30 минут до 7 дней, настраивается в консоли Firebase и не может быть изменен путем попытки принудительного обновления токена.

  • Активирует проверку приложения: включает проверку приложения Firebase для подтверждения подлинности входящих запросов.
  • Загружает API Google Maps: динамически загружает библиотеку JavaScript Google Maps для отображения карты.
  • Инициализирует карту: создает карту Google с центром в заданном по умолчанию местоположении.
  • Обработка кликов по карте: отслеживает клики по карте и соответствующим образом обновляет центральную точку.
  • Запросы к API мест: отправляет запросы к бэкэнд-API ( /api/data ) для получения информации о местах (ресторанах, парках, барах) рядом с выбранным местоположением, используя Firebase App Check для авторизации.
  • Отображение маркеров: отображает полученные данные на карте в виде маркеров, показывая их названия и значки.
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

  • Загружает переменные окружения (ключи API, идентификатор проекта Google) из файла .env .
  • Запускает сервер, который принимает запросы по адресу http://localhost:3000 .
  • Инициализирует Firebase Admin SDK с использованием учетных данных приложения по умолчанию (ADC).
  • Получает токен reCAPTCHA от script.js .
  • Проверяет действительность полученного токена .
  • Если токен действителен, отправляется POST-запрос к API Google Places с указанием параметров поиска.
  • Обрабатывает и возвращает клиенту ответ от Places API.
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. Запустите приложение.

В выбранной вами среде запустите команду server в терминале и перейдите по адресу http://localhost:3000

npm start 

Токен создается как глобальная переменная, скрытая от окна браузера пользователя, и передается на сервер для обработки. Подробную информацию о токене можно найти в журналах сервера.

Подробную информацию о функциях сервера и ответе на запрос поиска поблизости через Places API можно найти в журналах сервера.

Поиск неисправностей:

Убедитесь, что идентификатор проекта Google в настройках указан корректно:

  • в файле .env (переменная GOOGLE_CLOUD_PROJECT)
  • в конфигурации gcloud в терминале:
gcloud config set project your-project-id
  • в настройках reCaptcha

e6bddef9d5cf5460.png

  • в настройках Firebase

7e17bfbcb8007763.png

Другой

  • Создайте отладочный токен, который можно использовать вместо ключа сайта reCAPTCHA в script.js для целей тестирования и устранения неполадок.

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);
}
  • Слишком много неудачных попыток аутентификации, например, использование неверного ключа сайта reCAPTCHA, может привести к временному ограничению скорости аутентификации.
FirebaseError: AppCheck: Requests throttled due to 403 error. Attempts allowed again after 01d:00m:00s (appCheck/throttled).

Учетные данные ADC

  • Убедитесь, что вы используете правильную учетную запись gcloud.
gcloud auth login 
  • Убедитесь, что установлены необходимые библиотеки.
npm install @googlemaps/places firebase-admin
  • Убедитесь, что в файле server.js загружена библиотека Firebase.
const {GoogleAuth} = require('google-auth-library');
gcloud auth application-default login
  • Имитация: учетные данные ADC сохранены.
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
  • В конечном итоге протестируйте ADC локально, сохранив следующий скрипт как test.js и запустив его в терминале: 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. Вот и всё, молодец!

Дальнейшие шаги

Развертывание в App Engine:

  • Подготовьте свой проект к развертыванию в Google App Engine, внеся необходимые изменения в конфигурацию.
  • Для развертывания приложения используйте инструмент командной строки gcloud или консоль App Engine.

Улучшение аутентификации Firebase:

  • Токены по умолчанию против пользовательских токенов: Внедрите пользовательские токены Firebase для более широкого использования сервисов Firebase.
  • Срок действия токена: установите соответствующее время действия токена: более короткое для конфиденциальных операций (пользовательский токен Firebase до одного часа), более длительное для обычных сессий (токен reCAPTCHA: от 30 минут до 7 часов).
  • Изучите альтернативы reCAPTCHA: выясните, подходят ли DeviceCheck (iOS), SafetyNet (Android) или App Attest для ваших потребностей в области безопасности.

Интеграция продуктов Firebase:

  • Realtime Database или Firestore: Если вашему приложению необходима синхронизация данных в реальном времени или работа в автономном режиме, интегрируйте его с Realtime Database или Firestore.
  • Облачное хранилище: Используйте облачное хранилище для хранения и предоставления пользовательского контента, такого как изображения или видео.
  • Аутентификация: Используйте Firebase Authentication для создания учетных записей пользователей, управления сеансами входа в систему и обработки сброса паролей.

Развернуть для мобильных устройств:

  • Android и iOS: Если вы планируете создать мобильное приложение, разработайте версии как для Android, так и для iOS.
  • SDK Firebase: Используйте SDK Firebase для Android и iOS, чтобы легко интегрировать функции Firebase в ваши мобильные приложения.