Firebase App Check と reCAPTCHA を使用して Places API リクエストを検証する

1. 始める前に

ウェブ アプリケーションを操作するユーザーの正当性を確認するには、Firebase App Check を実装し、reCAPTCHA JWT トークンを利用してユーザー セッションを検証します。この設定により、クライアント アプリケーションから Places API(新版)へのリクエストを安全に処理できます。

b40cfddb731786fa.png

公開リンク

作成するアプリの概要。

これを説明するために、読み込み時に地図を表示するウェブアプリを作成します。また、Firebase SDK を使用して reCAPTCHA トークンを密かに生成します。このトークンは Node.js サーバーに送信され、Firebase が検証してから Places API へのリクエストが処理されます。

トークンが有効な場合、Firebase App Check はトークンの有効期限が切れるまでトークンを保存するため、クライアント リクエストごとに新しいトークンを作成する必要がなくなります。トークンが無効な場合、ユーザーは新しいトークンを取得するために reCAPTCHA による確認を再度完了するよう求められます。

2. 前提条件

この Codelab を完了するには、以下の内容を理解しておく必要があります。daea823b6bc38b67.png

必要な Google Cloud プロダクト

  • Google Cloud Firebase App Check: トークン管理用のデータベース
  • Google reCAPTCHA: トークンの作成と検証。ウェブサイトで人間と bot を区別するために使用されるツールです。ユーザーの行動、ブラウザの属性、ネットワーク情報を分析して、ユーザーがボットである可能性を示すスコアを生成します。スコアが十分に高い場合、ユーザーは人間と見なされ、それ以上の対応は必要ありません。スコアが低い場合は、ユーザーの身元を確認するために CAPTCHA パズルが表示されることがあります。このアプローチは、従来の CAPTCHA 方式よりもユーザーの負担が少なく、ユーザー エクスペリエンスが向上します。
  • (省略可)Google Cloud App Engine: デプロイ環境。

必要な Google Maps Platform プロダクト

この Codelab では次の Google Maps Platform サービスを使用します。

この Codelab のその他の要件

この Codelab を完了するには、次のアカウント、サービス、ツールが必要です。

  • 課金が有効になっている Google Cloud Platform アカウント
  • Maps JavaScript API と Places を有効にした Google Maps Platform API キー
  • JavaScript、HTML、CSS に関する基礎的な知識
  • Node.js の基本的な知識
  • 任意のテキスト エディタまたは IDE

3. 準備

Google Maps Platform を設定する

課金を有効にした Google Cloud Platform アカウントとプロジェクトをまだ作成していない場合は、Google Maps Platform スタートガイドに沿って請求先アカウントとプロジェクトを作成してください。

  1. Cloud Console で、プロジェクトのプルダウン メニューをクリックし、この Codelab に使用するプロジェクトを選択します。

e7ffad81d93745cd.png

  1. Google Cloud Marketplace でこの Codelab に必要な Google Maps Platform API と SDK を有効にします。詳しい手順については、こちらの動画またはドキュメントをご覧ください。
  2. Cloud Console の [認証情報] ページで API キーを生成します。詳しい手順については、こちらの動画またはドキュメントをご覧ください。Google Maps Platform へのすべてのリクエストで API キーが必要になります。

アプリケーションのデフォルト認証情報

Firebase Admin SDK を使用して Firebase プロジェクトを操作し、Places API にリクエストを送信します。このため、有効な認証情報を提供する必要があります。

ADC 認証(自動デフォルト認証情報)を使用して、リクエストを行うサーバーを認証します。または(推奨されません)、サービス アカウントを作成して、コード内に認証情報を保存することもできます。

定義: アプリケーションのデフォルト認証情報(ADC)は、認証情報を明示的に管理することなくアプリケーションを自動的に認証するために Google Cloud が提供するメカニズムです。さまざまな場所(環境変数、サービス アカウント ファイル、Google Cloud メタデータ サーバーなど)で認証情報を検索し、最初に見つかったものを使用します。

  • ターミナルで、現在ログインしているユーザーに代わってアプリケーションが Google Cloud リソースに安全にアクセスできるようにする次のコマンドを使用します。
gcloud auth application-default login
  • ルートに .env ファイルを作成し、Google Cloud プロジェクト変数を指定します。
GOOGLE_CLOUD_PROJECT="your-project-id"

サービス アカウントを作成する

認証情報

  • 作成したサービス アカウントをクリックします。
  • [KEYS] タブに移動して、[Create a Key] > [JSON] を選択し、ダウンロードした JSON 認証情報を保存します。自動的にダウンロードされた xxx.json ファイルをルートフォルダに移動します。
  • (次の章)nodejs ファイル server.js(firebase-credentials.json)に正しい名前を付けます。

4. Firebase AppCheck との統合

Firebase 構成の詳細と reCAPTCHA 秘密鍵を取得します。

これらをデモ アプリケーションに貼り付けて、サーバーを起動します。

Firebase でアプリケーションを作成する

すでに作成されている Google Cloud プロジェクトを選択します(「親リソースの選択」を指定する必要がある場合があります)。

