ยืนยันโทเค็น App Check จากแบ็กเอนด์ที่กำหนดเอง

คุณสามารถใช้ App Check เพื่อปกป้องทรัพยากรแบ็กเอนด์ที่กำหนดเองซึ่งไม่ใช่ของ Google สำหรับแอปของคุณ เช่น แบ็กเอนด์ที่โฮสต์เอง โดยคุณจะต้องดำเนินการทั้ง 2 อย่างต่อไปนี้

  • แก้ไขไคลเอ็นต์แอปเพื่อส่งโทเค็น 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.
    ...

Go

หากยังไม่ได้ติดตั้ง Admin SDK สำหรับ Go ให้ติดตั้งก่อน

จากนั้นในเครื่องจัดการปลายทาง API ให้เรียกใช้ appcheck.Client.VerifyToken() และปฏิเสธคำขอหากไม่สำเร็จ ในตัวอย่างต่อไปนี้ ฟังก์ชัน Wrapper จะเพิ่มตรรกะนี้ลงในเครื่องมือจัดการปลายทาง

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

Java

หากยังไม่ได้ติดตั้ง Admin SDK สำหรับ Java (เวอร์ชัน 9.12.0 ขึ้นไป) ให้ติดตั้ง

จากนั้นในเครื่องจัดการปลายทาง API หรือตัวกรองคำขอ ให้เรียกใช้ FirebaseAppCheck.getInstance().verifyToken() (หรือ verifyTokenAsync()) และปฏิเสธคำขอหากไม่สำเร็จ ตัวอย่างต่อไปนี้ใช้ตัวกรอง Spring Boot เพื่อยืนยันโทเค็นสำหรับคำขอที่เข้ามา

import com.google.firebase.FirebaseApp;
import com.google.firebase.appcheck.FirebaseAppCheck;
import com.google.firebase.appcheck.FirebaseAppCheckException;
import com.google.firebase.appcheck.VerifyAppCheckTokenResponse;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.filter.OncePerRequestFilter;

@SpringBootApplication
@RestController
public class Application {

    public static void main(String[] args) {
        FirebaseApp.initializeApp();
        SpringApplication.run(Application.class, args);
    }

    @GetMapping("/yourApiEndpoint")
    public String yourApiEndpoint() {
        // Handle request.
        return "Success";
    }

    @Component
    public static class AppCheckFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(
                HttpServletRequest request,
                HttpServletResponse response,
                FilterChain filterChain) throws ServletException, IOException {
            String appCheckToken = request.getHeader("X-Firebase-AppCheck");
            if (appCheckToken == null || appCheckToken.isEmpty()) {
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
                return;
            }

            try {
                VerifyAppCheckTokenResponse appCheckResponse =
                    FirebaseAppCheck.getInstance().verifyToken(appCheckToken);
                // If verifyToken() succeeds, continue with the filter chain.
                filterChain.doFilter(request, response);
            } catch (FirebaseAppCheckException e) {
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
            }
        }
    }
}

อื่นๆ

หากแบ็กเอนด์เขียนด้วยภาษาอื่น คุณสามารถใช้ไลบรารี JWT อเนกประสงค์ เช่น ไลบรารีที่พบใน jwt.io เพื่อยืนยันโทเค็น App Check

