使用 Digital Credentials API 實作電子郵件驗證

本指南說明如何透過 OpenID for Verifiable Presentations (OpenID4VP) 要求,使用 Digital Credentials Verifier API 實作已驗證電子郵件擷取作業。

新增依附元件

在應用程式的 build.gradle 檔案中,新增 Credential Manager 的下列依附元件:

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"
}

初始化 Credential Manager

使用應用程式或活動情境建立 CredentialManager 物件。

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
private val credentialManager = CredentialManager.create(context)

建構數位憑證要求

如要要求經過驗證的電子郵件,請建構包含 GetDigitalCredentialOptionGetCredentialRequest。如要使用這個選項,必須提供格式為 OpenID for Verifiable Presentations (OpenID4VP) 要求的 requestJson 字串。

OpenID4VP 要求 JSON 必須遵循特定結構。目前的供應商支援 JSON 結構,並以外部 "digital": {"requests": [...]} 包裝函式封裝。

        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 電子郵件,您應考慮採用額外的驗證方式 (例如一次性密碼),驗證使用者身分。如要瞭解憑證的結構定義,以及驗證 email_verified 等欄位的特定規則,請參閱 Google 身分識別指南

  • Nonce:系統會為每個要求產生一個獨一無二的密碼編譯安全隨機值。這對安全性至關重要,因為可防範重送攻擊。

  • UserInfoCredential:這個值代表包含使用者屬性的特定類型數位憑證。在要求中加入這項資訊,對於區分電子郵件驗證用途至關重要。

接著,將 openId4vpRequest JSON 包裝在 GetDigitalCredentialOption 中、建立 GetCredentialRequest,然後呼叫 getCredential()

向使用者顯示要求

使用 Credential Manager 內建 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) 和原始隨機值傳送至新的伺服器端點。

如要進行驗證,應用程式必須先將完整的 responseJsonString 傳送至伺服器進行加密驗證,才能建立帳戶或讓使用者登入。

數位憑證可為伺服器提供兩個重要層級的驗證:

  • 資料真實性:驗證核發者 (iss) 網址和SD-JWT簽章,證明這項資料是由可信任的機構核發。
  • 出示者身分:驗證 cnf 欄位和金鑰繫結 (kb) 簽章,確認憑證是由原始核發裝置共用,防止憑證遭到攔截或在其他裝置上使用。

伺服器上的驗證必須符合下列條件:

  • 驗證簽發者:確認「簽發者」iss欄位與 https://verifiablecredentials-pa.googleapis.com相符。
  • 驗證簽章:使用公開金鑰 (JWK) 檢查 SD-JWT 的簽章,公開金鑰位於 https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks。

為確保安全無虞,請務必驗證 nonce,防止重播攻擊。

結合這些步驟,伺服器就能驗證資料的真實性和簡報者的身分,確保憑證在佈建新帳戶前未遭攔截或偽造。

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 Bridge),以利交接。這個橋接器可讓 WebView 向原生應用程式發出信號,然後執行對 Credential Manager API 的實際呼叫。

另請參閱