依赖方实现

如需在您的网站上实现电子邮件验证,请更新表单标记以请求令牌,并为传入令牌添加服务器端验证。

注册参加源试用

验证网站必须在其网站上配置源试用。

自 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,或者用户跳过了验证。如果发生这种情况,请回退到现有的验证流程,例如发送一次性密码或魔法链接。

如果存在令牌,请按如下方式验证:

  1. 使用 SD-JWT 库解析令牌。
  2. 验证预期值和会话声明。
  3. 验证 DNS 委托。
  4. 发现颁发者元数据并提取 JWKS。
  5. 验证加密签名和密钥绑定。

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 库验证令牌软件包。该库会协调验证:

  1. 根据提取的 JWKS 验证 EVT 上的颁发者签名。
  2. 使用 cnf.jwk 中的临时公钥验证 KB-JWT 上的浏览器签名。
  3. 验证密钥绑定(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),而 EVT iss 声明是完整的 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 声明与提交的表单值进行匹配时,请使用不区分大小写的比较。