如需了解更多详情,您可以浏览模拟邮件服务提供商演示 代码 ,并参阅邮箱验证 API和邮箱验证 协议 提案中的签发者步骤。
作为签发者,您无需注册源试用,也无需提供令牌,因为依赖方网站会触发浏览器行为。确保您的端点已配置为响应这些请求。
配置签发者发现
如需允许浏览器在选择属于您网域的电子邮件地址时自动发现您的验证端点,请使用 DNS 和 .well-known HTTP 端点公开您的配置。
配置 DNS 委托记录
在您的电子邮件网域中配置 DNS TXT 记录,以将验证权限委托给您的签发者标识符。根据您的基础架构,这些标识符可以使用相同的网域。
记录格式:_email-verification.<email-domain>
区域文件示例:
_email-verification.example.com IN TXT "iss=accounts.issuer.example"
The iss= 值必须是规范的小写主机名(例如
iss=accounts.issuer.example),且不包含架构 (https://)、端口、路径或
大写字符。Chrome 会拒绝包含非主机名组件的记录。
托管 .well-known/email-verification 端点
在 /.well-known/ 路径下的签发者网域中托管 JSON 元数据文件。
此文件概述了您的签发功能以及您的基础架构支持的加密签名算法。
端点:https://<issuer-domain>/.well-known/email-verification
示例响应:
{
"issuer": "https://accounts.issuer.example",
"issuance_endpoint": "https://accounts.issuer.example/email-verification/issuance",
"jwks_uri": "https://accounts.issuer.example/.well-known/vc-public-jwks",
"signing_alg_values_supported": ["Ed25519", "EdDSA", "ES256"]
}
元数据属性:
issuer:您的签发者来源(https://<issuer-domain>,不带尾部 斜杠)。必须与元数据文件所服务的来源完全一致。issuance_endpoint:接收令牌签发POST请求的 HTTPS 网址。jwks_uri:提供公开 JSON Web 密钥集 (JWKS) 的 HTTPS 网址。signing_alg_values_supported:受支持的 JWS 签名算法数组。
托管 .well-known/web-identity 端点
您可能已实现额外的 .well-known JSON 资源,作为
Federated Credentials (FedCM)
API 的一部分。此资源提供指向您的账号端点和登录网址的链接。
端点:https://<domain>/.well-known/web-identity
示例响应:
{
"accounts_endpoint": "https://accounts.issuer.example/accounts",
"login_url": "https://accounts.issuer.example/login"
}
使用账号端点
FedCM API 中的账号端点目前提供已登录账号的列表。以下示例展示了最简单的响应。如需了解详情, 请参阅身份提供方实现 指南。
端点:如 .well-known/web-identity 中所指定
以下是一个示例响应:
{
"accounts": [
{
"id": "demo-example",
"name": "Demo User",
"email": "demo@example.com",
"given_name": "Demo"
}
]
}
与登录状态 API 集成
用户必须与提供方建立活跃会话,并且您必须使用登录状态 API向浏览器发出信号。
当用户成功登录或退出时,提供匹配的 HTTP 响应标头:
Set-Login: logged-in
Set-Login: logged-out
或者,在 Web 应用上下文中使用 JavaScript 更新状态:
navigator.login.setStatus("logged-in");
navigator.login.setStatus("logged-out");
处理签发请求
您的 issuance_endpoint 接收包含 email 键以及 HTTP 消息
签名 标头(用于 Signature、
Signature-Input 和 Signature-Key)的 application/json POST 请求。
为您的环境使用支持结构化标头和 HTTP 消息签名的库。在 Node.js 中,您可以使用
structured-headers 和
http-message-sig。
完整请求格式:
POST /email-verification/issuance HTTP/1.1
Host: provider.example
Accept: application/json
Content-Digest: sha-256=:aBc123aBc123aBc123aBc123aBc123=:
Content-Type: application/json
Sec-Fetch-Dest: email-verification
Signature: sig=:+dEf567dEf567/dEf567dEf567dEf567/dEf567==:
Signature-Input: sig=("@method" "@authority" "@path" "content-digest" "signature-key");created=1786455840
Signature-Key: sig=hwk;alg="Ed25519";crv="Ed25519";kty="OKP";x="gHi890_gHi890_gHi890"
{email: "demo@example.com"}
解析并验证请求:
- 会话身份验证:验证随请求发送的第一方会话 Cookie。用户必须经过身份验证。
Sec-Fetch-Dest标头 :设置为email-verification。- HTTP 消息签名:使用
中的临时公钥验证请求签名,并验证
Signature-KeyContent-Digest。 从 Chrome 156 开始,Signature-Key标头的alg="Ed25519"在其hwk参数中包含;接受"Ed25519"和"EdDSA",并允许alg由较早版本的 Chrome 省略。 - 载荷:JSON 正文包含请求
验证的
email字符串。
签发响应
成功验证会话和请求令牌后,使用适合您平台的相应库生成签名选择性披露 JWT (SD-JWT),并以 JSON 格式返回。例如,对于 Node,您可以使用
@sd-jwt/core 和
jose。
原始载荷格式应如下所示:
{
"iss": "https://accounts.issuer.example",
"iat": 1780272000,
"exp": 1780272300,
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "pUbLiCKeY123pUbLiCKeY123pUbLiCKeY123",
"y": "pUbLiCKeY456pUbLiCKeY456pUbLiCKeY456"
}
},
"email": "demo@example.com",
"email_verified": true
}
创建令牌、对其进行签名并返回:
import { importJWK, CompactSign } from "jose";
import { SDJwtInstance } from "@sd-jwt/core";
import crypto from "node:crypto";
// Issuer private key from secure storage
const privateKey = await importJWK(PRIVATE_KEY_JWK, "EdDSA");
const origin = url.origin;
const currentTime = Math.floor(Date.now() / 1000);
const evtPayload = {
iss: origin,
iat: currentTime,
exp: currentTime + 300, // 5 minutes
cnf: {
jwk: browserJwk,
},
email: payload.email, // exactly as received in payload
email_verified: true,
};
const sdJwt = new SDJwtInstance({
signer: async (data) => {
const [headerB64, payloadB64] = data.split(".");
const header = JSON.parse(Buffer.from(headerB64, "base64url").toString());
const payload = Buffer.from(payloadB64, "base64url");
const signed = await new CompactSign(payload).setProtectedHeader(header).sign(privateKey);
return signed.split(".").pop()!;
},
signAlg: "EdDSA",
hasher: async (data, alg) => {
const nodeAlg = alg.replace("-", "");
return new Uint8Array(crypto.createHash(nodeAlg).update(data).digest());
},
hashAlg: "sha-256",
saltGenerator: async () => crypto.randomBytes(16).toString("base64url"),
});
const issuanceToken = await sdJwt.issue(evtPayload, undefined, {
header: {
alg: "EdDSA",
kid: PRIVATE_KEY_JWK.kid,
typ: "evt+jwt",
},
});
return sendResponse({ issuance_token: issuanceToken, }, 200);
生成的响应正文如下所示:
{
"issuance_token": "tOkEn123tOkEn123tOkEn123...~"
}
创建邮箱验证令牌并对其进行签名后,浏览器会将此令牌传递给验证方网站以进行验证。
问题排查
如果浏览器未与您的端点联系或拒绝签发的令牌,请检查以下常见问题:
浏览器从不调用 accounts_endpoint 或 issuance_endpoint
- 未设置登录状态:Chrome 仅在知道
用户已登录的情况下查询您的端点。确保您的登录流程设置了
Set-Login: logged-inHTTP 标头或调用了navigator.login.setStatus("logged-in")。 - 会话 Cookie 被屏蔽 (
SameSite=None) :浏览器从不同网站上的依赖方提取您的accounts_endpoint和issuance_endpoint。由于这些是跨站请求,因此您的会话 Cookie 必须包含SameSite=None; Secure。在同一网站验证方上进行测试时,SameSite=LaxCookie 可以正常运行,但在跨站请求中会被省略。 - 发现或账号不匹配:验证
_email-verification.<email-domain>是否返回包含 规范小写主机名(iss=<issuer-domain>,不带https://、 端口、路径或大写字符)的单个 TXT 记录,两个.well-known端点是否都返回Content-Type: application/json,.well-known/email-verification是否具有与https://<issuer-domain>完全一致的issuer,以及accounts_endpoint响应是否包含email与 输入地址匹配的账号。 .well-known/email-verification中不支持的签名算法 :Chrome 154 和 155 需要"EdDSA"中的signing_alg_values_supported, 而 Chrome 156+ 和 IETF 规范支持"Ed25519"。 在过渡期间同时宣传["Ed25519", "EdDSA"],这样 Chrome 稳定版就不会拒绝您的发现元数据。
签发请求验证失败
Sec-Fetch-Dest标头拼写:Chrome 154+ 发送Sec-Fetch-Dest: email-verification(带连字符),而 Chrome 153 发送emailverification(不带连字符)。在发布期间接受这两个值。- 代理后面的 HTTP 消息签名 (
@authority) 不匹配 :验证 RFC 9421 签名时,@authority组件反映的是面向公众的主机。如果您的服务器位于反向代理或负载均衡器后面,请使用X-Forwarded-Host(或您的公开来源)而不是内部主机名来重建验证网址,并在 JSON 解析之前对原始请求正文字节计算Content-Digest。
浏览器拒绝返回的 issuance_token
- 修改后的或规范化的
email声明:从 Chrome 156 开始,浏览器 会检查 EVTemail声明是否与请求的email完全一致。如果您的后端将地址规范化为规范账号格式(例如,在请求first.LAST@example.com时返回First.Last@example.com),Chrome 会丢弃令牌。将请求与用户的账号进行匹配,但返回在请求正文中收到的确切email字符串。 - 缺少尾部波浪号 (
~):即使披露数量为零,issuance_token也必须是有效的 SD-JWT,并以尾部波浪号 (<Issuer-signed-JWT>~) 结尾,以便浏览器可以附加<KB-JWT>。 iss或cnf.jwk不匹配:确保 EVTiss声明是与您的.well-known/email-verification元数据完全一致的 HTTPS 来源(https://<issuer-domain>,不带尾部斜杠),并且cnf.jwk嵌入了来自Signature-Key标头的 浏览器临时公钥。