Firebase Phone Number Verification 使用入门页面详细介绍了如何使用 getVerifiedPhoneNumber() 方法与 Firebase PNV 集成,该方法可处理整个 Firebase PNV 流程,从征得用户同意到向 Firebase PNV 后端发出必要的网络调用。
建议大多数开发者使用单方法 API (getVerifiedPhoneNumber())。不过,如果您需要更精细地控制与 Android Credential Manager 的互动(例如,请求其他凭据以及电话号码),Firebase PNV 库还提供了以下两种方法,每种方法都处理与 Firebase PNV 后端的不同互动:
getDigitalCredentialPayload()会获得一个服务器签名的请求,您将使用该请求来调用 Credential Manager。exchangeCredentialResponseForPhoneNumber()会将来自 Credential Manager 的响应换成包含已验证电话号码的签名令牌。系统会在这一步进行结算。
在调用上述每种方法之间,您需要负责处理与 Android 的凭据管理器 API 的互动。本页简要介绍了如何实现这三个部分的流程。
准备工作
按照使用入门页面上的说明设置 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. 使用 Credential Manager 发出数字凭据请求
接下来,将请求传递给 Credential Manager。
为此,您需要将请求载荷封装在 DigitalCredential API 请求中。此请求必须包含您传递给 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 } 如果 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}") } } } 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() 方法会返回经过验证的电话号码以及包含该电话号码的已签名令牌。您可以在应用中使用这些数据,但必须遵守您的隐私权政策。
如果您在应用客户端之外使用经过验证的电话号码,则应传递令牌,而不是电话号码本身,以便在使用时验证其完整性。如需验证令牌,您需要实现两个端点:
- nonce 生成端点
- 令牌验证端点
这些端点的实现由您自行决定;以下示例展示了如何使用 Node.js 和 Express 来实现这些端点。
生成 nonce
此端点负责生成并暂时存储一次性值(称为 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 }); }); 这是第 1 步中的占位函数 fetchNonceFromYourServer() 将调用的端点。随机数将通过客户端执行的各种网络调用进行传播,最终在 Firebase PNV 令牌中返回到您的服务器。在下一步中,您将验证令牌是否包含您生成的随机数。
验证令牌
此端点从客户端接收 Firebase PNV 令牌并验证其真实性。如需验证令牌,您需要检查以下内容:
typ标头设置为JWT。令牌使用 Firebase PNV JWKS 端点上发布的密钥之一进行签名,所用算法为
ES256:https://fpnv.googleapis.com/v1beta/jwks签发者声明包含您的 Firebase 项目编号,格式如下:
https://fpnv.googleapis.com/projects/FIREBASE_PROJECT_NUMBER您可以在 Firebase 控制台的项目设置页面中找到您的 Firebase 项目编号。
受众群体声明是一个列表,其中包含您的 Firebase 项目编号和项目 ID,格式如下:
[ https://fpnv.googleapis.com/projects/FIREBASE_PROJECT_NUMBER, https://fpnv.googleapis.com/projects/FIREBASE_PROJECT_ID, ]令牌未过期。
令牌包含有效的随机数。如果满足以下条件,则随机数有效:
- 您生成了该令牌(也就是说,可以在您使用的任何持久性机制中找到该令牌)
- 尚未被使用
- 未过期
例如,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); } });