カスタム バックエンドから App Check トークンを検証する

App Check では、アプリで使用する Firebase 以外のリソース(セルフホスト バックエンドなど)も保護できます。そのためには、次の両方を行う必要があります。

  • iOS+Androidウェブのページで説明するように、バックエンドへのリクエストと一緒に App Check トークンを送信するようにアプリ クライアントを変更します。
  • このページで説明するように、すべてのリクエストで有効な App Check トークンを要求するようにバックエンドを変更します。

トークンの検証

App Check トークンをバックエンドで検証するには、次の処理を行うロジックを API エンドポイントに追加します。

  • 各リクエストに App Check トークンが含まれていることを確認します。

  • Admin SDK を使用して App Check トークンを検証します。

    検証に成功すると、Admin SDK はデコードされた App Check トークンを返します。検証に成功した場合、そのトークンは自社の Firebase プロジェクトに属するアプリからのものであることを意味します。

いずれかのチェックに合格しなかったリクエストは拒否してください。次に例を示します。

Node.js

Node.js Admin SDK をまだインストールしていない場合は、インストールします。

Express.js ミドルウェアを例として使用します。

import express from "express";
import { initializeApp } from "firebase-admin/app";
import { getAppCheck } from "firebase-admin/app-check";

const expressApp = express();
const firebaseApp = initializeApp();

const appCheckVerification = async (req, res, next) => {
    const appCheckToken = req.header("X-Firebase-AppCheck");

    if (!appCheckToken) {
        res.status(401);
        return next("Unauthorized");
    }

    try {
        const appCheckClaims = await getAppCheck().verifyToken(appCheckToken);

        // If verifyToken() succeeds, continue with the next middleware
        // function in the stack.
        return next();
    } catch (err) {
        res.status(401);
        return next("Unauthorized");
    }
}

expressApp.get("/yourApiEndpoint", [appCheckVerification], (req, res) => {
    // Handle request.
});

Python

Python Admin SDK をまだインストールしていない場合は、インストールします。

API エンドポイント ハンドラで app_check.verify_token() を呼び出し、失敗した場合はリクエストを拒否します。次の例では、@before_request で修飾された関数が、すべてのリクエストに対してこのタスクを実行します。

import firebase_admin
from firebase_admin import app_check
import flask
import jwt

firebase_app = firebase_admin.initialize_app()
flask_app = flask.Flask(__name__)

@flask_app.before_request
def verify_app_check() -> None:
    app_check_token = flask.request.headers.get("X-Firebase-AppCheck", default="")
    try:
        app_check_claims = app_check.verify_token(app_check_token)
        # If verify_token() succeeds, okay to continue to route handler.
    except (ValueError, jwt.exceptions.DecodeError):
        flask.abort(401)

@flask_app.route("/yourApiEndpoint")
def your_api_endpoint(request: flask.Request):
    # Handle request.
    ...

Go

Go 用の Admin SDK をまだインストールしていない場合は、インストールします。

API エンドポイント ハンドラで appcheck.Client.VerifyToken() を呼び出し、失敗した場合はリクエストを拒否します。次の例では、ラッパー関数がこのロジックをエンドポイント ハンドラに追加しています。

package main

import (
    "context"
    "log"
    "net/http"

    firebaseAdmin "firebase.google.com/go/v4"
    "firebase.google.com/go/v4/appcheck"
)

var (
    appCheck *appcheck.Client
)

