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

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

  • צריך לשנות את לקוח האפליקציה כך שישלח טוקן App Check עם כל בקשה לשרת העורפי, כמו שמתואר בדפים בנושא iOS+‎,‏ Android,‏ web,‏ Flutter,‏ Unity או C++‎.
  • משנים את הקצה העורפי כך שתידרש טוקן 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, עכשיו זה הזמן.

לאחר מכן, ב-handlers של נקודות הקצה של ה-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 for 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. מקבלים את קבוצת מפתחות ה-JWK הציבוריים של 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 באמצעות jwtgem כשכבת ביניים של 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 אחרי האימות שלו, כדי שאפשר יהיה להשתמש בו רק פעם אחת.

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

כדי להשתמש בהגנה מפני הפעלה חוזרת:

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

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

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