ตรรกะการยืนยันโทเค็นต้องทำตามขั้นตอนต่อไปนี้ให้เสร็จสมบูรณ์

  1. รับชุด JSON Web Key (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 โดยใช้ 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

การป้องกันการเล่นซ้ำ (เบต้า)

หากต้องการปกป้องปลายทางจากการโจมตีแบบใช้โทเค็นซ้ำ (Replay) คุณสามารถใช้โทเค็น App Check หลังจากยืนยันแล้วเพื่อให้ใช้ได้เพียงครั้งเดียว

การใช้การป้องกันการเล่นซ้ำจะเพิ่มการส่งข้อมูลไปกลับในเครือข่ายไปยังการเรียกใช้การยืนยันโทเค็น จึงเพิ่มเวลาในการตอบสนองให้กับปลายทางที่ใช้การป้องกันดังกล่าว ด้วยเหตุนี้ เราขอแนะนำให้คุณเปิดใช้การป้องกันการเล่นซ้ำเฉพาะในปลายทางที่มีความละเอียดอ่อนเป็นพิเศษเท่านั้น

หากต้องการใช้การป้องกันการเล่นซ้ำ ให้ทำดังนี้

  1. ในคอนโซล Cloud ให้มอบบทบาท "Firebase App Check Token Verifier" ให้กับบัญชีบริการที่ใช้ในการยืนยันโทเค็น

    • หากคุณเริ่มต้น Admin SDK ด้วยข้อมูลเข้าสู่ระบบของบัญชีบริการ Admin SDK ที่ดาวน์โหลดจากคอนโซล Firebase ระบบจะให้สิทธิ์บทบาทที่จำเป็นอยู่แล้ว
    • หากคุณใช้ Cloud Functions รุ่นที่ 1 กับการกำหนดค่า Admin SDK เริ่มต้น ให้มอบบทบาทให้กับบัญชีบริการเริ่มต้นของ App Engine โปรดดูหัวข้อการเปลี่ยนสิทธิ์ของบัญชีบริการ
    • หากคุณใช้ Cloud Functions รุ่นที่ 2 ที่มีการกำหนดค่า Admin SDK เริ่มต้น ให้มอบบทบาทให้กับบัญชีบริการ Compute เริ่มต้น
  2. หากต้องการใช้โทเค็น ให้ส่ง { consume: true } (Node.js), consume=True (Python) หรือ VerifyAppCheckTokenOptions พร้อม setConsume(true) (Java) ไปยังการเรียกใช้การยืนยันโทเค็น หรือเรียกใช้ VerifyOneTimeToken() (Go) แล้วตรวจสอบออบเจ็กต์ผลลัพธ์ หากใช้โทเค็นไปแล้ว ให้ปฏิเสธคำขอหรือดำเนินการแก้ไข เช่น กำหนดให้ผู้เรียกใช้ต้องผ่านการตรวจสอบอื่นๆ ดังนี้

    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)

    Go

    decodedToken, err := appCheck.VerifyOneTimeToken(r.Context(), appCheckToken[0])
    if err != nil || (decodedToken.AlreadyConsumed != nil && *decodedToken.AlreadyConsumed) {
        w.WriteHeader(http.StatusUnauthorized)
        w.Write([]byte("Unauthorized."))
        return
    }
    
    // If VerifyOneTimeToken() succeeds and AlreadyConsumed is not true, okay to
    // continue with the provided handler.
    

    ซึ่งจะยืนยันโทเค็นแล้วแจ้งว่ามีการใช้โทเค็นแล้ว การเรียกใช้ appCheck.VerifyOneTimeToken() ในอนาคตบนโทเค็นเดียวกันจะตั้งค่า AlreadyConsumed เป็น true (โปรดทราบว่า appCheck.VerifyToken() จะไม่ปฏิเสธโทเค็นที่ใช้แล้วหรือแม้แต่ตรวจสอบว่ามีการใช้โทเค็นหรือไม่)

    Java

    VerifyAppCheckTokenOptions options = VerifyAppCheckTokenOptions.builder()
        .setConsume(true)
        .build();
    VerifyAppCheckTokenResponse appCheckResponse =
        FirebaseAppCheck.getInstance().verifyToken(appCheckToken, options);
    
    if (appCheckResponse.isAlreadyConsumed().orElse(false)) {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
        return;
    }
    
    // If verifyToken() succeeds and isAlreadyConsumed() is not true, continue
    // with the filter chain.
    

    ซึ่งจะยืนยันโทเค็นแล้วแจ้งว่ามีการใช้โทเค็นแล้ว การเรียกใช้ verifyToken(appCheckToken, options) ในอนาคตด้วย setConsume(true) ในโทเค็นเดียวกันจะแสดงผล Optional.of(true) จาก isAlreadyConsumed() (โปรดทราบว่า verifyToken() จะไม่ปฏิเสธโทเค็นที่ใช้แล้วหรือแม้แต่ตรวจสอบว่ามีการใช้โทเค็นหรือไม่หากไม่ได้ตั้งค่า setConsume(true))

เมื่อเปิดใช้ฟีเจอร์นี้สำหรับปลายทางหนึ่งๆ คุณต้องอัปเดตโค้ดไคลเอ็นต์ของแอปเพื่อรับโทเค็นที่จำกัดการใช้ซึ่งใช้ได้สำหรับใช้กับปลายทางด้วย ดูเอกสารประกอบฝั่งไคลเอ็นต์สำหรับแพลตฟอร์ม Apple, Android และเว็บ