Implementasi pihak tepercaya

Untuk menerapkan verifikasi email di situs Anda, perbarui markup formulir untuk meminta token dan tambahkan validasi sisi server untuk token yang masuk.

Mendaftar untuk uji coba origin

Situs yang memverifikasi harus mengonfigurasi uji coba origin di situsnya.

Mulai Chrome 154, uji coba origin pihak ketiga didukung dengan peringatan penting: origin yang terdaftar untuk uji coba harus memiliki situs yang sama dengan penerbit. Contoh:

  • Domain penerbit: issuer.example
  • Pendaftar OT: https://issuer.example
  • Origin JavaScript: https://issuer.example (atau https://app.issuer.example dengan pencocokan subdomain)

Mengonfigurasi kolom formulir

Tambahkan kolom token tersembunyi ke formulir pengiriman email Anda:

<input
  type="email"
  name="email-address"
  autocomplete="email">
<input
  type="hidden"
  name="token"
  autocomplete="email-verification-token"
  nonce="rAnD0m-VaLuE">

Persyaratan kolom:

  • Kolom email: Tetapkan type="email" dan autocomplete="email" agar Chrome dapat mengisi otomatis dan mengenali alamat.
  • Atribut kolom token:
    • Setel autocomplete="email-verification-token": Chrome mengidentifikasi kolom ini untuk mengisi token saat pengiriman.
    • Set nonce="<VALUE>": Situs harus memberikan nonce unik yang terikat sesi untuk memverifikasi pengiriman formulir.

Memvalidasi token verifikasi email (EVT)

Saat pengguna mengirimkan formulir, server Anda akan menerima alamat email dan token dari kolom tersembunyi. Kolom token kosong menunjukkan bahwa browser atau penyedia tidak mendukung EVP, atau pengguna melewati verifikasi. Jika hal ini terjadi, kembali ke proses verifikasi yang ada, seperti mengirim OTP atau link ajaib.

Jika ada token, validasi token tersebut sebagai berikut:

  1. Parse token menggunakan library SD-JWT.
  2. Memvalidasi nilai yang diharapkan dan klaim sesi.
  3. Verifikasi delegasi DNS.
  4. Temukan metadata penerbit dan ambil JWKS.
  5. Verifikasi tanda tangan kriptografi dan pengikatan kunci.

1. Mengurai token

Token menggunakan format RFC 9901: Selective Disclosure JWT (SD-JWT+KB). Gunakan library yang sesuai untuk platform Anda guna mengurai dan memvalidasi token. Misalnya, untuk Node, Anda dapat menggunakan @sd-jwt/core dan jose. Dalam bentuk mentahnya, tampilannya seperti ini: JWT yang ditandatangani Penerbit, diikuti dengan nol atau lebih Pengungkapan, dan diakhiri dengan JWT Pengikatan Kunci dengan setiap komponen dipisahkan oleh tilde:

<Issuer-signed EVT>~<Disclosure 1>~...~<Disclosure N>~<Key Binding JWT>

Dalam implementasi saat ini, token berisi nol pengungkapan (<Issuer-signed EVT>~<Key Binding JWT>). Namun, hal ini dapat berubah pada masa mendatang.

Dekode token dengan library:

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;

Jika verifier.example memverifikasi demo@provider.example, token yang didekode akan terlihat mirip dengan berikut:

{
  "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. Memvalidasi nilai yang diharapkan dan klaim sesi

Periksa apakah nilai dasar dalam payload cocok dengan nilai yang Anda berikan dan harapkan:

  • email_verified: Harus true.
  • email: Harus cocok dengan alamat email yang dikirimkan dalam formulir.
  • aud (audiens): Harus cocok dengan asal situs Anda.
  • nonce: Harus cocok dengan nonce yang diberikan dalam formulir Anda.
  • iat (dikeluarkan pada) dan exp (masa berlaku): Pastikan token berada dalam jangka waktu yang valid dan belum habis masa berlakunya.

3. Memverifikasi delegasi DNS

Verifikasi data DNS _email-verification untuk domain alamat email. Misalnya, untuk demo@gmail.com, kueri data TXT _email-verification.gmail.com. Untuk penyedia ini, kueri menampilkan lokasi penyedia akun, yaitu accounts.google.com.

$ dig +short TXT _email-verification.gmail.com
"iss=accounts.google.com"

Verifikasi bahwa skema penerbit adalah https:// dan https://<domain> cocok dengan klaim iss di EVT.

4. Memverifikasi tanda tangan EVT

Ambil metadata penemuan penerbit dari 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"]
}