a6d171c6d7e98087.png a16010ba102cc90b.png

  • 左上のメニュー(歯車アイコン)からアプリケーションを追加する

18e5a7993ad9ea53.png 4632158304652118.png

Firebase 初期化コード

  • クライアント サイドの script.js(次の章)に貼り付ける Firebase 初期化コードを保存します。

f10dcf6f5027e9f0.png

  • Firebase で reCAPTCHA v3 トークンを使用できるようにアプリを登録する

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

da7efe203ce4142c.png

  • reCAPTCHA ウェブサイトで [reCAPTCHA] → [キーを作成] を選択します(正しいドメインが構成されていること: アプリ開発の場合は localhost)。

b47eab131617467.png e6bddef9d5cf5460.png

  • Firebase App Check に reCAPTCHA シークレットを貼り付けます

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 という名前のファイルを作成します。このファイルには機密性の高い構成データが保存されるため、バージョン管理に commit しないでください。
  • 環境変数を入力する: .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>

script.js

  • API キーを取得する: Google マップと Firebase App Check の API キーをバックエンド サーバーから取得します。
  • Firebase を初期化する: 認証とセキュリティのために Firebase を設定します。(構成の置換 → 第 4 章を参照)。

Firebase App Check トークンの有効期間(30 分~ 7 日)は Firebase コンソールで構成され、トークンの更新を強制的に試みても変更できません。

  • App Check を有効にする: Firebase App Check が受信リクエストの信頼性を検証できるようにします。
  • Google マップ API を読み込む: Google Maps JavaScript ライブラリを動的に読み込んで地図を表示します。
  • 地図を初期化する: デフォルトの場所を中心とする Google マップを作成します。
  • 地図のクリックを処理する: 地図上のクリックをリッスンし、それに応じて中心点を更新します。
  • Places API をクエリ: Firebase App Check を使用して、クリックされた場所の近くにある場所(レストラン、公園、バー)に関する情報を取得するために、バックエンド API(/api/data)にリクエストを送信します。
  • マーカーを表示: 取得したデータを地図上にマーカーとしてプロットし、名前とアイコンを表示します。
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

  • .env ファイルから環境変数(API キー、Google プロジェクト ID)を読み込みます。
  • サーバーを起動し、http://localhost:3000 でリクエストをリッスンします。
  • アプリケーションのデフォルト認証情報(ADC)を使用して Firebase Admin SDK を初期化します。
  • script.js から reCAPTCHA トークンを受け取ります
  • 受信したトークンの有効性を検証します。
  • トークンが有効な場合、検索パラメータを含めて Google Places API に POST リクエストを送信します
  • 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. アプリケーションを実行する

選択した環境で、ターミナルからサーバーを実行し、http://localhost:3000 に移動します。

npm start 

トークンはグローバル変数として作成され、ユーザーのブラウザ ウィンドウには表示されず、処理のためにサーバーに送信されます。トークンの詳細はサーバーログで確認できます。

サーバーの機能と Places API の Nearby Search リクエストに対するレスポンスの詳細については、サーバーログをご覧ください。

トラブルシューティング:

設定で Google プロジェクト ID が一貫していることを確認します。

  • .env ファイル(GOOGLE_CLOUD_PROJECT 変数)
  • ターミナルの gcloud 構成:
gcloud config set project your-project-id
  • reCaptcha の設定

e6bddef9d5cf5460.png

  • Firebase の設定

7e17bfbcb8007763.png

その他

  • テストとトラブルシューティングの目的で、script.js 内の reCAPTCHA サイトキーの代わりに使用できるデバッグ トークンを作成します。

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 Authentication を強化する:

  • デフォルト トークンとカスタム トークン: Firebase サービスをより深く使用するために、Firebase カスタム トークンを実装します。
  • トークンの有効期間: 適切なトークンの有効期間を設定します。機密性の高いオペレーションでは短く(カスタム Firebase トークンは最大 1 時間)、一般的なセッションでは長く(reCAPTCHA トークンは 30 分~ 7 時間)します。
  • reCAPTCHA の代替手段を検討する: DeviceCheck(iOS)、SafetyNet(Android)、App Attest がセキュリティ ニーズに適しているかどうかを検討します。

Firebase プロダクトを統合する:

  • Realtime Database または Firestore: アプリケーションでリアルタイムのデータ同期またはオフライン機能が必要な場合は、Realtime Database または Firestore と統合します。
  • Cloud Storage: Cloud Storage を使用して、画像や動画などのユーザー作成コンテンツを保存、提供します。
  • 認証: Firebase Authentication を活用して、ユーザー アカウントの作成、ログイン セッションの管理、パスワードの再設定を行います。

モバイルに拡大:

  • Android と iOS: モバイルアプリをリリースする予定がある場合は、Android と iOS の両方のプラットフォーム向けにバージョンを作成します。
  • Firebase SDK: Android と iOS 向けの Firebase SDK を使用して、Firebase の機能をモバイルアプリにシームレスに統合します。