Email Verification API

传统邮箱验证通常采用 Double Opt-In:用户提交邮箱后,服务端发送验证邮件,用户切换到邮箱客户端打开邮件,再点击链接返回网站完成验证。这个流程虽然可靠,但存在明显的行为断点。新的 Email Verification API 试图让浏览器在用户不离开当前页面的情况下完成邮箱所有权验证。

它的核心工作流程是:

  1. 用户填写或选择邮箱地址
  2. 浏览器根据邮箱域名发现对应的邮箱服务,并检查用户是否已登录该邮箱服务
  3. 邮箱服务向浏览器签发经过密码学签名的 Email Verification Token(EVT)

    1. 先根据邮箱域名通过 DNS 找到对应的验证服务,再获取其验证元数据和公钥。

      -> % dig +noall +answer txt _email-verification.gmail.com
      _email-verification.gmail.com. 3592 IN  TXT "iss=accounts.google.com"
      
      -> % curl https://accounts.google.com/.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"]
      }
      
      • issuance_endpoint: 浏览器向哪里请求 Email Verification Token
      • jwks_uri: issuer 的公钥在哪里,用于验证签发的 Token
      • signing_alg_values_supported: 支持的签名算法
    2. 浏览器生成临时密钥对

    3. 浏览器构造签发请求。Chrome 153 起不再使用 request_token JWT,而是对 HTTP 请求做 HTTP Message Signatures

      POST /gsi/email-verification/issue HTTP/1.1
      Host: accounts.google.com
      Accept: application/json
      Content-Type: application/json
      Content-Digest: sha-256=:...:
      Sec-Fetch-Dest: email-verification
      Signature-Input: sig=("@method" "@authority" "@path" "content-digest" "signature-key");created=1786455840
      Signature: sig=:...:
      Signature-Key: sig=hwk;crv="Ed25519";kty="OKP";x="..."
      
      {"email":"alice@gmail.com"}
      

      签名覆盖 @method@authority@pathcontent-digestsignature-keyContent-Digest 是请求体的 SHA-256,用来把邮箱地址绑进签名。浏览器临时公钥放在 Signature-Keyhwk 参数里,不放进 JWT header。

  4. 浏览器将 EVT 与网站的 origin 以及服务端生成的一次性 nonce 绑定,并将绑定后的 token 自动写入表单

  5. 服务端收到邮箱和验证 Token 后,

    1. 先根据邮箱域名通过 DNS 找到对应的验证服务,再获取其验证元数据和公钥。

      -> % dig +noall +answer txt _email-verification.gmail.com
      _email-verification.gmail.com. 3592 IN  TXT "iss=accounts.google.com"
      
      -> % curl https://accounts.google.com/.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"]
      }
      
      • issuance_endpoint: 浏览器向哪里请求 Email Verification Token
      • jwks_uri: issuer 的公钥在哪里,用于验证签发的 Token
      • signing_alg_values_supported: 支持的签名算法
    2. 随后解析 Token,验证签名及邮箱地址、网站 Origin、nonce 等绑定信息,

    3. 并检查 nonce 是否与当前 Session 匹配且未被使用。
    4. 全部通过后,服务器将邮箱标记为已验证。

其中,nonce 由服务端生成并与当前 Session 绑定,每次页面渲染都应使用新的随机值。它会被绑定到最终的验证 token 中,用于防止 token 被截获后重放到其他请求或上下文。

例如用户在 Chrome 中登录了 Gmail,同时注册表单填写的也是该 Gmail 地址,浏览器可以协调 Gmail 完成身份确认,并将验证 token 写入隐藏字段。整个过程中不需要发送传统的验证邮件,也不需要用户点击邮件中的链接。

前端只需要增加一个隐藏字段:

<input type="email" name="email" autocomplete="email" />
<input
  type="hidden"
  name="token"
  autocomplete="email-verification-token"
  nonce="server-generated-random-value"
/>

因此,这套机制本质上并不是“浏览器替网站发送一封验证邮件”,而是建立了一条新的邮箱所有权证明链路:邮箱服务负责证明用户控制该邮箱,浏览器负责协调和绑定证明,网站服务器最终独立验证证明。

目前该 API 仍处于早期的草案阶段,实际支持范围有限。因此产品层面不应该直接替换传统邮件验证,而应该采用渐进增强(Progressive Enhancement):支持该机制的环境直接完成验证,不支持时继续回退到传统邮件验证。

它最大的价值,是把原本“网站 → 邮箱 → 用户 → 网站”的跨应用验证流程,变成“邮箱服务 → 浏览器 → 网站”的密码学证明流程,从而减少用户操作,同时降低无效邮箱带来的验证和投递成本。

脚本请求签发端点时的 403

浏览器外复现签发请求时,第六步 HTTP Error 403: Forbidden 不是 Cookie 写错,而是请求在进到签发逻辑之前就被 Google 前端拒绝了。规范里签名失败应返回 400,会话无效应返回 401;直接 403 说明打到了错误路径,或者客户端不像浏览器。

有三处必须同时改:

  1. 元数据请求失败时不能回退到 https://accounts.google.com/email-verification/issuance。Gmail 的真实地址是 https://accounts.google.com/gsi/email-verification/issue。不存在的路径在 accounts.google.com 上经常直接 403,而不是 404。
  2. urllib 默认 User-Agent: Python-urllib/...。带登录 Cookie 访问 Google 账号域时,这个标识会被前端 403。请求要带浏览器 User-Agent,并且带上 Sec-Fetch-Dest: email-verification
  3. Chrome 153(2026-08)起,签发请求不再是 application/x-www-form-urlencodedrequest_token。请求体改成 {"email":"..."},并用 HTTP Message Signatures 签名。签名基字符串的每一行覆盖组件都以 LF 结尾,最后一行 @signature-params 不加换行。Content-DigestSignature 用普通 Base64(带填充),公钥 x 用 Base64url(不带填充)。

Cookie 过期或邮箱与登录账号不一致时,修正后应看到 JSON authentication_required(401),而不是没有正文的 403。

如果你有魔法,你可以看到一个评论框~