Weryfikowanie tokenów Sprawdzania aplikacji z niestandardowego backendu

Możesz używać App Check do ochrony niestandardowych zasobów backendu aplikacji, które nie należą do Google, takich jak Twój własny backend hostowany samodzielnie. Aby to zrobić, musisz wykonać obie te czynności:

  • Zmodyfikuj klienta aplikacji, aby wysyłał token App Check wraz z każdym żądaniem do backendu, zgodnie z opisem na stronach dotyczących iOS+, Androida, aplikacji internetowych, Fluttera, Unity lub C++.
  • Zmodyfikuj backend, aby wymagał prawidłowego tokena App Check przy każdym żądaniu, zgodnie z opisem na tej stronie.

Weryfikacja tokena

Aby zweryfikować tokeny App Check w backendzie, dodaj do punktów końcowych interfejsu API logikę, która:

  • Sprawdź, czy każde żądanie zawiera token App Check.

  • Zweryfikuj token App Check za pomocą pakietu Admin SDK.

    Jeśli weryfikacja się powiedzie, pakiet Admin SDK zwróci zdekodowany token App Check. Pomyślna weryfikacja oznacza, że token pochodzi z aplikacji należącej do Twojego projektu w Firebase.

Odrzuć wszystkie żądania, które nie przejdą którejkolwiek z tych kontroli. Przykład:

Node.js

Jeśli nie masz jeszcze zainstalowanego pakietu Admin SDK dla Node.js, zainstaluj go.

Następnie, używając oprogramowania pośredniczącego Express.js jako przykładu:

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

Jeśli nie masz jeszcze zainstalowanego pakietu Admin SDK dla Pythona, zainstaluj go.

Następnie w modułach obsługi punktów końcowych interfejsu API wywołaj funkcję app_check.verify_token() i odrzuć żądanie, jeśli się nie powiedzie. W tym przykładzie funkcja z dekoratorem @before_request wykonuje to zadanie dla wszystkich żądań:

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

Jeśli nie masz jeszcze zainstalowanego pakietu Admin SDK dla Go, zainstaluj go.

Następnie w modułach obsługi punktów końcowych interfejsu API wywołaj appcheck.Client.VerifyToken() i odrzuć żądanie, jeśli się nie powiedzie. W poniższym przykładzie funkcja opakowująca dodaje tę logikę do modułów obsługi punktów końcowych:

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.
}

Inne

Jeśli Twój backend jest napisany w innym języku, możesz użyć biblioteki JWT do zwykłych obciążeń, np. z witryny jwt.io, aby zweryfikować tokeny Sprawdzania aplikacji.

Logika weryfikacji tokena musi obejmować te czynności:

  1. Uzyskaj publiczny zestaw kluczy internetowych JSON (JWK) funkcji Sprawdzanie aplikacji Firebase z punktu końcowego JWKS funkcji Sprawdzanie aplikacji: https://firebaseappcheck.googleapis.com/v1/jwks
  2. Sprawdź podpis tokena Sprawdzania aplikacji, aby upewnić się, że jest on prawidłowy.
  3. Upewnij się, że nagłówek tokena używa algorytmu RS256.
  4. Upewnij się, że nagłówek tokena ma typ JWT.
  5. Upewnij się, że token został wydany przez Sprawdzanie aplikacji Firebase w Twoim projekcie.
  6. Upewnij się, że token nie wygasł.
  7. Upewnij się, że odbiorcy tokena pasują do Twojego projektu.
  8. Opcjonalnie: sprawdź, czy podmiot tokena jest zgodny z identyfikatorem aplikacji.

Możliwości bibliotek JWT mogą się różnić. Pamiętaj, aby ręcznie wykonać wszystkie kroki, których nie obsługuje wybrana biblioteka.

W poniższym przykładzie wykonujemy niezbędne czynności w języku Ruby, używając gemu jwt jako warstwy oprogramowania pośredniczącego 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

Ochrona przed ponownym odtworzeniem (beta)

Aby chronić punkt końcowy przed atakami typu replay, możesz wykorzystać token Sprawdzania aplikacji po jego zweryfikowaniu, aby można go było użyć tylko raz.

Korzystanie z ochrony przed powtórzeniem dodaje do wywołania weryfikacji tokena operację sieciową w obie strony, co zwiększa czas oczekiwania w przypadku każdego punktu końcowego, który korzysta z tej ochrony. Z tego powodu zalecamy włączanie ochrony przed powtórzeniami tylko w przypadku szczególnie wrażliwych punktów końcowych.

Aby korzystać z ochrony przed powtórzeniami:

  1. W konsoli Cloud przyznaj rolę „Weryfikator tokenów Sprawdzania aplikacji Firebase” kontu usługi używanemu do weryfikacji tokenów.

    • Jeśli pakiet Admin SDK został zainicjowany przy użyciu danych logowania konta usługi Admin SDK pobranych z konsoli Firebase, wymagana rola jest już przyznana.
    • Jeśli używasz Cloud Functions 1 generacji z domyślną konfiguracją pakietu Admin SDK, przypisz rolę do domyślnego konta usługi App Engine. Zobacz Zmienianie uprawnień konta usługi.
    • Jeśli używasz funkcji Cloud Functions drugiej generacji z domyślną konfiguracją pakietu Admin SDK, przypisz rolę do domyślnego konta usługi Compute.
  2. Aby wykorzystać token, przekaż { consume: true } (Node.js) lub consume=True (Python) do wywołania weryfikacji tokena, a następnie sprawdź obiekt wynikowy. Jeśli token został już wykorzystany, odrzuć żądanie lub podejmij działanie naprawcze, np. wymagaj od elementu wywołującego przejścia innych testów:

    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.
    

    Spowoduje to zweryfikowanie tokena, a następnie oznaczenie go jako wykorzystanego. Przyszłe wywołania verifyToken(appCheckToken, { consume: true }) na tym samym tokenie ustawiają alreadyConsumed na true. (Pamiętaj, że verifyToken() nie odrzuca wykorzystanego tokena ani nawet nie sprawdza, czy został on wykorzystany, jeśli nie ustawiono 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.
    

    Spowoduje to zweryfikowanie tokena, a następnie oznaczenie go jako wykorzystanego. Przyszłe wywołania app_check.verify_token(app_check_token, consume=True) na tym samym tokenie ustawiają already_consumed na True. (Pamiętaj, że app_check.verify_token() nie odrzuca wykorzystanego tokena ani nawet nie sprawdza, czy został on wykorzystany, jeśli nie ustawiono consume).

Gdy włączysz tę funkcję w przypadku konkretnego punktu końcowego, musisz też zaktualizować kod klienta aplikacji, aby uzyskać możliwe do wykorzystania tokeny o ograniczonym użyciu, które będą używane z tym punktem końcowym. Zapoznaj się z dokumentacją po stronie klienta dotyczącą platform Apple, Androida i internetu.