Personaliza el flujo de verificación de números de teléfono de Firebase en Android

En la página Comienza a usar Firebase Phone Number Verification, se detalla cómo realizar la integración con Firebase PNV usando el método getVerifiedPhoneNumber(), que controla todo el flujo de Firebase PNV, desde la obtención del consentimiento del usuario hasta la realización de las llamadas de red necesarias al backend de Firebase PNV.

Se recomienda la API de un solo método (getVerifiedPhoneNumber()) para la mayoría de los desarrolladores. Sin embargo, si necesitas un control más detallado sobre la interacción con el Administrador de credenciales de Android (por ejemplo, para solicitar otras credenciales junto con el número de teléfono), la biblioteca Firebase PNV también proporciona los siguientes dos métodos, cada uno de los cuales controla una interacción diferente con el backend de Firebase PNV:

  • getDigitalCredentialPayload() obtiene una solicitud firmada por el servidor que usarás para invocar Credential Manager.
  • exchangeCredentialResponseForPhoneNumber() intercambia la respuesta de Credential Manager por un token firmado que contiene el número de teléfono verificado.

Entre la llamada a cada uno de esos métodos, eres responsable de controlar la interacción con las APIs de Credential Manager de Android. En esta página, se proporciona una descripción general de cómo implementar este flujo de tres partes.

Antes de comenzar

Configura tu proyecto de Firebase y, luego, importa las dependencias de Firebase PNV como se describe en la página Comienza a usar.

1. Obtén la carga útil de la solicitud de credencial digital

Llama al método getDigitalCredentialPayload() para generar una solicitud del número de teléfono del dispositivo. En el siguiente paso, esta solicitud será la carga útil de tu interacción con la API de Credential Manager.

// 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. Cómo hacer una solicitud de credencial digital con Credential Manager

A continuación, pasa la solicitud al Administrador de credenciales.

Para ello, debes incluir la carga útil de la solicitud en una solicitud de la API de DigitalCredential. Esta solicitud debe incluir el mismo nonce que le pasaste a 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()

Una vez que lo hayas hecho, puedes realizar la solicitud con la API de 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
}

Si la llamada a Credential Manager se realiza correctamente, su respuesta contendrá una credencial digital, que puedes extraer con código como el del siguiente ejemplo:

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. Intercambia la respuesta de la credencial digital por un token de Firebase PNV

Por último, llama al método exchangeCredentialResponseForPhoneNumber() para intercambiar la respuesta de la credencial digital por el número de teléfono verificado y un token Firebase PNV:

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

4. Cómo verificar el token de Firebase PNV

Si el flujo se completa correctamente, el método getVerifiedPhoneNumber() devuelve el número de teléfono verificado y un token firmado que lo contiene. Puedes usar estos datos en tu app, tal como se documenta en tu política de privacidad.

Si usas el número de teléfono verificado fuera del cliente de la app, debes pasar el token en lugar del número de teléfono en sí para poder verificar su integridad cuando lo uses. Para verificar tokens, debes implementar dos extremos:

  • Un extremo de generación de nonce
  • Un extremo de verificación de tokens

La implementación de estos extremos depende de ti. En los siguientes ejemplos, se muestra cómo podrías implementarlos con Node.js y Express.

Cómo generar nonces

Este extremo es responsable de generar y almacenar temporalmente valores de un solo uso llamados nonces, que se utilizan para evitar ataques de repetición contra tus extremos. Por ejemplo, podrías tener una ruta de Express definida de la siguiente manera:

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

Este es el extremo al que llamaría la función de marcador de posición, fetchNonceFromYourServer(), en el paso 1. El nonce se propagará a través de las diversas llamadas de red que realiza el cliente y, finalmente, regresará a tu servidor en el token Firebase PNV. En el siguiente paso, verificarás que el token contenga un nonce que generaste.

Verifica tokens

Este extremo recibe tokens de Firebase PNV de tu cliente y verifica su autenticidad. Para verificar un token, debes comprobar lo siguiente:

  • El token se firma con una de las claves publicadas en el extremo Firebase PNV de JWKS:

    https://fpnv.googleapis.com/v1beta/jwks
    
  • Los reclamos de público y emisor contienen el número de tu proyecto de Firebase y tienen el siguiente formato:

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

    Puedes encontrar el número de tu proyecto de Firebase en la página Configuración del proyecto de Firebase console.

  • El token no venció.

  • El token contiene un nonce válido. Un nonce es válido si se cumplen las siguientes condiciones:

    • Lo generaste (es decir, se puede encontrar en cualquier mecanismo de persistencia que uses).
    • No se haya usado antes
    • No venció.

Por ejemplo, la implementación de Express podría verse de la siguiente manera:

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