ปรับแต่งขั้นตอนการยืนยันหมายเลขโทรศัพท์ด้วย Firebase ใน Android

หน้าเริ่มต้นใช้งาน Firebase Phone Number Verification จะอธิบายวิธีผสานรวมกับ Firebase PNV โดยใช้วิธี getVerifiedPhoneNumber() ซึ่งจัดการโฟลว์ Firebase PNV ทั้งหมด ตั้งแต่การขอรับความยินยอมจากผู้ใช้ไปจนถึงการโทรผ่านเครือข่ายที่จำเป็นไปยังแบ็กเอนด์ของ Firebase PNV

เราขอแนะนำให้ใช้ API แบบเมธอดเดียว (getVerifiedPhoneNumber()) สำหรับนักพัฒนาแอปส่วนใหญ่ อย่างไรก็ตาม หากคุณต้องการควบคุมการโต้ตอบกับ เครื่องมือจัดการข้อมูลเข้าสู่ระบบของ Android อย่างละเอียด เช่น เพื่อขอข้อมูลเข้าสู่ระบบอื่นๆ พร้อมกับ หมายเลขโทรศัพท์ Firebase PNV ไลบรารียังมี 2 วิธีต่อไปนี้ ซึ่งแต่ละวิธีจะจัดการ การโต้ตอบที่แตกต่างกันกับแบ็กเอนด์ของ Firebase PNV

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

ในระหว่างการเรียกใช้แต่ละเมธอด คุณมีหน้าที่รับผิดชอบในการจัดการการโต้ตอบกับ API ของเครื่องมือจัดการข้อมูลเข้าสู่ระบบของ Android หน้านี้จะแสดงภาพรวม ของวิธีใช้ขั้นตอน 3 ส่วนนี้

ก่อนเริ่มต้น

ตั้งค่าโปรเจ็กต์ Firebase และนำเข้าFirebase PNVการอ้างอิงตามที่อธิบายไว้ ในหน้าเริ่มต้นใช้งาน

1. รับเพย์โหลดคำขอข้อมูลประจำตัวดิจิทัล

เรียกใช้เมธอด getDigitalCredentialPayload() เพื่อสร้างคำขอหมายเลขโทรศัพท์ของอุปกรณ์ ในขั้นตอนถัดไป คำขอนี้จะเป็นเพย์โหลดของ การโต้ตอบกับ Credential Manager API

// This instance does not require an Activity context.
val fpnv = FirebasePhoneNumberVerification.getInstance()

// Your request should include a nonce, which will propagate through the flow
// and be present in the final response from FPNV. See the section "Verifying
// the Firebase PNV token" for details on generating and verifying this.
val nonce = fetchNonceFromYourServer()

fpnv.getDigitalCredentialPayload(nonce, "https://example.com/privacy-policy")
  .addOnSuccessListener { fpnvDigitalCredentialPayload ->
    // Use the payload in the next step.
    // ...
  }
  .addOnFailureListener { e -> /* Handle payload fetch failure */ }

2. ส่งคำขอการรับรองทางดิจิทัลโดยใช้เครื่องมือจัดการข้อมูลเข้าสู่ระบบ

จากนั้นส่งคำขอไปยังเครื่องมือจัดการข้อมูลเข้าสู่ระบบ

โดยคุณต้องรวมเพย์โหลดคำขอไว้ในคำขอ DigitalCredential API คำขอนี้ต้องมี Nonce เดียวกับที่คุณส่งไปยัง getDigitalCredentialPayload()

// This example uses string interpolation for clarity, but you should use some kind of type-safe
// serialization method.
fun buildDigitalCredentialRequestJson(nonce: String, fpnvDigitalCredentialPayload: String) = """
    {
      "requests": [
        {
          "protocol": "openid4vp-v1-unsigned",
          "data": {
            "response_type": "vp_token",
            "response_mode": "dc_api",
            "nonce": "$nonce",
            "dcql_query": { "credentials": [$fpnvDigitalCredentialPayload] }
          }
        }
      ]
    }
""".trimIndent()

