Implementasi penyedia email (penerbit)

Untuk mengetahui detail tambahan, Anda dapat mempelajari kode demo penyedia email tiruan dan melihat langkah-langkah penerbit dalam proposal Email Verification API dan Email Verification Protocol.

Sebagai penerbit, Anda tidak perlu mendaftar ke uji coba origin atau memberikan token karena situs pihak tepercaya memicu perilaku browser. Pastikan endpoint Anda dikonfigurasi untuk merespons permintaan tersebut.

Mengonfigurasi penemuan penerbit

Agar browser dapat otomatis menemukan endpoint verifikasi Anda saat alamat email yang termasuk dalam domain Anda dipilih, ekspos konfigurasi Anda menggunakan DNS dan endpoint HTTP .well-known.

Mengonfigurasi data delegasi DNS

Konfigurasi data TXT DNS di domain email Anda yang mendelegasikan otoritas verifikasi ke ID penerbit Anda. ID ini dapat menggunakan domain yang sama, bergantung pada infrastruktur Anda.

Format Data: _email-verification.<email-domain>

Contoh File Zona:

_email-verification.example.com IN TXT "iss=accounts.issuer.example"

Menghosting endpoint .well-known/email-verification

Menghosting file metadata JSON di domain penerbit Anda di jalur /.well-known/. File ini menguraikan kemampuan penerbitan Anda dan algoritma penandatanganan kriptografi yang didukung infrastruktur Anda.

Endpoint: https://<issuer-domain>/.well-known/email-verification

Contoh respons:

{
  "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"]
}

Menghosting endpoint .well-known/web-identity

Anda mungkin telah menerapkan resource JSON .well-known tambahan sebagai bagian dari Federated Credentials (FedCM) API. Resource ini menyediakan link ke endpoint akun dan URL login Anda.

Endpoint: https://<domain>/.well-known/web-identity

Contoh respons:

{
  "accounts_endpoint": "https://accounts.issuer.example/accounts",
  "login_url": "https://accounts.issuer.example/login"
}

Menggunakan endpoint akun

Endpoint akun dari FedCM API menyediakan daftar akun yang login saat ini. Contoh berikut menunjukkan respons minimal. Untuk mengetahui detail selengkapnya, lihat panduan penerapan penyedia identitas.

Endpoint: seperti yang ditentukan dalam .well-known/web-identity

Berikut adalah contoh respons:

{
  "accounts": [
    {
      "id": "demo-example",
      "name": "Demo User",
      "email": "demo@example.com",
      "given_name": "Demo"
    }
  ]
}

Berintegrasi dengan Login Status API

Pengguna harus memiliki sesi aktif dengan penyedia dan Anda harus memberi sinyal tersebut ke browser menggunakan Login Status API.

Saat pengguna berhasil login atau logout, sajikan header respons HTTP yang cocok:

Set-Login: logged-in
Set-Login: logged-out

Atau, perbarui status menggunakan JavaScript dalam konteks aplikasi web Anda:

navigator.login.setStatus("logged-in");
navigator.login.setStatus("logged-out");

Menangani permintaan penerbitan

issuance_endpoint Anda menerima permintaan POST application/json yang berisi kunci email dan header Tanda Tangan Pesan HTTP untuk Signature, Signature-Input, dan Signature-Key.

Gunakan library yang mendukung header terstruktur dan tanda tangan Pesan HTTP untuk lingkungan Anda. Di Node.js, Anda dapat menggunakan structured-headers dan http-message-sig.

Format permintaan lengkap:

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"}

Mengurai dan memvalidasi permintaan:

  • Autentikasi sesi: Validasi cookie sesi pihak pertama yang dikirim dengan permintaan. Pengguna harus diautentikasi.
  • Header Sec-Fetch-Dest: Tetapkan ke email-verification.
  • Tanda Tangan Pesan HTTP: Verifikasi tanda tangan permintaan menggunakan kunci publik sementara di Signature-Key dan validasi Content-Digest.
  • Payload: Isi JSON berisi string email yang diminta untuk verifikasi.

