如需在您的网站上实现电子邮件验证,请更新表单标记以请求令牌,并为传入令牌添加服务器端验证。
注册参加源试用
验证网站必须在其网站上配置源试用。
自 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>":网站必须提供与会话绑定的唯一 nonce,以验证表单提交。
- 设置
验证邮箱验证令牌 (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:必须与表单中提供的 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提取颁发者的发现元数据:
{
"issuer": "https://accounts.google.com",
"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": ["Ed25519", "EdDSA"]
}
如果元数据包含 issuer 字段,请验证该字段是否与 EVT 中的 iss
声明完全一致。从 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 allowedAlgs = ["Ed25519", "EdDSA", "ES256"];
const evtAlg = decoded.jwt.header.alg;
const kbAlg = decoded.kbJwt?.header.alg;
if (!allowedAlgs.includes(evtAlg) || !allowedAlgs.includes(kbAlg)) {
throw new Error("Unsupported signing algorithm");
}
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;
// 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 || evtAlg);
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, kbAlg);
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;
if (
verifiedPayload.email_verified !== true ||
verifiedPayload.email?.toLowerCase() !== submittedEmail.toLowerCase()
) {
throw new Error("Token email does not match submitted email");
}
如果所有步骤都成功完成,则表示您已针对提供方验证了电子邮件地址。 如果验证失败,请回退到使用正常流程向用户发送确认邮件。
问题排查
如果验证失败或浏览器未提供令牌,请检查以下常见问题:
提交时令牌字段为空
- 源试用注册:确认页面上提供了
Origin-Trial标头或<meta>标记,并且令牌未过期 (在 Chrome 160 延期之前生成的令牌将于 11 月 17 日 2026 年过期)。对于第三方源试用(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 查找失败而失败。 - 算法标识符(
Ed25519和EdDSA) :从 Chrome 156 开始,Chrome 对cnf.jwk和KB-JWT标头使用完全指定的Ed25519算法标识符 (RFC 9864),而颁发者和库可以指定Ed25519或EdDSA(以及ES256)。确保您的 JWK 导入和验证逻辑接受这两个标识符。 - 颁发者 (
iss) 来源格式:DNS TXT 记录 (_email-verification.<domain>) 包含裸主机名 (iss=accounts.issuer.example),而 EVTiss声明是完整的 HTTPS 来源 (https://accounts.issuer.example),不带尾部斜杠。在比较之前,为 DNS 记录值添加https://前缀。
密钥绑定 (KB-JWT) 验证失败
- nonce 不匹配或已过期:确保
nonce中呈现的<input>与服务器上的活跃会话 nonce 一致,并且未被其他标签页覆盖 或被之前的请求使用。 - 受众群体 (
aud) 不匹配 :aud声明是验证者的 HTTPS 来源 (https://verifier.example,不带路径或尾部斜杠)。
电子邮件声明 (email) 比较失败
- 大小写和规范化:Chrome 156 及更高版本会完全按照表单中的输入返回
email声明,但之前的浏览器版本或提供方可能会返回规范化的地址(例如,first.last@example.com返回First.Last@example.com)。在将令牌的email声明与提交的表单值进行匹配时,请使用不区分大小写的比较。