func main() {
    app, err := firebaseAdmin.NewApp(context.Background(), nil)
    if err != nil {
        log.Fatalf("error initializing app: %v\n", err)
    }

    appCheck, err = app.AppCheck(context.Background())
    if err != nil {
        log.Fatalf("error initializing app: %v\n", err)
    }

    http.HandleFunc("/yourApiEndpoint", requireAppCheck(yourApiEndpointHandler))
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func requireAppCheck(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
    wrappedHandler := func(w http.ResponseWriter, r *http.Request) {
        appCheckToken, ok := r.Header[http.CanonicalHeaderKey("X-Firebase-AppCheck")]
        if !ok {
            w.WriteHeader(http.StatusUnauthorized)
            w.Write([]byte("Unauthorized."))
            return
        }

        _, err := appCheck.VerifyToken(appCheckToken[0])
        if err != nil {
            w.WriteHeader(http.StatusUnauthorized)
            w.Write([]byte("Unauthorized."))
            return
        }

        // If VerifyToken() succeeds, continue with the provided handler.
        handler(w, r)
    }
    return wrappedHandler
}

func yourApiEndpointHandler(w http.ResponseWriter, r *http.Request) {
    // Handle request.
}

その他

バックエンドが別の言語で記述されている場合は、jwt.io にあるような汎用 JWT ライブラリを使用して App Check トークンを検証できます。

トークン検証ロジックでは、次の手順を行う必要があります。

  1. App Check JWKS エンドポイント(https://firebaseappcheck.googleapis.com/v1/jwks)から Firebase App Check 公開 JSON ウェブキー(JWK)セットを取得します。
  2. App Check トークンの署名が正規のものであることを検証します。
  3. トークンのヘッダーで RS256 アルゴリズムが使用されていることを確認します。
  4. トークンのヘッダーのタイプが JWT であることを確認します。
  5. トークンが自社のプロジェクトの Firebase App Check によって発行されているものであることを確認します。
  6. トークンの有効期限が切れていないことを確認します。
  7. トークンのオーディエンスが自社のプロジェクトと一致していることを確認します。
  8. オプション: トークンの件名がアプリのアプリ ID と一致していることを確認します。

JWT ライブラリによって機能が異なる場合があります。選択したライブラリに含まれていない手順については、自身が記述するロジックで行ってください。

次の例は、Ruby で必要な手順を実行する方法を示しています。Rack ミドルウェア レイヤとして jwt gem を使用しています。

require 'json'
require 'jwt'
require 'net/http'
require 'uri'

class AppCheckVerification
def initialize(app, options = {})
    @app = app
    @project_number = options[:project_number]
end

def call(env)
    app_id = verify(env['HTTP_X_FIREBASE_APPCHECK'])
    return [401, { 'Content-Type' => 'text/plain' }, ['Unauthenticated']] unless app_id
    env['firebase.app'] = app_id
    @app.call(env)
end

def verify(token)
    return unless token

    # 1. Obtain the Firebase App Check Public Keys
    # Note: It is not recommended to hard code these keys as they rotate,
    # but you should cache them for up to 6 hours.
    uri = URI('https://firebaseappcheck.googleapis.com/v1/jwks')
    jwks = JSON(Net::HTTP.get(uri))

    # 2. Verify the signature on the App Check token
    payload, header = JWT.decode(token, nil, true, jwks: jwks, algorithms: 'RS256')

    # 3. Ensure the token's header uses the algorithm RS256
    return unless header['alg'] == 'RS256'

    # 4. Ensure the token's header has type JWT
    return unless header['typ'] == 'JWT'

    # 5. Ensure the token is issued by App Check
    return unless payload['iss'] == "https://firebaseappcheck.googleapis.com/#{@project_number}"

    # 6. Ensure the token is not expired
    return unless payload['exp'] > Time.new.to_i

    # 7. Ensure the token's audience matches your project
    return unless payload['aud'].include? "projects/#{@project_number}"

    # 8. The token's subject will be the app ID, you may optionally filter against
    # an allow list
    payload['sub']
rescue
end
end

class Application
def call(env)
    [200, { 'Content-Type' => 'text/plain' }, ["Hello app #{env['firebase.app']}"]]
end
end

use AppCheckVerification, project_number: 1234567890
run Application.new

リプレイ保護(ベータ版)

リプレイ攻撃からエンドポイントを保護するために、App Check トークンを検証後に使用済みにし、一度しか使用できないようにすることができます。

リプレイ保護を使用すると、verifyToken() 呼び出しにネットワークのラウンド トリップが一つ追加されるため、リプレイ保護を使用するすべてのエンドポイントでレイテンシが増加します。このため、リプレイ保護は特に機密性の高いエンドポイントのみで有効にすることをおすすめします。

リプレイ保護を使用するには、次の手順を行います。

  1. Cloud コンソールで、トークンの確認に使用するサービス アカウントに「Firebase App Check Token Verifier」ロールを付与します。

    • Firebase コンソールからダウンロードした Admin SDK サービス アカウントの認証情報で Admin SDK を初期化した場合は、必要なロールがすでに付与されています。
    • デフォルトの Admin SDK 構成で第 1 世代の Cloud Functions を使用している場合は、App Engine のデフォルト サービス アカウントにロールを付与します。詳しくは、サービス アカウントの権限を変更するをご覧ください。
    • デフォルトの Admin SDK 構成で第 2 世代の Cloud Functions を使用している場合は、デフォルトのコンピューティング サービス アカウントにロールを付与します。
  2. 次に、トークンを消費するため { consume: true }verifyToken() メソッドに渡し、結果オブジェクトを調べます。alreadyConsumed プロパティが true の場合は、リクエストを拒否するか、なんらかの対応処理(呼び出し元に他のチェックを満たすことを要求するなど)を行います。

    次に例を示します。

    const appCheckClaims = await getAppCheck().verifyToken(appCheckToken, { consume: true });
    
    if (appCheckClaims.alreadyConsumed) {
        res.status(401);
        return next('Unauthorized');
    }
    
    // If verifyToken() succeeds and alreadyConsumed is not set, okay to continue.
    

    これにより、トークンが検証され、使用済みとしてフラグが付けられます。今後、同じトークンに対して verifyToken(appCheckToken, { consume: true }) が呼び出されると、alreadyConsumedtrue に設定されます(consume が設定されていない場合、verifyToken() は使用済みトークンを拒否せず、トークンが使用済みかどうかの確認も行いません)。

特定のエンドポイントでこの機能を有効にするには、そのエンドポイントで使用済みにできる限定使用のトークンを取得するようにアプリのクライアント コードを変更する必要があります。Apple プラットフォームAndroidウェブのクライアントサイドのドキュメントをご覧ください。