การใช้งาน Relying Party

หากต้องการใช้การยืนยันอีเมลในเว็บไซต์ ให้อัปเดตมาร์กอัปของแบบฟอร์มเพื่อขอโทเค็นและเพิ่มการตรวจสอบฝั่งเซิร์ฟเวอร์สำหรับโทเค็นขาเข้า

ลงทะเบียนเพื่อเข้าร่วมช่วงทดลองใช้จากต้นทาง

เว็บไซต์ที่ยืนยันแล้วต้องมีการกำหนดค่า 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 หรือลิงก์ มหัศจรรย์

หากมีโทเค็น ให้ตรวจสอบดังนี้

  1. แยกวิเคราะห์โทเค็นโดยใช้ไลบรารี SD-JWT
  2. ตรวจสอบค่าที่คาดไว้และการอ้างสิทธิ์เซสชัน
  3. ยืนยันการมอบสิทธิ์ DNS
  4. ค้นหาข้อมูลเมตาของผู้ออกใบรับรองและดึงข้อมูล JWKS
  5. ยืนยันลายเซ็นการเข้ารหัสและการเชื่อมโยงคีย์

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: ต้องเป็น true
  • email: ต้องตรงกับอีเมลที่ส่งในแบบฟอร์ม
  • 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 เพื่อยืนยันแพ็กเกจโทเค็น ไลบรารีจะประสานงานการ ตรวจสอบ

  1. ตรวจสอบลายเซ็นของผู้ออกใน EVT กับ JWKS ที่ดึงข้อมูลมา
  2. การตรวจสอบลายเซ็นของเบราว์เซอร์ใน KB-JWT โดยใช้คีย์สาธารณะชั่วคราว ใน cnf.jwk
  3. การยืนยันการเชื่อมโยงคีย์ (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) ในขณะที่คำกล่าวอ้าง EVT iss เป็นต้นทาง HTTPS แบบเต็ม (https://accounts.issuer.example โดยไม่มีเครื่องหมายทับต่อท้าย) เติม Prefix https:// ให้กับค่าระเบียน 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 ของโทเค็นกับ ค่าแบบฟอร์มที่ส่ง