이 가이드에서는 OpenID for Verifiable Presentations (OpenID4VP) 요청을 통해 Digital Credentials Verifier API를 사용하여 인증된 이메일 검색을 구현하는 방법을 설명합니다.
종속 항목 추가
앱의 build.gradle 파일에서 인증 관리자의 다음 종속 항목을 추가합니다.
Kotlin
dependencies { implementation("androidx.credentials:credentials:1.7.0-alpha01") implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha01") }
Groovy
dependencies { implementation "androidx.credentials:credentials:1.7.0-alpha01" implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha01" }
인증 관리자 초기화
앱 또는 활동 컨텍스트를 사용하여 CredentialManager 객체를 만듭니다.
// Use your app or activity context to instantiate a client instance of
// CredentialManager.
private val credentialManager = CredentialManager.create(context)
디지털 사용자 인증 정보 요청 구성
인증된 이메일을 요청하려면 GetDigitalCredentialOption이 포함된 GetCredentialRequest을 구성합니다. 이 옵션에는 OpenID for Verifiable Presentations (OpenID4VP) 요청으로 형식이 지정된 requestJson 문자열이 필요합니다.
OpenID4VP 요청 JSON은 특정 구조를 따라야 합니다. 현재 제공업체는 외부 "digital": {"requests":
[...]} 래퍼가 있는 JSON 구조를 지원합니다.
val nonce = generateSecureRandomNonce()
// This request follows the OpenID4VP spec
val openId4vpRequest = """
{
"requests": [
{
"protocol": "openid4vp-v1-unsigned",
"data": {
"response_type": "vp_token",
"response_mode": "dc_api",
"nonce": "$nonce",
"dcql_query": {
"credentials": [
{
"id": "user_info_query",
"format": "dc+sd-jwt",
"meta": {
"vct_values": ["UserInfoCredential"]
},
"claims": [
{"path": ["email"]},
{"path": ["name"]},
{"path": ["given_name"]},
{"path": ["family_name"]},
{"path": ["picture"]},
{"path": ["hd"]},
{"path": ["email_verified"]}
]
}
]
}
}
}
]
}
"""
val getDigitalCredentialOption = GetDigitalCredentialOption(requestJson = openId4vpRequest)
val request = GetCredentialRequest(listOf(getDigitalCredentialOption))
요청에는 다음 주요 정보가 포함됩니다.
DCQL 쿼리:
dcql_query는 사용자 인증 정보 유형과 요청된 클레임 (email_verified)을 지정합니다. 인증 수준을 확인하기 위해 다른 클레임을 요청할 수 있습니다. 가능한 몇 가지 주장은 다음과 같습니다.email_verified: 응답에서 이메일이 인증되었는지 여부를 나타내는 불리언입니다.hd(호스팅 도메인): 응답에서 이 필드는 비어 있습니다.
이메일이 @gmail.com이 아닌 경우 Google 계정이 생성될 때 Google에서 이 이메일을 인증했지만 최신성 주장이 없습니다. 따라서 Google 이메일이 아닌 경우 OTP와 같은 추가 챌린지를 고려하여 사용자를 인증해야 합니다. 인증 정보의 스키마와
email_verified와 같은 필드 유효성 검사를 위한 특정 규칙을 이해하려면 Google ID 가이드를 참고하세요.nonce: 각 요청에 대해 고유한 암호화 방식으로 안전한 랜덤 값이 생성됩니다. 이는 재전송 공격을 방지하므로 보안에 매우 중요합니다.
UserInfoCredential: 이 값은 사용자 속성이 포함된 특정 유형의 디지털 사용자 인증 정보를 의미합니다. 요청에 이를 포함하는 것은 이메일 인증 사용 사례를 구분하는 데 중요합니다.
그런 다음 openId4vpRequest JSON을 GetDigitalCredentialOption로 래핑하고 GetCredentialRequest를 만들어 getCredential()를 호출합니다.
사용자에게 요청을 표시합니다.
인증 관리자 기본 제공 UI를 사용하여 사용자에게 요청을 표시합니다.
try {
// Requesting Digital Credential from user...
val result = credentialManager.getCredential(activity, request)
when (val credential = result.credential) {
is DigitalCredential -> {
val responseJsonString = credential.credentialJson
// Successfully received digital credential response.
// Next, parse this response and send it to your server.
// ...
}
else -> {
// handle Unexpected State() - Up to the developer
}
}
} catch (e: Exception) {
// handle exceptions - Up to the developer
}
클라이언트에서 응답 파싱
응답을 받은 후 클라이언트에서 예비 파싱을 실행할 수 있습니다. 이는 사용자 이름을 표시하는 등 UI를 즉시 업데이트하는 데 유용합니다.
다음 코드는 원시 선택적 공개 JWT(SD-JWT)를 추출하고 도우미를 사용하여 클레임을 디코딩합니다.
// 1. Parse the outer JSON wrapper to get the `vp_token`
val responseData = JSONObject(responseJsonString)
val vpToken = responseData.getJSONObject("vp_token")
// 2. Extract the raw SD-JWT string
val credentialId = vpToken.keys().next()
val rawSdJwt = vpToken.getJSONArray(credentialId).getString(0)
// 3. Use your parser to get the verified claims
// Server-side validation/parsing is highly recommended.
// Assumes a local parser like the one in our SdJwtParser.kt sample
val claims = SdJwtParser.parse(rawSdJwt)
Log.d("TAG", "Parsed Claims: ${claims.toString(2)}")
// 4. Create your VerifiedUserInfo object with REAL data
val userInfo = VerifiedUserInfo(
email = claims.getString("email"),
displayName = claims.optString("name", claims.getString("email"))
)
응답 처리
Credential Manager API는 DigitalCredential 응답을 반환합니다.
다음은 원시 responseJsonString의 모습과 검증된 이메일과 함께 추가 메타데이터를 가져오는 내부 SD-JWT를 파싱한 후의 클레임의 모습의 예입니다.
/*
// Example of the raw JSON response from credential.credentialJson:
{
"vp_token": {
// This key matches the 'id' you set in your dcql_query
"user_info_query": [
// The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT)
"eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..."
]
}
}
// Example of the parsed and verified claims from the SD-JWT on your server:
{
"cnf": {
"jwk": {..}
},
"exp": 1775688222,
"iat": 1775083422,
"iss": "https://verifiablecredentials-pa.googleapis.com",
"vct": "UserInfoCredential",
"email": "jane.doe.246745@gmail.com",
"email_verified": true,
"given_name": "Jane",
"family_name": "Doe",
"name": "Jane Doe",
"picture": "http://example.com/janedoe/me.jpg",
"hd": ""
}
*/
계정 생성 시 서버 측 유효성 검사
검색된 이메일은 암호화 방식으로 확인되므로 이메일 OTP 확인 단계를 생략하여 가입 마찰을 크게 줄이고 전환을 늘릴 수 있습니다. 이 프로세스는 서버에서 처리하는 것이 좋습니다. 클라이언트는 vp_token가 포함된 원시 응답과 원래 nonce를 새 서버 엔드포인트로 전송합니다.
인증을 위해 애플리케이션은 계정을 만들거나 사용자를 로그인하기 전에 암호화 유효성 검사를 위해 서버에 전체 responseJsonString를 전송해야 합니다.
디지털 사용자 인증 정보는 서버에 대해 두 가지 중요한 수준의 확인을 제공합니다.
- 데이터의 진위성: 발급자 (
iss) URL과SD-JWT서명을 확인하면 신뢰할 수 있는 기관에서 이 데이터를 발급했음을 증명할 수 있습니다. - 발표자 ID:
cnf필드와 키 바인딩(kb) 서명을 확인하면 사용자 인증 정보가 원래 발급된 것과 동일한 기기에서 공유되고 있음을 확인할 수 있으므로 다른 기기에서 가로채거나 사용되지 않습니다.
서버의 유효성 검사는 다음을 충족해야 합니다.
- 발급자 확인:
iss(발급자) 필드가https://verifiablecredentials-pa.googleapis.com와 일치하는지 확인합니다. - 서명 확인: https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks에서 제공되는 공개 키 (JWK)를 사용하여 SD-JWT의 서명을 확인합니다.
완전한 보안을 위해 재생 공격을 방지하도록 nonce도 검증해야 합니다.
이러한 단계를 결합하면 서버에서 데이터의 진위성과 프레젠터의 ID를 모두 검증하여 새 계정을 프로비저닝하기 전에 사용자 인증 정보가 가로채거나 스푸핑되지 않았음을 확인할 수 있습니다.
try {
// Send the raw credential response and the original nonce to your server.
// Your server must validate the response. createAccountWithVerifiedCredentials
// is a custom implementation per each RP for server side verification and account creation.
val serverResponse = createAccountWithVerifiedCredentials(responseJsonString, nonce)
// Server returns the new account info (e.g., email, name)
val claims = JSONObject(serverResponse.json)
val userInfo = VerifiedUserInfo(
email = claims.getString("email"),
displayName = claims.optString("name", claims.getString("email"))
)
// handle response - Up to the developer
} catch (e: Exception) {
// handle exceptions - Up to the developer
}
패스키 생성
계정을 프로비저닝한 후 선택사항이지만 적극 권장되는 다음 단계는 해당 계정의 패스키를 즉시 만드는 것입니다. 이를 통해 사용자가 비밀번호 없이 안전하게 로그인할 수 있습니다. 이 흐름은 표준 패스키 등록과 동일합니다.
WebView 지원
WebView에서 흐름이 작동하려면 개발자가 핸드오버를 용이하게 하는 JavaScript 브리지 (JS 브리지)를 구현해야 합니다. 이 브리지를 사용하면 WebView가 네이티브 앱에 신호를 보낼 수 있으며, 네이티브 앱은 Credential Manager API에 대한 실제 호출을 실행할 수 있습니다.