หากต้องการใช้การยืนยันอีเมลในเว็บไซต์ ให้อัปเดตมาร์กอัปของแบบฟอร์มเพื่อขอโทเค็นและเพิ่มการตรวจสอบฝั่งเซิร์ฟเวอร์สำหรับโทเค็นขาเข้า
ลงทะเบียนเพื่อเข้าร่วมช่วงทดลองใช้จากต้นทาง
เว็บไซต์ที่ยืนยันแล้วต้องมีการกำหนดค่า Origin Trial ในเว็บไซต์
ตั้งแต่ 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>": เว็บไซต์ต้องระบุ Nonce ที่เชื่อมโยงกับเซสชันที่ไม่ซ้ำกัน เพื่อยืนยันการส่งแบบฟอร์ม
- ตั้งค่า
ตรวจสอบโทเค็นการยืนยันอีเมล (EVT)
เมื่อผู้ใช้ส่งแบบฟอร์ม เซิร์ฟเวอร์ของคุณจะได้รับอีเมลและโทเค็นจากฟิลด์ที่ซ่อนอยู่ ฟิลด์โทเค็นที่ว่างเปล่าแสดงว่าเบราว์เซอร์หรือ ผู้ให้บริการไม่รองรับ EVP หรือผู้ใช้ข้ามการยืนยัน หากเกิดกรณีนี้ ให้กลับไปใช้กระบวนการยืนยันตัวตนที่มีอยู่ เช่น การส่ง OTP หรือลิงก์ มหัศจรรย์
หากมีโทเค็น ให้ตรวจสอบดังนี้
- แยกวิเคราะห์โทเค็นโดยใช้ไลบรารี SD-JWT
- ตรวจสอบค่าที่คาดไว้และการอ้างสิทธิ์เซสชัน
- ยืนยันการมอบสิทธิ์ DNS
- ค้นหาข้อมูลเมตาของผู้ออกใบรับรองและดึงข้อมูล JWKS
- ยืนยันลายเซ็นการเข้ารหัสและการเชื่อมโยงคีย์
1. แยกวิเคราะห์โทเค็น
โทเค็นใช้รูปแบบ RFC 9901: Selective Disclosure JWT
(SD-JWT+KB) ใช้ไลบรารีที่เหมาะสมสำหรับแพลตฟอร์มของคุณเพื่อแยกวิเคราะห์และตรวจสอบโทเค็น
เช่น สำหรับ Node คุณสามารถใช้
@sd-jwt/core และ
jose ในรูปแบบดิบ ข้อมูลนี้จะมีลักษณะดังนี้
นี้: JWT ที่ลงนามโดยผู้ออก ตามด้วยการเปิดเผยอย่างน้อย 1 รายการ และปิดท้าย
ด้วย 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: ต้องเป็นtrueemail: ต้องตรงกับอีเมลที่ส่งในแบบฟอร์มaud(กลุ่มเป้าหมาย): ต้องตรงกับต้นทางของเว็บไซต์nonce: ต้องตรงกับ 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> ตรงกับ
การอ้างสิทธิ์ iss ใน EVT
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"]
}
ดึงข้อมูลชุดคีย์เว็บ JSON จาก jwks_uri
ใช้ไลบรารี SD-JWT เพื่อยืนยันแพ็กเกจโทเค็น ไลบรารีจะประสานงานการ ตรวจสอบ
- ตรวจสอบลายเซ็นของผู้ออกใน EVT กับ JWKS ที่ดึงข้อมูลมา
- การตรวจสอบลายเซ็นของเบราว์เซอร์ใน KB-JWT โดยใช้คีย์สาธารณะชั่วคราว
ใน
cnf.jwk - การยืนยันการเชื่อมโยงคีย์ (
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>) คุณสามารถตรวจสอบการกำหนดค่าช่วงทดลองใช้จากต้นทาง ในเว็บไซต์ได้ในเครื่องมือสำหรับนักพัฒนาเว็บที่ส่วนแอปพลิเคชัน > เฟรม > (เลือก เฟรมที่เกี่ยวข้อง) > ช่วงทดลองใช้จากต้นทาง - มาร์กอัปแบบฟอร์ม: ทั้ง
<input type="email" autocomplete="email">และ<input type="hidden" autocomplete="email-verification-token" nonce="...">ต้องอยู่ในองค์ประกอบ<form>เดียวกัน (ไม่ได้แยกกันในขอบเขต Shadow DOM ) และnonceต้องไม่ว่างเปล่า - ส่งก่อนเวลาหรือใช้หน้าเว็บซ้ำ: เบราว์เซอร์จะดึงโทเค็นใน เบื้องหลังหลังจากป้อนหรือป้อนอีเมลโดยอัตโนมัติ การส่งก่อนที่คำขอจะเสร็จสมบูรณ์จะทำให้โทเค็นว่างเปล่า ปัญหานี้อาจเกิดขึ้นหากผู้ใช้ กด Return เพื่อส่งแบบฟอร์มหลังจากป้อนอีเมล
- ข้อกำหนดเบื้องต้นของเบราว์เซอร์และผู้ให้บริการ: ผู้ใช้ต้องลงชื่อเข้าใช้ผู้ให้บริการที่เข้าร่วมในโปรไฟล์เบราว์เซอร์เดียวกัน และต้องเปิดใช้อีเมลที่ยืนยันแล้ว
ในการตั้งค่า Chrome (
chrome://settings/contactInfo)
การยืนยันลายเซ็นของผู้ออกบัตรไม่สำเร็จ
- ไม่มีส่วนหัว
kid: การอ้างสิทธิ์kid(รหัสคีย์) ในส่วนหัว EVT และ JWKS เป็นตัวเลือก (เช่น Gmail ละเว้นkid) หากไม่มีkidให้ วนซ้ำคีย์ผู้สมัครทั้งหมดในjwks_uriของผู้ออกแทนที่จะ ล้มเหลวในการค้นหารหัสคีย์ - ตัวระบุอัลกอริทึม (EdDSA และ Ed25519): ผู้ออกและไลบรารีอาจระบุ
EdDSAหรือEd25519(ควบคู่กับES256) ตรวจสอบว่าตรรกะการนำเข้าและการยืนยัน JWK ยอมรับตัวระบุทั้ง 2 รายการ - รูปแบบต้นทางของผู้ออก (
iss): ระเบียน TXT ของ DNS (_email-verification.<domain>) มีชื่อโฮสต์เปล่า (iss=accounts.issuer.example) ในขณะที่คำกล่าวอ้าง EVTissเป็นต้นทาง HTTPS แบบเต็ม (https://accounts.issuer.exampleโดยไม่มีเครื่องหมายทับต่อท้าย) เติม Prefixhttps://ให้กับค่าระเบียน DNS ก่อนเปรียบเทียบ
การตรวจสอบการเชื่อมโยงคีย์ (KB-JWT) ไม่สำเร็จ
- Nonce ไม่ตรงกันหรือหมดอายุ: ตรวจสอบว่า
nonceที่แสดงใน<input>ตรงกับ nonce ของเซสชันที่ใช้งานอยู่บนเซิร์ฟเวอร์และไม่ได้ถูกเขียนทับ โดยแท็บอื่นหรือใช้ไปกับคำขอก่อนหน้า - กลุ่มเป้าหมาย (
aud) ไม่ตรงกัน: การอ้างสิทธิ์audคือต้นทาง HTTPS ของผู้ยืนยัน (https://verifier.exampleโดยไม่มีเส้นทางหรือเครื่องหมายทับต่อท้าย)
การเปรียบเทียบการอ้างสิทธิ์ทางอีเมล (email) ไม่สำเร็จ
- การใช้อักษรตัวพิมพ์เล็กและตัวพิมพ์ใหญ่และการแปลงเป็นรูปแบบมาตรฐาน: Chrome 156 ขึ้นไปจะแสดงการอ้างสิทธิ์
emailแบบไบต์ต่อไบต์ตามที่ป้อนในแบบฟอร์ม แต่เบราว์เซอร์เวอร์ชันก่อนหน้าหรือ ผู้ให้บริการอาจแสดงที่อยู่ที่แปลงเป็นรูปแบบมาตรฐาน (เช่นFirst.Last@example.comสำหรับfirst.last@example.com) ใช้การเปรียบเทียบแบบไม่คำนึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่เมื่อจับคู่การอ้างสิทธิ์emailของโทเค็นกับ ค่าแบบฟอร์มที่ส่ง