Firebase Phone Number Verification 시작하기 페이지에서는 사용자 동의를 얻는 것부터 Firebase PNV 백엔드에 필요한 네트워크 호출을 하는 것까지 전체 Firebase PNV 흐름을 처리하는 getVerifiedPhoneNumber()
메서드를 사용하여 Firebase PNV와 통합하는 방법을 자세히 설명합니다.
단일 메서드 API (getVerifiedPhoneNumber()
)는 대부분의 개발자에게 권장됩니다. 하지만 Android 인증 관리자와의 상호작용을 더 세부적으로 제어해야 하는 경우(예: 전화번호와 함께 다른 사용자 인증 정보를 요청하는 경우) Firebase PNV 라이브러리는 다음 두 가지 메서드도 제공하며, 각 메서드는 Firebase PNV 백엔드와의 서로 다른 상호작용을 처리합니다.
getDigitalCredentialPayload()
는 인증 관리자를 호출하는 데 사용할 서버 서명 요청을 가져옵니다.exchangeCredentialResponseForPhoneNumber()
는 사용자 인증 정보 관리자의 응답을 확인된 전화번호가 포함된 서명된 토큰으로 교환합니다.
이러한 각 메서드를 호출하는 사이에 Android의 Credential Manager API와의 상호작용을 처리해야 합니다. 이 페이지에서는 이 3단계 흐름을 구현하는 방법을 간략하게 설명합니다.
시작하기 전에
시작하기 페이지에 설명된 대로 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. 인증 관리자를 사용하여 디지털 인증 요청하기
그런 다음 요청을 인증 관리자에 전달합니다.
이렇게 하려면 요청 페이로드를 DigitalCredential API 요청으로 래핑해야 합니다. 이 요청에는 getDigitalCredentialPayload()
에 전달한 것과 동일한 nonce가 포함되어야 합니다.
// 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 토큰에서 서버로 다시 전달됩니다. 다음 단계에서는 토큰에 생성한 nonce가 포함되어 있는지 확인합니다.
토큰 확인
이 엔드포인트는 클라이언트로부터 Firebase PNV 토큰을 수신하고 진위 여부를 확인합니다. 토큰을 확인하려면 다음을 확인해야 합니다.
토큰은 Firebase PNV JWKS 엔드포인트에 게시된 키 중 하나를 사용하여 서명됩니다.
https://fpnv.googleapis.com/v1beta/jwks
잠재고객 및 발급자 클레임에는 Firebase 프로젝트 번호가 포함되며 형식은 다음과 같습니다.
https://fpnv.googleapis.com/projects/FIREBASE_PROJECT_NUMBER
Firebase 프로젝트 번호는 Firebase Console의 프로젝트 설정 페이지에서 확인할 수 있습니다.
토큰이 만료되지 않았습니다.
토큰에 유효한 nonce가 포함되어 있습니다. 다음과 같은 경우 nonce가 유효합니다.
- 생성한 경우 (즉, 사용 중인 지속성 메커니즘에서 찾을 수 있음)
- 아직 사용되지 않음
- 만료되지 않음
예를 들어 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);
}
});