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 令牌并验证其真实性。如需验证令牌,您需要检查以下内容:
令牌是使用 Firebase PNV JWKS 端点上发布的某个密钥进行签名的:
https://fpnv.googleapis.com/v1beta/jwks
受众群体声明和签发者声明包含您的 Firebase 项目编号,格式如下:
https://fpnv.googleapis.com/projects/FIREBASE_PROJECT_NUMBER
您可以在 Firebase 控制台的项目设置页面中找到您的 Firebase 项目编号。
令牌未过期。
令牌包含有效的随机数。如果满足以下条件,则随机数有效:
- 您生成了该 ID(也就是说,可以在您使用的任何持久性机制中找到该 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);
}
});