如需在您的网站上实现电子邮件验证,请更新表单标记以请求令牌,并为传入的令牌添加服务器端验证。
注册参加源试用
验证网站必须在其网站上配置源试用。
自 Chrome 154 起,第三方源试用已受支持,但需要注意一个重要事项:试用的注册源必须与签发者同站。例如:
- 发卡机构网域:
issuer.example - OT 注册人:
https://issuer.example - JavaScript 来源:
https://issuer.example(或https://app.issuer.example,用于匹配子网域)
配置表单字段
向电子邮件提交表单添加隐藏的令牌字段:
<input
type="email"
name="email-address"
autocomplete="email">
<input
type="hidden"
name="token"
autocomplete="email-verification-token"
nonce="rAnD0m-VaLuE">
字段要求:
- 电子邮件地址字段:设置
type="email"和autocomplete="email",以便 Chrome 可以自动填充并识别地址。 - 令牌字段属性:
- 设置为
autocomplete="email-verification-token":Chrome 会识别此字段,以便在提交时填充令牌。 - 设置
nonce="<VALUE>":网站必须提供唯一的会话绑定随机数,以验证表单提交。
- 设置为
验证邮箱验证令牌 (EVT)
当用户提交表单时,您的服务器会收到电子邮件地址和来自隐藏字段的令牌。如果令牌字段为空,则表示浏览器或提供方不支持 EVP,或者用户跳过了验证。如果发生这种情况,请回退到现有的验证流程,例如发送一次性密码或魔力链接。
如果存在令牌,请按如下方式验证:
- 使用 SD-JWT 库解析令牌。
- 验证预期值和会话声明。
- 验证 DNS 委托。
- 发现提供方元数据并提取 JWKS。
- 验证加密签名和密钥绑定。
1. 解析令牌
令牌采用 RFC 9901:选择性披露 JWT (SD-JWT+KB) 格式。使用适合您平台的库来解析和验证令牌。
例如,对于 Node,您可以使用 @sd-jwt/core 和 jose。其原始形式如下所示:一个由签发者签名的 JWT,后跟零个或多个披露信息,最后是一个密钥绑定 JWT,每个组成部分之间用波浪号分隔:
<Issuer-signed EVT>~<Disclosure 1>~...~<Disclosure N>~<Key Binding JWT>
在当前实现中,令牌包含零项披露信息 (<Issuer-signed EVT>~<Key Binding JWT>)。不过,未来可能会发生变化。
使用库对令牌进行解码:
import { decodeSdJwtSync } from "@sd-jwt/core";
import { createHash } from "node:crypto";
const hasher = (data, alg) =>
createHash(alg === "sha-256" ? "sha256" : alg)
.update(data)
.digest();
const decoded = decodeSdJwtSync(rawToken, hasher);
const evtPayload = decoded.jwt.payload;
const kbPayload = decoded.kbJwt?.payload;
如果 verifier.example 验证 demo@provider.example,则解码后的令牌类似于以下内容:
{
"evtJwtDecodedHeader": {
"typ": "evt+jwt",
"alg": "EdDSA",
"kid": "issuer-key-id"
},
"evtJwtDecodedPayload": {
"iss": "https://provider.example",
"iat": 12345678901,
"exp": 12345679901,
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "pUbLiCkEy123pUbLiCkEy123pUbLiCkEy123"
}
},
"email": "demo@provider.example",
"email_verified": true
},
"kbJwtDecodedHeader": {
"alg": "EdDSA",
"typ": "kb+jwt"
},
"kbJwtDecodedPayload": {
"aud": "https://verifier.example",
"iat": 12345678901,
"nonce": "rAnDoM123rAnDoM123rAnDoM123rAnDoM123",
"sd_hash": "hAsH456hAsH456hAsH456hAsH456hAsH456"
},
"disclosures": []
}
2. 验证预期值和会话声明
检查载荷中的基本值是否与您提供的值和预期值一致:
email_verified:必须为true。email:必须与表单中提交的电子邮件地址一致。aud(受众群体):必须与您网站的来源一致。nonce:必须与您在表单中提供的随机数一致。iat(签发时间)和exp(到期时间):确认令牌在有效时间范围内且未过期。
3. 验证 DNS 委托
验证电子邮件地址网域的 _email-verification DNS 记录。例如,对于 demo@gmail.com,查询 _email-verification.gmail.com TXT 记录。对于此提供方,查询会返回账号提供方的位置,即 accounts.google.com。
$ dig +short TXT _email-verification.gmail.com
"iss=accounts.google.com"
验证签发者方案是否为 https://,以及 https://<domain> 是否与 EVT 中的 iss 声明一致。
4. 验证 EVT 签名
从 https://<issuer>/.well-known/email-verification 中获取提供方的发现元数据:
{
"issuance_endpoint": "https://accounts.google.com/gsi/email-verification/issue",
"jwks_uri": "https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks",
"signing_alg_values_supported": ["EdDSA"]
}
从 jwks_uri 中提取 JSON Web 密钥集。
使用 SD-JWT 库验证令牌软件包。该库会协调验证:
- 根据提取的 JWKS 验证 EVT 上的颁发者签名。
- 使用
cnf.jwk中的临时公钥验证浏览器在 KB-JWT 上的签名。 - 验证密钥绑定(
aud、nonce和摘要哈希值sd_hash)。
Node.js 中的验证逻辑示例:
import { SDJwtInstance } from "@sd-jwt/core";
import { importJWK, compactVerify } from "jose";
import { createHash } from "node:crypto";
const hasher = (data, alg) =>
createHash(alg === "sha-256" ? "sha256" : alg)
.update(data)
.digest();
const sdJwt = new SDJwtInstance({ hasher });
sdJwt.config({
hasher,
// Verifier for the Issuer-signed EVT
verifier: async (data, sig) => {
const token = `${data}.${sig}`;
const header = decoded.jwt.header;
const headerAlg = header.alg || "ES256";
// Match by kid if present, or iterate across matching algorithm keys
const keysToTry = header.kid
? jwksData.keys.filter(k => k.kid === header.kid)
: jwksData.keys;
for (const jwk of keysToTry) {
try {
const pubKey = await importJWK(jwk, jwk.alg || headerAlg);
await compactVerify(token, pubKey);
return true;
} catch {
// Try next candidate key
}
}
return false;
},
// Verifier for the Key Binding JWT (KB-JWT)
kbVerifier: async (data, sig) => {
try {
const browserJwkKey = evtPayload.cnf?.jwk;
if (!browserJwkKey) return false;
const pubKey = await importJWK(browserJwkKey, decoded.kbJwt.header.alg || "ES256");
await compactVerify(`${data}.${sig}`, pubKey);
return true;
} catch {
return false;
}
},
});
// The library automatically verifies EVT signature, KB-JWT signature, audience, nonce, and sd_hash
const result = await sdJwt.verify(rawToken, {
kb: {
expectedNonce: sessionNonce,
expectedAudience: "https://example.com",
required: true,
},
});
const verifiedPayload = result.payload;
如果所有步骤都成功完成,则表示您已针对提供商验证了电子邮件地址。 如果验证失败,请回退到使用正常流程向用户发送确认电子邮件。
问题排查
如果验证失败或浏览器未提供令牌,请检查以下常见问题:
提交时令牌字段为空
- 源试用注册:确认网页上是否提供了
Origin-Trial标头或<meta>标记。对于第三方源试用(Chrome 154 及更高版本),注册的试用源必须与签发者 (https://<issuer-domain>) 属于同一网站。您可以在开发者工具中检查网站上的源试用配置,具体路径为应用 > 框架 > (选择相关框架)> 源试用。 - 表单标记:
<input type="email" autocomplete="email">和<input type="hidden" autocomplete="email-verification-token" nonce="...">必须位于同一<form>元素中(不能跨 Shadow DOM 边界隔离),并且nonce不得为空。 - 过早提交或重复使用网页:浏览器会在用户输入或自动填充电子邮件地址后在后台提取令牌。在请求完成之前提交会导致令牌为空。如果用户在输入电子邮件地址后按 Return 键提交表单,则可能会出现这种情况。
- 浏览器和提供方前提条件:用户必须在同一浏览器个人资料中登录参与计划的提供方,并且在 Chrome 设置 (
chrome://settings/contactInfo) 中启用已验证的电子邮件地址。
发卡机构签名验证失败
- 缺少
kid标头:EVT 标头和 JWKS 中的kid(密钥 ID)声明是可选的(例如,Gmail 省略了kid)。如果缺少kid,请遍历颁发者的jwks_uri中的所有候选密钥,而不是在密钥 ID 查找失败时失败。 - 算法标识符(EdDSA 和 Ed25519):签发者和库可以指定
EdDSA或Ed25519(以及ES256)。请确保您的 JWK 导入和验证逻辑接受这两个标识符。 - 签发者 (
iss) 源格式:DNS TXT 记录 (_email-verification.<domain>) 包含裸主机名 (iss=accounts.issuer.example),而 EVTiss声明是完整的 HTTPS 源(https://accounts.issuer.example,不带尾部斜杠)。在比较之前,为 DNS 记录值添加前缀https://。
密钥绑定 (KB-JWT) 验证失败
- 随机数不匹配或已过期:确保
<input>中呈现的nonce与服务器上的有效会话随机数相匹配,并且未被其他标签页覆盖或被之前的请求使用。 - 受众群体 (
aud) 不匹配:aud声明是验证者的 HTTPS 源 (https://verifier.example,不含路径或尾部斜杠)。
电子邮件声明 (email) 比较失败
- 大小写和规范化:Chrome 156 及更高版本会按原样返回表单中输入的
email声明(逐字节),但更早的浏览器版本或提供方可能会返回规范化的地址(例如,first.last@example.com的规范化地址为First.Last@example.com)。在将令牌的email声明与提交的表单值进行匹配时,请使用不区分大小写的比较。