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

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

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

אימות טוקן

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

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

  • מאמתים את הטוקן של App Check באמצעות SDK לאדמינים.

    אם האימות מצליח, ה-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 SDK לאדמינים, התקינו אותו.

אחר כך, ב-handlers של נקודת הקצה ל-API, קוראים ל-app_check.verify_token() ודוחים את הבקשה אם היא נכשלת. בדוגמה הבאה, פונקציה עם ה-decorator‏ @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.
    ...

המשך

אם עדיין לא התקנתם את SDK לאדמינים ל-Go, התקינו אותו.

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

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

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

בדוגמה הבאה מפורטים השלבים הנדרשים ב-Ruby באמצעות ה-gem‏ jwt כשכבת תווכה של 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

הגנה מפני הפעלה מחדש (בטא)

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

השימוש בהגנה מפני הפעלה חוזרת מוסיף זמן הלוך ושוב ברשת לקריאה לאימות הטוקן, ולכן מוסיף זמן אחזור לכל נקודת קצה שמשתמשת בו. לכן מומלץ להפעיל הגנה מפני מתקפות Replay רק בנקודות קצה רגישות במיוחד.

כדי להשתמש בהגנה מפני מתקפות Replay:

  1. במסוף Cloud, מקצים את התפקיד Firebase App Check Token Verifier לחשבון השירות שמשמש לאימות טוקנים.

    • אם הפעלתם את SDK לאדמינים באמצעות פרטי הכניסה של חשבון השירות של SDK לאדמינים שהורדתם ממסוף Firebase, התפקיד הנדרש כבר הוקצה.
    • אם אתם משתמשים בפונקציות של Cloud Functions מהדור הראשון עם הגדרת ברירת המחדל של Admin SDK, אתם צריכים להקצות את התפקיד לחשבון השירות שמשמש כברירת המחדל של App Engine. שינוי ההרשאות של חשבון השירות
    • אם אתם משתמשים בפונקציות של Cloud Functions מהדור השני עם הגדרת ברירת המחדל של ה-SDK לאדמינים, הקצו את התפקיד לחשבון ברירת המחדל של שירות Compute.
  2. כדי לצרוך טוקן, מעבירים את { consume: true } (Node.js) או את consume=True (Python) לקריאה לאימות הטוקן, ואז בודקים את אובייקט התוצאה. אם כבר נעשה שימוש בטוקן, דוחים את הבקשה או מבצעים פעולה לתיקון, כמו דרישה מהגורם המבקש לעבור בדיקות אחרות:

    Node.js

    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 לא מוגדר.)

    Python

    app_check_claims = app_check.verify_token(app_check_token, consume=True)
    
    if app_check_claims.get("already_consumed"):
        return "App Check token already consumed", 401
    
    # If verify_token() succeeds and already_consumed is not set, okay to continue.
    

    הפעולה הזו מאמתת את הטוקן ומסמנת אותו כטוקן שנעשה בו שימוש. הפעלות עתידיות של app_check.verify_token(app_check_token, consume=True) באותו טוקן מגדירות את already_consumed לערך True. (שימו לב ש-app_check.verify_token() לא דוחה אסימון שנעשה בו שימוש, ואפילו לא בודקת אם נעשה בו שימוש אם consume לא מוגדרת).

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