Ambil JSON Web Key Set dari jwks_uri.

Gunakan library SD-JWT Anda untuk memverifikasi paket token. Library mengoordinasikan validasi:

  1. Memvalidasi tanda tangan penerbit pada EVT terhadap JWKS yang diambil.
  2. Memvalidasi tanda tangan browser pada KB-JWT menggunakan kunci publik sementara di cnf.jwk.
  3. Memverifikasi pengikatan kunci (aud, nonce, dan hash ringkasan sd_hash).

Contoh logika verifikasi di 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;

Jika semua langkah berhasil, Anda telah memverifikasi alamat email terhadap penyedia. Jika verifikasi gagal, kirim email konfirmasi kepada pengguna menggunakan alur normal Anda.

Pemecahan masalah

Jika verifikasi gagal atau browser tidak memberikan token, periksa masalah umum berikut:

Kolom token kosong saat pengiriman

  • Pendaftaran uji coba origin: Pastikan header Origin-Trial atau tag <meta> ditayangkan di halaman. Untuk uji coba origin pihak ketiga (Chrome 154+), origin uji coba terdaftar harus memiliki situs yang sama dengan penerbit (https://<issuer-domain>). Anda dapat memeriksa konfigurasi uji coba origin di situs di DevTools pada bagian Application > Frames > (pilih frame yang relevan) > Origin trials.
  • Markup formulir: <input type="email" autocomplete="email"> dan <input type="hidden" autocomplete="email-verification-token" nonce="..."> harus berada dalam elemen <form> yang sama (tidak terisolasi di seluruh batas Shadow DOM), dan nonce tidak boleh kosong.
  • Pengiriman awal atau halaman yang digunakan kembali: Browser mengambil token di latar belakang setelah email dimasukkan atau diisi otomatis. Mengirimkan sebelum permintaan selesai akan membuat token kosong. Hal ini dapat terjadi jika pengguna menekan Kembali untuk mengirimkan formulir setelah memasukkan emailnya.
  • Prasyarat browser dan penyedia: Pengguna harus login ke penyedia yang berpartisipasi di profil browser yang sama dan mengaktifkan Email Terverifikasi di setelan Chrome (chrome://settings/contactInfo).

Verifikasi tanda tangan penerbit gagal

  • Header kid tidak ada: Klaim kid (ID Kunci) di header EVT dan JWKS bersifat opsional (misalnya, Gmail tidak menyertakan kid). Jika kid tidak ada, lakukan iterasi pada semua kunci kandidat di jwks_uri penerbit, bukan gagal pada pencarian ID kunci.
  • ID algoritma (EdDSA dan Ed25519): Penerbit dan library dapat menentukan EdDSA atau Ed25519 (bersama dengan ES256). Pastikan logika verifikasi dan impor JWK Anda menerima kedua ID tersebut.
  • Format asal Penerbit (iss): Data TXT DNS (_email-verification.<domain>) berisi nama host kosong (iss=accounts.issuer.example), sedangkan klaim iss EVT adalah asal HTTPS lengkap (https://accounts.issuer.example, tanpa garis miring di akhir). Tambahkan awalan https:// ke nilai data DNS sebelum membandingkan.

Validasi pengikatan kunci (KB-JWT) gagal

  • Nonce tidak cocok atau telah habis masa berlakunya: Pastikan nonce yang dirender di <input> cocok dengan nonce sesi aktif di server Anda dan belum ditimpa oleh tab lain atau digunakan oleh permintaan sebelumnya.
  • Ketidakcocokan audiens (aud): Klaim aud adalah asal HTTPS verifier (https://verifier.example, tanpa jalur atau garis miring di akhir).

Perbandingan klaim email (email) gagal

  • Penggunaan huruf besar/kecil dan kanonikalisasi: Chrome 156+ menampilkan klaim email byte demi byte seperti yang dimasukkan dalam formulir, tetapi versi atau provider browser yang lebih lama dapat menampilkan alamat yang dikanonikalisasi (misalnya, First.Last@example.com untuk first.last@example.com). Gunakan perbandingan yang tidak peka huruf besar/kecil saat mencocokkan klaim email token dengan nilai formulir yang dikirimkan.