使用 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

使用应用或 activity 上下文创建 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 必须遵循特定结构。当前提供方支持使用外部 "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 电子邮件地址,您应考虑使用其他质询(例如动态密码)来验证用户。如需了解凭证的架构以及验证 email_verified 等字段的具体规则,请参阅 Google Identity 指南

  • nonce:系统会为 每个请求生成一个唯一的加密安全随机值。这对于安全性至关重要,因为它可以防止重放攻击。

  • UserInfoCredential:此值表示包含用户属性的特定类型的数字凭证。在请求中包含此值对于区分电子邮件地址验证用例至关重要。

接下来,将 openId4vpRequest JSON 封装在 GetDigitalCredentialOption 中,创建 GetCredentialRequest,然后调用 getCredential()

向用户显示请求

使用 Credential Manager 内置界面向用户显示请求。

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
}

在客户端解析响应

收到响应后,您可以在客户端执行初步解析。 这对于立即更新界面非常有用,例如显示用户的姓名。

以下代码会提取原始 选择性披露 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": ""
}
 */

账号创建的服务器端验证

由于检索到的电子邮件地址经过加密验证,因此您可以省略电子邮件地址动态密码验证步骤,从而显著减少注册摩擦并可能提高转化率。此过程最好在服务器上处理。客户端会将原始响应(包含 vp_token)和原始 nonce 发送到新的服务器端点。

为了进行验证,您的应用必须先将完整的 responseJsonString 发送到您的服务器进行加密验证,然后才能创建账号或让用户登录。

数字凭证为您的服务器提供两个关键级别的验证:

  • 数据的真实性:验证颁发者 (iss) 网址和 SD-JWT 签名可证明此数据是由可信的权威机构颁发的。
  • 演示者的身份:验证 cnf 字段和密钥绑定 (kb) 签名可确认凭证是由最初颁发给它的同一 设备共享的,从而防止凭证被拦截或 在其他设备上使用。

服务器上的验证必须实现以下目标:

  • 验证颁发者:确保 iss(颁发者)字段与 https://verifiablecredentials-pa.googleapis.com 匹配。
  • 验证签名:使用 https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks 提供的公钥 (JWK) 检查 SD-JWT 的签名。

为了确保完全安全,请务必验证 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 执行实际调用。

另请参阅