เมื่อดำเนินการดังกล่าวแล้ว คุณจะส่งคำขอได้โดยใช้ Credential Manager API ดังนี้

suspend fun makeFpnvRequest(
  context: Activity, nonce: String, fpnvDigitalCredentialPayload: String): GetCredentialResponse {
  // Helper function to build the digital credential request (defined above).
  // Pass the same nonce you passed to getDigitalCredentialPayload().
  val digitalCredentialRequestJson =
    buildDigitalCredentialRequestJson(nonce, fpnvDigitalCredentialPayload)

  // CredentialManager requires an Activity context.
  val credentialManager = CredentialManager.create(context)

  // Build a Credential Manager request that includes the Firebase PNV option. Note that
  // you can't combine the digital credential option with other options.
  val request = GetCredentialRequest.Builder()
    .addCredentialOption(GetDigitalCredentialOption(digitalCredentialRequestJson))
    .build()

  // getCredential is a suspend function, so it must run in a coroutine scope,
  val cmResponse: GetCredentialResponse = try {
    credentialManager.getCredential(context, request)
  } catch (e: GetCredentialException) {
    // If the user cancels the operation, the feature isn't available, or the
    // SIM doesn't support the feature, a GetCredentialCancellationException
    // will be returned. Otherwise, a GetCredentialUnsupportedException will
    // be returned with details in the exception message.
    throw e
  }
  return cmResponse
}

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

val dcApiResponse = extractApiResponse(cmResponse)
fun extractApiResponse(response: GetCredentialResponse): String {
  val credential = response.credential
  when (credential) {
    is DigitalCredential -> {
      val json = JSONObject(credential.credentialJson)
      val firebaseJwtArray =
          json.getJSONObject("data").getJSONObject("vp_token").getJSONArray("firebase")
      return firebaseJwtArray.getString(0)

    }
    else -> {
      // Handle any unrecognized credential type here.
      Log.e(TAG, "Unexpected type of credential ${credential.type}")
    }
  }
}

3. แลกเปลี่ยนการตอบกลับของข้อมูลเข้าสู่ระบบดิจิทัลเป็นโทเค็น Firebase PNV

สุดท้าย ให้เรียกใช้เมธอด exchangeCredentialResponseForPhoneNumber() เพื่อ แลกเปลี่ยนการตอบกลับของข้อมูลเข้าสู่ระบบดิจิทัลเป็นหมายเลขโทรศัพท์ที่ยืนยันแล้วและโทเค็น Firebase PNV ดังนี้

fpnv.exchangeCredentialResponseForPhoneNumber(dcApiResponse)
  .addOnSuccessListener { result ->
    val phoneNumber = result.getPhoneNumber()
    // Verification successful
  }
  .addOnFailureListener { e -> /* Handle exchange failure */ }

4. การยืนยันโทเค็น Firebase PNV

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

หากใช้หมายเลขโทรศัพท์ที่ยืนยันแล้วนอกไคลเอ็นต์แอป คุณควรส่งต่อโทเค็นแทนหมายเลขโทรศัพท์เองเพื่อให้ยืนยันความสมบูรณ์ของโทเค็นได้เมื่อใช้งาน หากต้องการยืนยันโทเค็น คุณต้องใช้ปลายทาง 2 รายการดังนี้

  • ปลายทางการสร้าง Nonce
  • ปลายทางการยืนยันโทเค็น

การใช้งานปลายทางเหล่านี้ขึ้นอยู่กับคุณ ตัวอย่างต่อไปนี้แสดงวิธีใช้งานโดยใช้ Node.js และ Express

การสร้าง Nonce

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

app.get('/fpnvNonce', async (req, res) => {
    const nonce = crypto.randomUUID();

    // TODO: Save the nonce to a database, key store, etc.
    // You should also assign the nonce an expiration time and periodically
    // clear expired nonces from your database.
    await persistNonce({
        nonce,
        expiresAt: Date.now() + 180000, // Give it a short duration.
    });

    // Return the nonce to the caller.
    res.send({ nonce });
});

