mcpField
V
verified-email

verified-email

github.com/android开发10.0k1.5k 次安装2 天前更新
代码仓库

关于 verified-email

面向 Android 客户端的集成指南,使用 OpenID4VP 通过 Digital Credentials Verifier API 获取已验证邮箱。它介绍凭证请求与响应解析;密码学验证仍需由后端负责。

主要功能

  1. 01构建 OpenID4VP 凭证请求
  2. 02通过 Credential Manager 请求凭证
  3. 03在设备上解析返回的 SD-JWT 声明
  4. 04服务端凭证验证指导
  5. 05账户流程集成指导

适用场景

  1. 01创建账户时验证邮箱
  2. 02账户恢复时确认邮箱归属
  3. 03敏感操作时重新验证身份

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/android/skills --skill verified-email

安装后在 Claude Code 中输入/verified-email确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/android/identity/credential-manager/
index.md
references/android/identity/digital-credentials/
credential-verifier.md
email-verification-implementation.md
email-verification.md
index.md
references/android/identity/passkeys/
create-passkeys.md
references/android/identity/sign-in/
credential-manager-webview.md

详细介绍

基础知识

  • 数字凭证概述:了解可通过密码学验证的文档,以及 Credential Manager 的作用。
  • 术语表:dcql_query、UserInfoCredential 和 GetDigitalCredentialOption 的定义。
标准与示例
  • OpenID4VP 标准:用于创建数字凭证请求的规范。
  • 数字凭证演示:示例请求和跨平台测试工具。
  • W3C 可验证凭证:用于以密码学方式保护声明的数据模型。
  • SD-JWT:响应使用的选择性披露 JSON Web Token 格式。
  • mdoc:移动文档的 ISO/IEC 18013-5 标准。
要求
  • SDK 版本:要求最低 SDK 版本为 28(Android 9)。
  • GMS 版本:Google Play 服务版本为 25.49.x 或更高。
使用场景

电子邮件验证适用于以下使用场景:

  • 创建账户/注册:跳过手动电子邮件验证,减少操作步骤。
  • 账户恢复:在账户恢复流程中安全地验证电子邮件所有权。
  • 重新认证:为高风险操作提供灵活的验证方式,不受初始登录方式限制。
限制与注意事项
  • Workspace 账户:Google 不会为 Google Workspace 账户签发可验证凭证。
  • 时效性:对于非 @gmail.com 地址,Google 会在创建账户时验证电子邮件,但不会提供时效性声明;请实施 OTP 等额外验证。
范围与先决条件

重要:此技能仅关注 Android 客户端集成。它不会实现应用的服务端密码学验证逻辑。出于安全考虑,必须在服务端验证返回的凭证,并在后端实现此逻辑。

探索代码库以查找使用场景

开始时,可以在项目源代码中使用以下查询,查找适用于不同使用场景的相关界面,以实现已验证电子邮件:

  • SignUpScreen
  • "Email address"
  • "Recover Account"
  • "Account Recovery"
  • "Forgot password?"
  • "Delete Account"

确定集成点

要有效实现此功能,必须先在代码库中找到相关流程。首先,可采用以下方法,根据不同使用场景使用已验证电子邮件:

1. 搜索导航路由

如果应用使用 Navigation,请搜索与身份验证相关的路由或目标位置:

查找:

  • 关键词:signup、registration、create_account、forgot_password、recovery、verify_email。
  • 代码模式:搜索包含这些字符串的 NavHost 或 composable 目标位置。
2. 查找身份验证 ViewModel

查找处理用户属性、账户创建和账户恢复的业务逻辑:

  • 关键词:SignUpViewModel、AuthViewModel、RegistrationRepository。
  • 代码模式:查找 onCreateAccount、onRecoverAccount 或 validateEmail 等方法。
3. 查找敏感操作的重新认证实例

对于重新认证场景,请查找用户执行敏感操作的相关位置:

  • 关键词:ChangePassword、UpdatePayment、DeleteAccount、UpdateDetails、EditUserDetails

实现注意事项

  • 构造数字凭证请求,并向用户展示。
  • 确保遵循文档中提到的请求 JSON 结构。
  • 向用户展示请求时,检查结果凭证是否为 DigitalCredential,以及 credential.credentialJson 是否为 responseJsonString。
  • 解析客户端返回的响应。
  • 如果尚未提供通行密钥创建选项,请添加该选项。
  • 假设使用本地的 SdJwtParser 解析原始 SD-JWT 并返回 JSONObject。
  • 使用 VerifiedUserInfo 数据类存储解析后得到的姓名和电子邮件地址。
  • 留下 TODO,供开发者处理应用的服务端验证和解析。
  • API 调用成功后,将用户引导至主屏幕,并显示包含用户详细信息的 snackbar,仅供参考。

