אימות אסימונים של בדיקת אפליקציה בקצה עורפי בהתאמה אישית

אפשר להגן על משאבי האפליקציה שאינם של Firebase, כמו קצוות עורפיים שמתארחים באופן עצמאי, באמצעות App Check. כדי לעשות זאת, צריך לבצע את שתי הפעולות הבאות:

  • משנים את לקוח האפליקציה כך שישלח אסימון App Check יחד עם כל בקשה לקצה העורפי, כפי שמתואר בדפים של iOS+,‏ Android ו-אינטרנט.
  • משנים את הקצה העורפי כך שיחייב אסימון App Check תקף בכל בקשה, כפי שמתואר בדף הזה.

אימות טוקנים

כדי לאמת אסימוני App Check בקצה העורפי, מוסיפים לקצוות ה-API את הלוגיקה הבאה:

  • בודקים שכל בקשה כוללת אסימון App Check.

  • מאמתים את האסימון App Check באמצעות Admin SDK.

    אם האימות מסתיים בהצלחה, ה-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

אם עדיין לא התקנתם את Admin SDK ל-Go, עליכם לעשות זאת.

לאחר מכן, בטיפולי נקודות הקצה של ה-API, צריך לבצע קריאה ל-appcheck.Client.VerifyToken() ולהשיב על הבקשה בשלילה אם היא נכשלת. בדוגמה הבאה, פונקציית wrapper מוסיפה את הלוגיקה הזו למטפלים בנקודות הקצה:

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 למטרות כלליות, כמו זו שזמינה בכתובת jwt.io, כדי לאמת אסימוני App Check.

הלוגיקה של אימות האסימון צריכה להשלים את השלבים הבאים:

  1. מקבלים את קבוצת מפתחות ה-JWK (JSON Web Key) הציבוריים של Firebase App Check מנקודת הקצה של JWKS של App Check: https://firebaseappcheck.googleapis.com/v1/jwks
  2. מוודאים שהחתימה של אסימון App Check תקינה.
  3. חשוב לוודא שהכותרת של האסימון משתמשת באלגוריתם RS256.
  4. מוודאים שהכותרת של האסימון היא מסוג JWT.
  5. מוודאים שהאסימון הונפק על ידי Firebase App Check בפרויקט שלכם.
  6. מוודאים שתוקף הטוקן לא פג.
  7. מוודאים שהקהל של האסימון תואם לפרויקט.
  8. אופציונלי: בודקים שהנושא של האסימון תואם למזהה האפליקציה שלכם.

יכולות הספריות של JWT עשויות להיות שונות. חשוב להשלים באופן ידני את כל השלבים שלא מטופלים על ידי הספרייה שבחרתם.

בדוגמה הבאה מתוארים השלבים הנדרשים ב-Ruby באמצעות ה-gem‏ jwt בתור שכבת middleware של Rack.

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' לחשבון השירות שמשמש לאימות האסימונים.

    • אם אתם מאתחלים את Admin SDK באמצעות פרטי הכניסה של חשבון השירות של Admin SDK שהורדתם ממסוף Firebase, התפקיד הנדרש כבר הוקצה.
    • אם אתם משתמשים ב-Cloud Functions מדור ראשון עם הגדרת ברירת המחדל של Admin SDK, צריך להקצות את התפקיד לחשבון השירות שמוגדר כברירת מחדל ב-App Engine. מידע נוסף זמין במאמר שינוי ההרשאות של חשבון שירות.
    • אם אתם משתמשים ב-Cloud Functions מדור שני עם הגדרת ברירת המחדל של Admin SDK, מקצים את התפקיד לחשבון ברירת המחדל של שירות המחשוב.
  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 }) באותו אסימון יגדירו את alreadyConsumed לערך true. (שימו לב ש-verifyToken() לא דוחה אסימון שנעשה בו שימוש, ואפילו לא בודק אם הוא נוצל אם לא מגדירים את consume).

כשמפעילים את התכונה הזו בנקודת קצה מסוימת, צריך גם לעדכן את הקוד של לקוח האפליקציה כדי לקבל אסימונים לשימוש מוגבל לשימוש בנקודת הקצה. תוכלו לעיין במסמכים בצד הלקוח לפלטפורמות של Apple, ל-Android ול-אינטרנט.