关于 verified-email
面向 Android 客户端的集成指南,使用 OpenID4VP 通过 Digital Credentials Verifier API 获取已验证邮箱。它介绍凭证请求与响应解析;密码学验证仍需由后端负责。
主要功能
- 01构建 OpenID4VP 凭证请求
- 02通过 Credential Manager 请求凭证
- 03在设备上解析返回的 SD-JWT 声明
- 04服务端凭证验证指导
- 05账户流程集成指导
适用场景
- 01创建账户时验证邮箱
- 02账户恢复时确认邮箱归属
- 03敏感操作时重新验证身份
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ android/ skills --skill verified-email安装后在 Claude Code 中输入/verified-email确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/android/identity/credential-manager/index.mdreferences/android/identity/digital-credentials/credential-verifier.mdemail-verification-implementation.mdemail-verification.mdindex.mdreferences/android/identity/passkeys/create-passkeys.mdreferences/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键,OpenID4VPvp_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)签名,可确认凭证由最初获取它的同一设备共享,防止凭证被截取或在其他设备上使用。
服务器端验证必须满足以下要求:
- 验证签发者:确保
iss(签发者)字段与https://verifiablecredentials-pa.googleapis.com匹配。 - 验证签名:使用 https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks 上提供的公钥(JWK)检查 SD-JWT 的签名。
说明
**注意:**使用标准库(例如适用于 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字段验证呈现者身份。