概览

本指南介绍如何通过 OpenID for Verifiable Presentations (OpenID4VP) 请求,使用 Digital Credentials Verifier API 实现经过验证的电子邮件地址获取功能。

添加依赖项

在应用的 build.gradle 文件中,添加以下 Credential Manager 依赖项:

Kotlin
dependencies {
    implementation("androidx.credentials:credentials:1.7.0-alpha03")
    implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03")
}
Groovy
dependencies {
    implementation "androidx.credentials:credentials:1.7.0-alpha03"
    implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03"
}

初始化 Credential Manager

使用应用或 Activity 上下文创建 CredentialManager 对象。

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

构造数字凭证请求

若要请求经过验证的电子邮件地址,请构造一个 GetCredentialRequest, 其中包含 GetDigitalCredentialOption。此选项需要一个 格式符合 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(托管域):响应中的值为空。

    说明

    **注意:**如果响应中的 email_verified 为 true 且 hd 为空,则表示该账号是已授权的 Google 账号。Google 不会为 Google Workspace 账号签发可验证凭证。不过,为非 Workspace 账号签发的可验证凭证中会包含 hd 字段。建议实现对此字段的处理,以确保应用适应未来变化。如果电子邮件地址不是 @gmail.com,Google 会在创建 Google 账号时验证该地址,但无法保证验证信息是最新的。因此,对于非 Google 电子邮件地址,应考虑通过 OTP 等额外验证来确认用户身份。要了解凭证的架构以及验证 email_verified 等字段的具体规则,请参阅 Google 身份指南。

  • nonce:为每个请求生成唯一且在密码学上安全的随机值。这对安全至关重要,因为它可以防止重放攻击。

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

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

说明

注意:hd 和 email_verified 字段在 Credential Manager 的内置界面中对用户隐藏。你不能仅使用这些隐藏字段发起请求——此类请求的响应为 GetCredentialCancellationException。

向用户展示请求

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

coroutineScope {
    try {
        // 正在向用户请求数字凭证……
        val result = credentialManager.getCredential(activity, request)

        when (val credential = result.credential) {
            is DigitalCredential -> {
                val responseJsonString = credential.credentialJson

                // 已成功收到数字凭证响应。

                // 接下来,解析此响应并将其发送到你的服务端。
                // ...
            }

            else -> {
                // 处理意外状态 () ——由开发者决定
            }
        }
    } catch (e: Exception) {
        // 处理异常 ——由开发者决定
    }
}

说明

**注意:**数字凭证没有与 Google 登录中的 preferImmediatelyAvailableCredentials 等效的功能。如果找不到可验证凭证(例如,设备上没有符合条件的账号),系统会向用户显示“无可用选项”或类似界面。

在客户端解析响应

警告

警告: 从 2026 年 8 月起,响应 JSON 格式已更新,严格遵循 W3C 标准。它包含 data 和 protocol 键,OpenID4VP vp_token 嵌套在 data 中,而旧版格式则直接包含 vp_token。在过渡期间,请确保客户端解析和服务端验证均能处理这两种格式,同时逐步淘汰旧版实现。2026 年 8 月之后开始集成电子邮件验证流程的应用必须仅使用新格式。

收到响应后,你可以先在客户端进行初步解析。 这样可以立即更新 UI,例如显示用户姓名。

重要

重要: 此步骤不用于验证。必须在服务端执行完整的加密验证。

以下代码提取原始的 选择性披露 JWT (SD-JWT),并使用辅助工具解码其声明。

// 1. 解析外层 JSON 包装,以获取 `vp_token`
val responseData = JSONObject(responseJsonString)
val dataObject = responseData.getJSONObject("data")
val vpToken = dataObject.getJSONObject("vp_token")

// 2. 提取原始 SD-JWT 字符串
val credentialId = vpToken.keys().next()
val rawSdJwt = vpToken.getJSONArray(credentialId).getString(0)

// 3. 使用解析器获取已验证的声明
// 强烈建议在服务端进行验证/解析。

// 假设本地解析器类似于示例 SdJwtParser.kt 中的解析器
val claims = SdJwtParser.parse(rawSdJwt)
Log.d("TAG", "Parsed Claims: ${claims.toString(2)}")

