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 を区別するために使用されるツールです。ユーザーの行動、ブラウザの属性、ネットワーク情報を分析して、ユーザーが 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
  • Google Cloud プロジェクト変数を指定する .env ファイルをルートに作成します。
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 初期化コード

  • 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.png e6bddef9d5cf5460.png

  • Firebase AppCheck に 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 Maps API を読み込む: Google Maps JavaScript ライブラリを動的に読み込み、地図を表示します。
  • 地図を初期化: デフォルトの場所を中心に Google マップを作成します。
  • 地図のクリックを処理する: 地図上のクリックをリッスンし、それに応じて中心点を更新します。
  • Places 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

  • .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. アプリケーションを実行します。

選択した環境で、ターミナルから server を実行し、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 機能をモバイルアプリにシームレスに統合します。