如需更多詳細資料,請參閱模擬電子郵件供應商的示範程式碼,以及「電子郵件驗證 API」和「電子郵件驗證通訊協定」提案中的發行者步驟。
身為發行者,您不需要註冊原始碼試用計畫或提供權杖,因為依賴方網站會觸發瀏覽器行為。請確保端點已設定為回應這些要求。
設定簽發者探索
如要讓瀏覽器在選取網域所屬的電子郵件地址時,自動探索驗證端點,請使用 DNS 和 .well-known HTTP 端點公開設定。
設定 DNS 委派記錄
在電子郵件網域中設定 DNS TXT 記錄,將驗證授權委派給發行者 ID。視基礎架構而定,這些 ID 可以使用相同網域。
記錄格式:_email-verification.<email-domain>
區域檔案範例:
_email-verification.example.com IN TXT "iss=accounts.issuer.example"
代管 .well-known/email-verification 端點
在發行者網域的 /.well-known/ 路徑下代管 JSON 中繼資料檔案。這個檔案會列出您的發行功能,以及基礎架構支援的密碼編譯簽署演算法。
端點:https://<issuer-domain>/.well-known/email-verification
回應範例:
{
"issuance_endpoint": "https://accounts.issuer.example/email-verification/issuance",
"jwks_uri": "https://accounts.issuer.example/.well-known/vc-public-jwks",
"signing_alg_values_supported": ["EdDSA", "ES256"]
}
代管 .well-known/web-identity 端點
您可能已實作額外的 .well-known JSON 資源,做為聯合憑證 (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"
}
]
}
整合 Login Status API
使用者必須與供應商建立有效的工作階段,且您必須使用 Login Status API 向瀏覽器發出信號。
使用者成功登入或登出時,請提供相符的 HTTP 回應標頭:
Set-Login: logged-in
Set-Login: logged-out
或者,您也可以在網頁應用程式環境中使用 JavaScript 更新狀態:
navigator.login.setStatus("logged-in");
navigator.login.setStatus("logged-out");
處理核發要求
您的 issuance_endpoint 會收到 application/json POST 要求,其中包含 email 金鑰和 HTTP 訊息簽章標頭 (適用於 Signature、Signature-Input 和 Signature-Key)。
為您的環境使用支援結構化標頭和 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;crv="Ed25519";kty="OKP";x="gHi890_gHi890_gHi890"
{email: "demo@example.com"}
剖析並驗證要求:
- 工作階段驗證:驗證隨要求傳送的第一方工作階段 Cookie。使用者必須通過驗證。
Sec-Fetch-Dest標頭:設為email-verification。- HTTP 訊息簽章:使用
Signature-Key中的暫時公開金鑰驗證要求簽章,並驗證Content-Digest。 - 酬載:JSON 主體包含要求驗證的
email字串。
核發回應
成功驗證工作階段和要求權杖後,請使用適用於您平台的適當程式庫,產生以 JSON 形式傳回的簽署選擇性揭露 JWT (SD-JWT)。舉例來說,如果是 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 可正常運作,但跨網站要求會省略該 Cookie。 - 探索或帳戶不符:確認
_email-verification.<email-domain>傳回單一 TXT 記錄 (iss=<issuer-domain>,不含https://)、兩個.well-known端點都傳回Content-Type: application/json,且accounts_endpoint回應包含email與輸入地址相符的帳戶。
核發要求驗證失敗
Sec-Fetch-Dest標頭拼字:Chrome 154 以上版本會傳送Sec-Fetch-Dest: email-verification(含連字號),而 Chrome 153 則會傳送emailverification(不含連字號)。在推出期間接受這兩個值。- Proxy 後方的 HTTP 訊息簽章 (
@authority) 不符:驗證 RFC 9421 簽章時,@authority元件會反映面向公眾的主機。如果伺服器位於反向 Proxy 或負載平衡器後方,請使用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標頭嵌入瀏覽器的暫時性公開金鑰。