นี่คือปลายทางที่ฟังก์ชันตัวยึดตำแหน่ง fetchNonceFromYourServer() ในขั้นตอนที่ 1 จะเรียกใช้ ค่า Nonce จะแพร่กระจาย ผ่านการเรียกเครือข่ายต่างๆ ที่ไคลเอ็นต์ดำเนินการ และในที่สุดจะกลับมายังเซิร์ฟเวอร์ของคุณในโทเค็น Firebase PNV ในขั้นตอนถัดไป คุณจะยืนยัน ว่าโทเค็นมี Nonce ที่คุณสร้างขึ้น

การยืนยันโทเค็น

ปลายทางนี้จะรับโทเค็น Firebase PNV จากไคลเอ็นต์และยืนยันความถูกต้อง หากต้องการยืนยันโทเค็น คุณต้องตรวจสอบสิ่งต่อไปนี้

  • โทเค็นได้รับการลงนามโดยใช้คีย์ใดคีย์หนึ่งที่เผยแพร่ในปลายทาง Firebase PNVJWKS

    https://fpnv.googleapis.com/v1beta/jwks
    
  • การอ้างสิทธิ์ของกลุ่มเป้าหมายและผู้ออกจะประกอบด้วยหมายเลขโปรเจ็กต์ Firebase ของคุณและอยู่ในรูปแบบต่อไปนี้

    https://fpnv.googleapis.com/projects/FIREBASE_PROJECT_NUMBER
    

    คุณดูหมายเลขโปรเจ็กต์ Firebase ได้ในหน้าการตั้งค่าโปรเจ็กต์ ของคอนโซล Firebase

  • โทเค็นยังไม่หมดอายุ

  • โทเค็นมี Nonce ที่ถูกต้อง Nonce จะถูกต้องในกรณีต่อไปนี้

    • คุณสร้างขึ้น (กล่าวคือ ค้นหาได้ในกลไกการคงอยู่ใดก็ตามที่คุณใช้)
    • ยังไม่มีการใช้งาน
    • ยังไม่หมดอายุ

ตัวอย่างเช่น การติดตั้งใช้งาน Express อาจมีลักษณะดังนี้

import { JwtVerifier } from "aws-jwt-verify";

// Find your Firebase project number in the Firebase console.
const FIREBASE_PROJECT_NUMBER = "123456789";

// The issuer and audience claims of the FPNV token are specific to your
// project.
const issuer = `https://fpnv.googleapis.com/projects/${FIREBASE_PROJECT_NUMBER}`;
const audience = `https://fpnv.googleapis.com/projects/${FIREBASE_PROJECT_NUMBER}`;

// The JWKS URL contains the current public signing keys for FPNV tokens.
const jwksUri = "https://fpnv.googleapis.com/v1beta/jwks";

// Configure a JWT verifier to check the following:
// - The token is signed by Google
// - The issuer and audience claims match your project
// - The token has not yet expired (default begavior)
const fpnvVerifier = JwtVerifier.create({ issuer, audience, jwksUri });

app.post('/verifiedPhoneNumber', async (req, res) => {
    if (!req.body) return res.sendStatus(400);
    // Get the token from the body of the request.
    const fpnvToken = req.body;
    try {
        // Attempt to verify the token using the verifier configured above.
        const verifiedPayload = await fpnvVerifier.verify(fpnvToken);

        // Now that you've verified the signature and claims, verify the nonce.
        // TODO: Try to look up the nonce in your database and remove it if it's
        // found; if it's not found or it's expired, throw an error.
        await testAndRemoveNonce(verifiedPayload.nonce);

        // Only after verifying the JWT signature, claims, and nonce, get the
        // verified phone number from the subject claim.
        // You can use this value however it's needed by your app.
        const verifiedPhoneNumber = verifiedPayload.sub;
        // (Do something with it...)

        return res.sendStatus(200);
    } catch {
        // If verification fails, reject the token.
        return res.sendStatus(400);
    }
});