Tuỳ chỉnh quy trình Xác minh số điện thoại của Firebase trên Android

Trang Bắt đầu với Firebase Phone Number Verification trình bày chi tiết cách tích hợp với Firebase PNV bằng phương thức getVerifiedPhoneNumber(). Phương thức này xử lý toàn bộ quy trình Firebase PNV, từ việc lấy sự đồng ý của người dùng đến việc thực hiện các lệnh gọi mạng cần thiết đến phần phụ trợ Firebase PNV.

API một phương thức (getVerifiedPhoneNumber()) được đề xuất cho hầu hết các nhà phát triển. Tuy nhiên, nếu bạn cần kiểm soát chi tiết hơn đối với hoạt động tương tác với Trình quản lý thông tin xác thực của Android (ví dụ: yêu cầu thông tin xác thực khác cùng với số điện thoại), thì thư viện Firebase PNV cũng cung cấp 2 phương thức sau, mỗi phương thức xử lý một hoạt động tương tác khác với phần phụ trợ Firebase PNV:

  • getDigitalCredentialPayload() nhận được một yêu cầu do máy chủ ký mà bạn sẽ dùng để gọi Credential Manager.
  • exchangeCredentialResponseForPhoneNumber() trao đổi phản hồi từ Trình quản lý thông tin đăng nhập để lấy một mã thông báo đã ký chứa số điện thoại đã xác minh.

Giữa các lần gọi từng phương thức đó, bạn chịu trách nhiệm xử lý hoạt động tương tác với Credential Manager API của Android. Trang này cung cấp thông tin tổng quan về cách bạn triển khai quy trình gồm 3 phần này.

Trước khi bắt đầu

Thiết lập dự án Firebase và nhập các phần phụ thuộc Firebase PNV như mô tả trên trang Bắt đầu.

1. Nhận tải trọng yêu cầu Chứng chỉ kỹ thuật số

Gọi phương thức getDigitalCredentialPayload() để tạo yêu cầu về số điện thoại của thiết bị. Trong bước tiếp theo, yêu cầu này sẽ là tải trọng của hoạt động tương tác của bạn với API Trình quản lý thông tin xác thực.

// 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. Đưa ra yêu cầu về thông tin xác thực kỹ thuật số bằng Trình quản lý thông tin xác thực

Tiếp theo, hãy truyền yêu cầu đến Trình quản lý thông tin xác thực.

Để làm như vậy, bạn cần bao bọc tải trọng yêu cầu trong một yêu cầu DigitalCredential API. Yêu cầu này phải bao gồm cùng một số chỉ dùng một lần mà bạn đã truyền đến 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()

Sau khi thực hiện xong, bạn có thể đưa ra yêu cầu bằng API Trình quản lý thông tin xác thực:

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
}

Nếu lệnh gọi Credential Manager thành công, phản hồi của lệnh gọi này sẽ chứa một thông tin đăng nhập kỹ thuật số mà bạn có thể trích xuất bằng mã như ví dụ sau:

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. Trao đổi phản hồi thông tin đăng nhập kỹ thuật số để lấy mã thông báo Firebase PNV

Cuối cùng, hãy gọi phương thức exchangeCredentialResponseForPhoneNumber() để trao đổi phản hồi về thông tin đăng nhập kỹ thuật số cho số điện thoại đã xác minh và mã thông báo Firebase PNV:

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

4. Xác minh mã thông báo Firebase PNV

Nếu quy trình thành công, phương thức getVerifiedPhoneNumber() sẽ trả về số điện thoại đã xác minh và một mã thông báo đã ký chứa số điện thoại đó. Bạn có thể sử dụng dữ liệu này trong ứng dụng của mình theo quy định trong chính sách quyền riêng tư.

Nếu sử dụng số điện thoại đã xác minh bên ngoài ứng dụng khách, bạn nên truyền mã thông báo thay vì chính số điện thoại để có thể xác minh tính toàn vẹn của số điện thoại khi sử dụng. Để xác minh mã thông báo, bạn cần triển khai 2 điểm cuối:

  • Một điểm cuối tạo số chỉ dùng một lần
  • Một điểm cuối xác minh mã thông báo

Bạn có thể triển khai các điểm cuối này; các ví dụ sau đây cho thấy cách bạn có thể triển khai chúng bằng Node.js và Express.

Tạo số chỉ dùng một lần

Điểm cuối này chịu trách nhiệm tạo và tạm thời lưu trữ các giá trị dùng một lần (gọi là số chỉ dùng một lần). Các giá trị này được dùng để ngăn chặn các cuộc tấn công phát lại nhắm vào điểm cuối của bạn. Ví dụ: bạn có thể có một tuyến đường Express được xác định như sau:

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 });
});

Đây là điểm cuối mà hàm giữ chỗ fetchNonceFromYourServer() trong Bước 1 sẽ gọi. Số chỉ dùng một lần sẽ truyền qua nhiều lệnh gọi mạng mà máy khách thực hiện và cuối cùng sẽ quay lại máy chủ của bạn trong mã thông báo Firebase PNV. Trong bước tiếp theo, bạn sẽ xác minh rằng mã thông báo chứa một số chỉ dùng một lần mà bạn đã tạo.

Xác minh mã thông báo

Điểm cuối này nhận mã thông báo Firebase PNV từ ứng dụng của bạn và xác minh tính xác thực của mã thông báo đó. Để xác minh mã thông báo, bạn cần kiểm tra:

  • Mã thông báo được ký bằng một trong các khoá được xuất bản tại điểm cuối JWKS Firebase PNV:

    https://fpnv.googleapis.com/v1beta/jwks
    
  • Các giá trị nhận dạng đối tượng và tổ chức phát hành chứa số dự án Firebase của bạn và có định dạng như sau:

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

    Bạn có thể tìm thấy số dự án Firebase trên trang Cài đặt dự án của bảng điều khiển Firebase.

  • Mã thông báo chưa hết hạn.

  • Mã thông báo chứa một số chỉ dùng một lần hợp lệ. Số chỉ dùng một lần hợp lệ nếu:

    • Bạn đã tạo mã này (tức là mã này có thể được tìm thấy trong bất kỳ cơ chế duy trì nào mà bạn đang sử dụng)
    • Chưa được sử dụng
    • Chưa hết hạn

Ví dụ: cách triển khai Express có thể có dạng như sau:

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);
    }
});