جریان تأیید شماره تلفن Firebase را در اندروید سفارشی کنید

صفحه «شروع به کار با Firebase Phone Number Verification جزئیات نحوه ادغام با Firebase PNV با استفاده از متد getVerifiedPhoneNumber() را شرح می‌دهد، که کل جریان Firebase PNV را از اخذ رضایت کاربر گرفته تا برقراری تماس‌های شبکه‌ای لازم به backend Firebase PNV مدیریت می‌کند.

API تک‌متدی ( getVerifiedPhoneNumber() ) برای اکثر توسعه‌دهندگان توصیه می‌شود. با این حال، اگر به کنترل دقیق‌تری بر تعامل با Android Credential Manager نیاز دارید - برای مثال، برای درخواست سایر اعتبارنامه‌ها به همراه شماره تلفن - کتابخانه Firebase PNV دو روش زیر را نیز ارائه می‌دهد که هر کدام تعامل متفاوتی را با backend Firebase PNV مدیریت می‌کنند:

  • getDigitalCredentialPayload() یک درخواست امضا شده توسط سرور دریافت می‌کند که شما از آن برای فراخوانی Credential Manager استفاده خواهید کرد.
  • exchangeCredentialResponseForPhoneNumber() پاسخ دریافتی از Credential Manager را با یک توکن امضا شده حاوی شماره تلفن تأیید شده، مبادله می‌کند.

بین فراخوانی هر یک از این متدها، شما مسئول مدیریت تعامل با APIهای مدیریت اعتبارنامه اندروید هستید. این صفحه مروری بر نحوه پیاده‌سازی این جریان سه‌گانه ارائه می‌دهد.

قبل از اینکه شروع کنی

پروژه Firebase خود را راه‌اندازی کنید و وابستگی‌های Firebase PNV را همانطور که در صفحه شروع به کار توضیح داده شده است، وارد کنید.

۱. دریافت درخواست اعتبارنامه دیجیتال (Digital Credential request)

متد getDigitalCredentialPayload() را برای ایجاد درخواست شماره تلفن دستگاه فراخوانی کنید. در مرحله بعد، این درخواست، payload تعامل شما با 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 */ }

۲. با استفاده از Credential Manager درخواست اعتبارنامه دیجیتال دهید

در مرحله بعد، درخواست را به مدیر اعتبارنامه ارسال کنید.

برای انجام این کار، باید درخواست payload را در یک درخواست API مربوط به DigitalCredential قرار دهید. این درخواست باید شامل همان 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()

پس از انجام این کار، می‌توانید درخواست را با استفاده از API مدیریت اعتبارنامه (Credential Manager) ارسال کنید:

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
}

اگر فراخوانی Credential Manager با موفقیت انجام شود، پاسخ آن حاوی یک اعتبارنامه دیجیتال خواهد بود که می‌توانید با استفاده از کدی مانند مثال زیر آن را استخراج کنید:

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

۳. پاسخ اعتبارنامه دیجیتال را با یک توکن Firebase PNV مبادله کنید.

در نهایت، متد exchangeCredentialResponseForPhoneNumber() را برای تبادل پاسخ اعتبارنامه دیجیتال با شماره تلفن تأیید شده و یک توکن Firebase PNV فراخوانی کنید:

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

۴. تأیید توکن Firebase PNV

اگر این روند با موفقیت انجام شود، متد getVerifiedPhoneNumber() شماره تلفن تأیید شده و یک توکن امضا شده حاوی آن را برمی‌گرداند. می‌توانید از این داده‌ها در برنامه خود، طبق سیاست حفظ حریم خصوصی خود، استفاده کنید.

اگر از شماره تلفن تأیید شده خارج از کلاینت برنامه استفاده می‌کنید، باید به جای خود شماره تلفن، توکن را ارسال کنید تا بتوانید هنگام استفاده از آن، صحت آن را تأیید کنید. برای تأیید توکن‌ها، باید دو نقطه پایانی را پیاده‌سازی کنید:

  • یک نقطه پایانی تولید نانس
  • یک نقطه پایانی تأیید توکن

پیاده‌سازی این نقاط پایانی به خودتان بستگی دارد؛ مثال‌های زیر نشان می‌دهند که چگونه می‌توانید آن‌ها را با استفاده از Node.js و Express پیاده‌سازی کنید.

تولید نانس‌ها

این نقطه پایانی مسئول تولید و ذخیره موقت مقادیر یکبار مصرف به نام nonce است که برای جلوگیری از حملات بازپخش علیه نقاط پایانی شما استفاده می‌شوند. به عنوان مثال، ممکن است یک مسیر 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 PNV JWKS امضا شده است:

    https://fpnv.googleapis.com/v1beta/jwks
    
  • ادعاهای مخاطب و صادرکننده شامل شماره پروژه Firebase شما هستند و به شکل زیر می‌باشند:

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

    می‌توانید شماره پروژه فایربیس خود را در صفحه تنظیمات پروژه کنسول فایربیس پیدا کنید.

  • توکن منقضی نشده است.

  • توکن حاوی یک نانس معتبر است. یک نانس در صورتی معتبر است که:

    • شما آن را ایجاد کرده‌اید (یعنی، می‌توان آن را در هر مکانیسم ماندگاری که استفاده می‌کنید، یافت)
    • قبلاً استفاده نشده است
    • منقضی نشده است

برای مثال، پیاده‌سازی 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);
    }
});