// 4. 使用真实数据创建 VerifiedUserInfo 对象
val userInfo = VerifiedUserInfo(
    email = claims.getString("email"),
    displayName = claims.optString("name", claims.getString("email"))
)

处理响应

Credential Manager API 将返回 DigitalCredential 响应。

以下是原始 responseJsonString 的示例,以及解析内部 SD-JWT 后的声明示例;解析后除了已验证的电子邮件,还会获取其他元数据。

/*
// 来自 credential.credentialJson 的原始 JSON 响应示例:
{
  "protocol": "openid4vp-v1-unsigned",
  "data": {
    "vp_token": {
      // 此键与在 dcql_query 中设置的 'id' 相匹配
      "user_info_query": [
        // SD-JWT 字符串(签发方 JWT ~ 披露内容 ~ 密钥绑定 JWT)
        "eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..."
      ]
    }
  }
}

// 服务端对 SD-JWT 进行解析和验证后得到的声明示例:
{
  "cnf": {
    "jwk": {..}
  },
  "exp": 1775688222,
  "iat": 1775083422,
  "iss": "https://verifiablecredentials-pa.googleapis.com",
  "vct": "UserInfoCredential",
  "email": "[email protected]",
  "email_verified": true,
  "given_name": "Jane",
  "family_name": "Doe",
  "name": "Jane Doe",
  "picture": "http://example.com/janedoe/me.jpg",
  "hd": ""
}
 */

重要

重要: 我们强烈建议你在收到已验证的电子邮件后,触发 Credential Manager 的通行密钥创建。

账户创建的服务器端验证

由于获取的电子邮件地址已通过加密验证,因此可以省略电子邮件 OTP 验证步骤,大幅减少注册阻力,并有可能提高转化率。此流程最好由服务器处理。客户端会将原始响应(包含 vp_token)和原始 nonce 发送到新的服务器端点。

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

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

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

服务器端验证必须满足以下要求:

说明

**注意:**使用标准库(例如适用于 Node.js 的 @sd-jwt/sd-jwt-vc)执行 OpenID for Verifiable Presentations specification 中所述的验证步骤。

为确保完整的安全性,请务必同时验证 nonce,以防止重放攻击。

通过结合这些步骤,服务器可以验证数据的真实性和呈示者的身份,确保凭证在配置新账户之前没有被截取或伪造。

警告

**警告:**如在客户端解析响应中所述,从 2026 年 8 月起,响应 JSON 格式已更新为符合 W3C 标准。在过渡期间,请确保客户端解析和服务器端验证能够处理这两种格式,同时逐步淘汰旧实现。2026 年 8 月之后开始集成电子邮件验证流程的应用必须仅使用新格式。

try {
    // 将原始凭证响应和原始 nonce 发送到服务器。
    // 服务器必须验证响应。createAccountWithVerifiedCredentials
    // 是每个 RP 自行实现的服务器端验证和账户创建逻辑。
    val serverResponse = createAccountWithVerifiedCredentials(responseJsonString, nonce)

    // 服务器返回新账户信息(例如电子邮件地址、姓名)
    val claims = JSONObject(serverResponse.json)

    val userInfo = VerifiedUserInfo(
        email = claims.getString("email"),
        displayName = claims.optString("name", claims.getString("email"))
    )

    // 处理响应——由开发者决定
} catch (e: Exception) {
    // 处理异常——由开发者决定
}

创建 Passkey

配置账户后的下一步(可选但强烈建议)是立即为该账户创建 Passkey。这为用户提供了一种安全、无需密码的登录方式。此流程与标准 Passkey 注册完全相同。

WebView 支持

要使此流程在 WebView 中正常运行,开发者应实现 JavaScript 桥接(JS Bridge),以便完成交接。此桥接 允许 WebView 对象向原生应用发出信号,随后由原生应用 实际调用 Credential Manager API。

另请参阅

关键安全指南

为确保电子邮件验证流程的完整性,必须满足以下安全要求:

  • 服务器端验证:对于创建账户等安全敏感操作,切勿信任在客户端解析的声明。将完整的原始 responseJsonString 和原始 nonce 发送到应用的服务器进行全面验证。
  • Nonce 完整性:为每个请求生成唯一且经密码学安全处理的 nonce,并且切勿在多个请求间重复使用 nonce,以防止重放攻击。
  • 密码学检查:应用的服务器必须验证发行者(iss)字段、SD-JWT 签名,以及使用 cnf 字段验证呈现者身份。

评论

支持 Markdown