電子郵件服務供應商 (發行者) 導入

如需更多詳細資料,請參閱模擬電子郵件供應商的示範程式碼,以及「電子郵件驗證 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=Lax Cookie 可正常運作,但跨網站要求會省略該 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 開始,瀏覽器會檢查 EVT email 聲明是否與要求的 email 完全相符。如果後端將地址正規化為標準帳戶格式 (例如要求 first.LAST@example.com 時傳回 First.Last@example.com),Chrome 會捨棄權杖。比對使用者帳戶的要求,但傳回要求主體中收到的確切 email 字串。
  • 缺少尾端波浪符號 (~):即使揭露次數為零,issuance_token 也必須是有效的 SD-JWT,且結尾為波浪符號 (<Issuer-signed-JWT>~),瀏覽器才能附加 <KB-JWT>。
  • iss 或 cnf.jwk 不符:請確認 EVT iss 聲明是與 .well-known/email-verification 中繼資料相符的確切 HTTPS 來源 (https://<issuer-domain>,沒有尾端斜線),且 cnf.jwk 會從 Signature-Key 標頭嵌入瀏覽器的暫時性公開金鑰。