커스텀 백엔드에서 앱 체크 토큰 확인

자체 호스팅 백엔드와 같은 앱의 Firebase 외의 리소스를 App Check로 보호할 수 있습니다. 이렇게 하려면 다음 두 가지 작업을 모두 수행해야 합니다.

  • iOS+, Android, 페이지에 설명된 대로 각 요청에 따라 App Check 토큰을 백엔드로 보내도록 앱 클라이언트를 수정합니다.
  • 이 페이지에 설명된 대로 모든 요청에 유효한 App Check 토큰을 요구하도록 백엔드를 수정합니다.

토큰 확인

백엔드에서 App Check 토큰을 확인하려면 다음을 수행하는 로직을 API 엔드포인트에 추가합니다.

  • 각 요청에 App Check 토큰이 포함되어 있는지 확인하세요.

  • Admin SDK를 사용하여 App Check 토큰을 확인합니다.

    확인에 성공하면 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

Go용 Admin SDK를 아직 설치하지 않았다면 설치합니다.

그런 다음 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.io에 있는 것과 같은 범용 JWT 라이브러리를 사용하여 앱 체크 토큰을 확인할 수 있습니다.

토큰 확인 로직은 다음 단계를 완료해야 합니다.

  1. 앱 체크 JWKS 엔드포인트에서 Firebase 앱 체크 공개 JSON 웹 키(JWK) 집합을 가져옵니다. https://firebaseappcheck.googleapis.com/v1/jwks
  2. 앱 체크 토큰의 서명이 합법적인지 확인합니다.
  3. 토큰의 헤더가 알고리즘 RS256을 사용하는지 확인합니다.
  4. 토큰의 헤더에 JWT 유형이 있는지 확인합니다.
  5. 토큰이 프로젝트의 Firebase 앱 체크에서 발급되었는지 확인합니다.
  6. 토큰이 만료되지 않았는지 확인합니다.
  7. 토큰의 잠재고객이 프로젝트와 일치하는지 확인합니다.
  8. 선택사항: 토큰의 제목이 앱의 앱 ID와 일치하는지 확인합니다.

JWT 라이브러리의 기능은 다를 수 있습니다. 선택한 라이브러리에서 처리하지 않는 모든 단계를 수동으로 완료해야 합니다.

다음 예시에서는 jwt gem을 Rack 미들웨어 레이어로 사용하여 Ruby에서 필요한 단계를 수행합니다.

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

재생 보호(베타)

재생 공격으로부터 엔드포인트를 보호하려면 앱 체크 토큰을 한 번만 사용할 수 있도록 확인한 후에 이를 소비하면 됩니다.

재생 보호 기능을 사용하면 verifyToken() 호출에 네트워크 왕복이 추가되므로 이 기능을 사용하는 모든 엔드포인트에 지연 시간이 추가됩니다. 따라서 특히 민감한 엔드포인트에만 재생 보호를 사용 설정하는 것이 좋습니다.

재생 보호를 사용하려면 다음을 수행합니다.

  1. Cloud 콘솔에서 토큰을 확인하는 데 사용되는 서비스 계정에 'Firebase 앱 체크 토큰 확인자' 역할을 부여합니다.

    • Firebase Console에서 다운로드한 Admin SDK 서비스 계정 사용자 인증 정보로 Admin SDK를 초기화한 경우 필수 역할이 이미 부여된 것입니다.
    • 1세대 Cloud Functions를 기본 Admin SDK 구성과 함께 사용하는 경우 App Engine 기본 서비스 계정에 역할을 부여합니다. 서비스 계정 권한 변경을 참조하세요.
    • 2세대 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 })를 호출하면 alreadyConsumedtrue로 설정됩니다. (verifyToken()은 소비된 토큰을 거부하지 않으며 consume이 설정되지 않은 경우 토큰이 소비되었는지 확인하지도 않습니다.)

특정 엔드포인트에 이 기능을 사용 설정하는 경우 앱 클라이언트 코드를 업데이트하여 엔드포인트에서 사용할 수 있는 사용 빈도가 제한된 소모성 토큰을 얻어야 합니다. Apple 플랫폼, Android, 에 대한 클라이언트 측 문서를 참조하세요.