Respons penerbitan

Setelah validasi sesi dan token permintaan berhasil, buat JWT Pengungkapan Selektif (SD-JWT) bertanda tangan yang ditampilkan sebagai JSON menggunakan library yang sesuai untuk platform Anda. Misalnya, untuk Node, Anda dapat menggunakan @sd-jwt/core dan jose.

Format payload mentah akan terlihat seperti:

{
  "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
}

Buat, tandatangani, dan tampilkan token:

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);

Isi respons yang dihasilkan akan terlihat seperti:

{
  "issuance_token": "tOkEn123tOkEn123tOkEn123...~"
}

Setelah Anda membuat dan menandatangani token verifikasi email, browser akan meneruskan token ini ke situs verifikasi untuk divalidasi.

Pemecahan masalah

Jika browser tidak menghubungi endpoint Anda atau menolak token yang dikeluarkan, periksa masalah umum berikut:

Browser tidak pernah memanggil accounts_endpoint atau issuance_endpoint

  • Status Login tidak ditetapkan: Chrome hanya mengkueri endpoint Anda jika mengetahui bahwa pengguna login. Pastikan alur login Anda menetapkan header HTTP Set-Login: logged-in atau memanggil navigator.login.setStatus("logged-in").
  • Cookie sesi diblokir (SameSite=None): Browser mengambil accounts_endpoint dan issuance_endpoint dari pihak tepercaya di situs yang berbeda. Karena ini adalah permintaan lintas situs, cookie sesi Anda harus menyertakan SameSite=None; Secure. Cookie SameSite=Lax berfungsi saat pengujian di verifier situs yang sama, tetapi dihilangkan pada permintaan lintas situs.
  • Ketidakcocokan penemuan atau akun: Verifikasi bahwa _email-verification.<email-domain> menampilkan satu catatan TXT (iss=<issuer-domain>, tanpa https://), kedua endpoint .well-known menampilkan Content-Type: application/json, dan respons accounts_endpoint mencakup akun yang email-nya cocok dengan alamat yang dimasukkan.

Validasi permintaan penerbitan gagal

  • Ejaan header Sec-Fetch-Dest: Chrome 154+ mengirim Sec-Fetch-Dest: email-verification (dengan tanda hubung), sedangkan Chrome 153 mengirim emailverification (tanpa tanda hubung). Terima kedua nilai selama peluncuran.
  • Ketidakcocokan Tanda Tangan Pesan HTTP (@authority) di balik proxy: Saat memverifikasi tanda tangan RFC 9421, komponen @authority mencerminkan host yang menghadap publik. Jika server Anda berada di belakang reverse proxy atau load balancer, rekonstruksi URL verifikasi menggunakan X-Forwarded-Host (atau origin publik Anda) dan bukan nama host internal, lalu hitung Content-Digest melalui byte isi permintaan mentah sebelum parsing JSON.

Browser menolak issuance_token yang ditampilkan

  • Klaim email yang diubah atau dikanonisasi: Mulai Chrome 156, browser memeriksa apakah klaim email EVT cocok dengan email yang diminta byte demi byte. Jika backend Anda menormalisasi alamat ke format akun kanonis (seperti menampilkan First.Last@example.com saat first.LAST@example.com diminta), Chrome akan menghapus token. Mencocokkan permintaan dengan akun pengguna, tetapi menampilkan string email yang diterima persis di isi permintaan.
  • Tidak ada tilde di akhir (~): Meskipun tidak ada pengungkapan, issuance_token harus berupa SD-JWT yang valid dan diakhiri dengan tilde (<Issuer-signed-JWT>~) agar browser dapat menambahkan <KB-JWT>.
  • iss atau cnf.jwk tidak cocok: Pastikan klaim iss EVT adalah asal HTTPS yang sama persis (https://<issuer-domain>, tanpa garis miring di akhir) yang cocok dengan metadata .well-known/email-verification Anda, dan cnf.jwk menyematkan kunci publik sementara browser dari header Signature-Key.