如要在網站上實作電子郵件驗證,請更新表單標記來要求權杖,並為傳入的權杖新增伺服器端驗證。
註冊來源試用
驗證網站時,必須在網站上設定原始碼試用。
自 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>":網站必須提供與工作階段綁定的專屬隨機碼,以驗證表單提交。
- 設定
驗證電子郵件驗證權杖 (EVT)
使用者提交表單時,您的伺服器會收到電子郵件地址和隱藏欄位中的權杖。如果權杖欄位空白,表示瀏覽器或供應商不支援 EVP,或是使用者略過驗證。如果發生這種情況,請改用現有的驗證程序,例如傳送動態密碼或魔術連結。
如有權杖,請按照下列步驟驗證:
- 使用 SD-JWT 程式庫剖析權杖。
- 驗證預期值和工作階段聲明。
- 驗證 DNS 委派。
- 探索簽發者中繼資料並擷取 JWKS。
- 驗證加密簽章和金鑰繫結。
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:必須與表單中提供的隨機碼相符。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 擷取簽發者的探索中繼資料:
{
"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"]
}
從 jwks_uri 擷取 JSON Web Key Set。
使用 SD-JWT 程式庫驗證權杖套件。程式庫會協調驗證作業:
- 根據擷取的 JWKS 驗證 EVT 上的簽名。
- 使用
cnf.jwk中的暫時性公開金鑰,驗證 KB-JWT 上的瀏覽器簽章。 - 驗證金鑰繫結 (
aud、nonce和摘要雜湊sd_hash)。
Node.js 中的驗證邏輯範例:
import { SDJwtInstance } from "@sd-jwt/core";
import { importJWK, compactVerify } from "jose";
import { createHash } from "node:crypto";
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;
const headerAlg = header.alg || "ES256";
// 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 || headerAlg);
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, decoded.kbJwt.header.alg || "ES256");
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;
如果所有步驟都成功,表示您已向供應商驗證電子郵件地址。 如果驗證失敗,請使用一般流程,改為傳送確認電子郵件給使用者。
疑難排解
如果驗證失敗或瀏覽器未提供權杖,請檢查下列常見問題:
提交時權杖欄位空白
- 來源試用註冊:確認網頁上是否提供
Origin-Trial標頭或<meta>標記。如果是第三方來源試用 (Chrome 154 以上版本),註冊的試用來源必須與發行者 (https://<issuer-domain>) 位於相同網站。您可以在開發人員工具中,依序選取「Application」 >「Frames」 > (選取相關影格) >「Origin trials」,檢查網站上的來源試用設定。 - 表單標記:
<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 查詢失敗時停止作業。 - 演算法 ID (EdDSA 和 Ed25519):簽發者和程式庫可以指定
EdDSA或Ed25519(以及ES256)。請確保 JWK 匯入和驗證邏輯接受這兩種 ID。 - 簽發者 (
iss) 來源格式:DNS TXT 記錄 (_email-verification.<domain>) 包含裸主機名稱 (iss=accounts.issuer.example),而 EVTiss聲明是完整的 HTTPS 來源 (https://accounts.issuer.example,不含尾端斜線)。比較前,請在 DNS 記錄值加上前置字元https://。
金鑰繫結 (KB-JWT) 驗證失敗
- Nonce 不符或已過期:請確認
<input>中呈現的nonce與伺服器上的有效工作階段 Nonce 相符,且未遭其他分頁覆寫,或由先前的要求使用。 - 目標對象 (
aud) 不符:aud聲明是驗證者的 HTTPS 來源 (https://verifier.example,沒有路徑或尾端斜線)。
電子郵件權杖附加資訊 (email) 比較失敗
- 大小寫和正規化:Chrome 156 以上版本會傳回表單中輸入的
email聲明 (逐一比對位元組),但較舊的瀏覽器版本或供應商可能會傳回正規化地址 (例如first.last@example.com的First.Last@example.com)。比對權杖的email聲明與提交的表單值時,請使用不區分大小寫的比較方式。