Как проверять токены Проверки приложений из пользовательского бэкенда

С помощью App Check можно защитить сторонние серверные ресурсы, например собственный сервер. Для этого вам нужно сделать следующее:

  • Измените клиент приложения, чтобы он отправлял токен App Check вместе с каждым запросом к серверной части, как описано на страницах для iOS+, Android, веб-приложений, 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, сделайте это.

Затем в обработчиках конечных точек 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.
    ...

Проложить маршрут

Если вы ещё не установили Admin SDK для Go, сделайте это.

Затем в обработчиках конечных точек 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, например с сайта jwt.io, для проверки токенов Проверки приложений.

Логика проверки токена должна включать следующие шаги:

  1. Получите набор открытых веб-ключей JSON (JWK) для Проверки приложений Firebase из конечной точки JWKS Проверки приложений: https://firebaseappcheck.googleapis.com/v1/jwks
  2. Проверьте подпись токена Проверки приложений, чтобы убедиться, что он действителен.
  3. Убедитесь, что в заголовке токена используется алгоритм RS256.
  4. Убедитесь, что заголовок токена имеет тип JWT.
  5. Убедитесь, что токен выдан Проверкой приложений Firebase в вашем проекте.
  6. Убедитесь, что срок действия токена не истек.
  7. Убедитесь, что аудитория токена соответствует вашему проекту.
  8. Необязательно. Проверьте, совпадает ли субъект токена с идентификатором вашего приложения.

Возможности библиотек JWT могут различаться. Обязательно вручную выполните все шаги, которые не поддерживаются выбранной вами библиотекой.

В следующем примере необходимые действия выполняются в Ruby с использованием jwt gem в качестве промежуточного слоя 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

Защита от повторного воспроизведения (бета)

Чтобы защитить конечную точку от атак повторного воспроизведения, вы можете использовать токен Проверки приложений после его подтверждения, чтобы он использовался только один раз.

При использовании защиты от повторного воспроизведения к вызову проверки токена добавляется сетевой цикл, что увеличивает задержку для любой конечной точки, которая его использует. Поэтому мы рекомендуем включать защиту от повторного воспроизведения только для конечных точек, которые требуют повышенной безопасности.

Чтобы использовать защиту от повторного воспроизведения, выполните следующие действия:

  1. В Cloud Console назначьте роль Firebase App Check Token Verifier (Проверяющий токенов Проверки приложений Firebase) сервисному аккаунту, который используется для проверки токенов.

    • Если вы инициализировали Admin SDK с учетными данными сервисного аккаунта Admin SDK, скачанными из консоли Firebase, то необходимая роль уже назначена.
    • Если вы используете Cloud Functions первого поколения с конфигурацией Admin SDK по умолчанию, назначьте роль сервисному аккаунту App Engine по умолчанию. Подробнее о том, как изменить разрешения сервисного аккаунта…
    • Если вы используете Cloud Functions второго поколения с конфигурацией Admin SDK по умолчанию, назначьте роль сервисному аккаунту Compute Engine по умолчанию.
  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. Обратите внимание, что если параметр consume не задан, то verifyToken() не отклоняет использованный токен и даже не проверяет, использован ли он.

    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 и веб-ресурсов.