「開始使用 Firebase Phone Number Verification」頁面詳細說明如何使用 getVerifiedPhoneNumber()
方法整合 Firebase PNV,這個方法會處理整個 Firebase PNV 流程,包括取得使用者同意聲明,以及對 Firebase PNV 後端進行必要的網路呼叫。
建議大多數開發人員使用單一方法 API (getVerifiedPhoneNumber()
)。不過,如果您需要更精細地控制與 Android Credential Manager 的互動 (例如要求其他憑證和電話號碼),Firebase PNV 程式庫也提供下列兩種方法,每種方法都會處理與 Firebase PNV 後端的不同互動:
getDigitalCredentialPayload()
會取得伺服器簽署的要求,您將使用這項要求叫用憑證管理工具。exchangeCredentialResponseForPhoneNumber()
會將憑證管理工具的回應換成包含已驗證電話號碼的簽署權杖。
在呼叫每個方法之間,您必須負責處理與 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
}
如果憑證管理工具呼叫成功,回應會包含數位憑證,您可以使用下列範例程式碼擷取:
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
這個端點負責產生並暫時儲存一次性值 (稱為隨機數),用於防範針對端點的重送攻擊。舉例來說,您可能會定義類似這樣的 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
權杖尚未過期。
權杖包含有效的隨機數。如果符合下列條件,隨機碼即為有效:
- 您已產生該 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);
}
});