Triển khai nhà cung cấp dịch vụ email (tổ chức phát hành)

Để biết thêm thông tin chi tiết, bạn có thể xem mã minh hoạ nhà cung cấp email mô phỏng và tham khảo các bước của tổ chức phát hành trong đề xuất API Xác minh email và Giao thức Xác minh email.

Là một đơn vị phát hành, bạn không cần đăng ký dùng thử nguồn gốc hoặc cung cấp mã thông báo vì trang web của bên phụ thuộc sẽ kích hoạt hành vi của trình duyệt. Đảm bảo rằng các điểm cuối của bạn được định cấu hình để phản hồi những yêu cầu đó.

Định cấu hình tính năng khám phá tổ chức phát hành

Để cho phép trình duyệt tự động phát hiện các điểm cuối xác minh khi một địa chỉ email thuộc miền của bạn được chọn, hãy hiển thị cấu hình của bạn bằng cách sử dụng DNS và điểm cuối HTTP .well-known.

Định cấu hình bản ghi uỷ quyền DNS

Định cấu hình bản ghi TXT DNS trên miền email của bạn để uỷ quyền xác minh cho mã nhận dạng tổ chức phát hành. Các giá trị nhận dạng này có thể sử dụng cùng một miền, tuỳ thuộc vào cơ sở hạ tầng của bạn.

Định dạng bản ghi: _email-verification.<email-domain>

Ví dụ về tệp vùng:

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

Lưu trữ một điểm cuối .well-known/email-verification

Lưu trữ tệp siêu dữ liệu JSON trên miền của đơn vị phát hành theo đường dẫn /.well-known/. Tệp này trình bày các chức năng phát hành và thuật toán ký mã hoá mà cơ sở hạ tầng của bạn hỗ trợ.

Điểm cuối: https://<issuer-domain>/.well-known/email-verification

Ví dụ về câu trả lời:

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

Lưu trữ một điểm cuối .well-known/web-identity

Có thể bạn đã triển khai một tài nguyên JSON .well-known bổ sung trong Federated Credentials (FedCM) API. Tài nguyên này cung cấp các đường liên kết đến điểm cuối tài khoản và URL đăng nhập của bạn.

Điểm cuối: https://<domain>/.well-known/web-identity

Ví dụ về câu trả lời:

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

Sử dụng điểm cuối tài khoản

Điểm cuối tài khoản của FedCM API hiện cung cấp danh sách các tài khoản đã đăng nhập. Ví dụ sau đây cho thấy một phản hồi tối thiểu. Để biết thêm thông tin chi tiết, hãy tham khảo hướng dẫn triển khai trình nhận dạng.

Điểm cuối: như được chỉ định trong .well-known/web-identity

Sau đây là một ví dụ về phản hồi:

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

Tích hợp với Login Status API

Người dùng phải có một phiên hoạt động với nhà cung cấp và bạn phải báo hiệu điều đó cho trình duyệt bằng Login Status API (API Trạng thái đăng nhập).

Khi người dùng đăng nhập hoặc đăng xuất thành công, hãy phân phát tiêu đề phản hồi HTTP phù hợp:

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

Ngoài ra, hãy cập nhật trạng thái bằng JavaScript trong ngữ cảnh ứng dụng web của bạn:

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

Xử lý yêu cầu phát hành

issuance_endpoint của bạn nhận được một yêu cầu POST application/json có chứa khoá email và tiêu đề Chữ ký thông báo HTTP cho Signature, Signature-Input và Signature-Key.

Sử dụng một thư viện hỗ trợ tiêu đề có cấu trúc và Chữ ký thông báo HTTP cho môi trường của bạn. Trong Node.js, bạn có thể dùng structured-headers và http-message-sig.

Định dạng yêu cầu đầy đủ:

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

Phân tích cú pháp và xác thực yêu cầu:

  • Xác thực phiên: Xác thực cookie phiên của bên thứ nhất được gửi cùng với yêu cầu. Người dùng phải được xác thực.
  • Tiêu đề Sec-Fetch-Dest: Đặt thành email-verification.
  • Chữ ký thông báo HTTP: Xác minh chữ ký yêu cầu bằng khoá công khai tạm thời trong Signature-Key và xác thực Content-Digest.
  • Tải trọng: Phần nội dung JSON chứa chuỗi email được yêu cầu để xác minh.

Phản hồi về việc phát hành

Sau khi xác thực thành công mã thông báo phiên và yêu cầu, hãy tạo một JWT Tiết lộ có chọn lọc (SD-JWT) đã ký được trả về dưới dạng JSON bằng cách sử dụng các thư viện phù hợp cho nền tảng của bạn. Ví dụ: đối với Node, bạn có thể sử dụng @sd-jwt/core và jose.

Định dạng tải trọng thô sẽ có dạng như sau:

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

Tạo, ký và trả về mã thông báo:

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

Phần nội dung phản hồi thu được sẽ có dạng như sau:

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

Sau khi bạn tạo và ký mã thông báo xác minh email, trình duyệt sẽ chuyển mã thông báo này đến trang web xác minh để xác thực.

Khắc phục sự cố

Nếu trình duyệt không liên hệ với các điểm cuối của bạn hoặc từ chối mã thông báo đã phát hành, hãy kiểm tra các vấn đề thường gặp sau:

Trình duyệt không bao giờ gọi accounts_endpoint hoặc issuance_endpoint

  • Chưa đặt trạng thái đăng nhập: Chrome chỉ truy vấn các điểm cuối của bạn nếu biết người dùng đã đăng nhập. Đảm bảo quy trình đăng nhập của bạn đặt tiêu đề HTTP Set-Login: logged-in hoặc gọi navigator.login.setStatus("logged-in").
  • Cookie phiên bị chặn (SameSite=None): Trình duyệt tìm nạp accounts_endpoint và issuance_endpoint của bạn từ một bên đáng tin cậy trên một trang web khác. Vì đây là các yêu cầu trên nhiều trang web, nên cookie của phiên của bạn phải bao gồm SameSite=None; Secure. Cookie SameSite=Lax hoạt động khi kiểm thử trên trình xác minh cùng trang web, nhưng bị bỏ qua trên các yêu cầu trên nhiều trang web.
  • Phát hiện hoặc tài khoản không khớp: Xác minh rằng _email-verification.<email-domain> trả về một bản ghi TXT duy nhất (iss=<issuer-domain>, không có https://), cả hai điểm cuối .well-known đều trả về Content-Type: application/json và phản hồi accounts_endpoint bao gồm một tài khoản có email khớp với địa chỉ đã nhập.

Không xác thực được yêu cầu phát hành

  • Lỗi chính tả tiêu đề Sec-Fetch-Dest: Chrome 154 trở lên gửi Sec-Fetch-Dest: email-verification (có dấu gạch ngang), trong khi Chrome 153 gửi emailverification (không có dấu gạch ngang). Chấp nhận cả hai giá trị trong quá trình triển khai.
  • Chữ ký thông báo HTTP (@authority) không khớp sau một proxy: Khi xác minh chữ ký RFC 9421, thành phần @authority sẽ phản ánh máy chủ công khai. Nếu máy chủ của bạn nằm sau một proxy đảo ngược hoặc bộ cân bằng tải, hãy tạo lại URL xác minh bằng X-Forwarded-Host (hoặc nguồn gốc công khai của bạn) thay vì tên máy chủ nội bộ và tính toán Content-Digest trên các byte của nội dung yêu cầu thô trước khi phân tích cú pháp JSON.

Trình duyệt từ chối issuance_token được trả về

  • Yêu cầu email đã được sửa đổi hoặc chuẩn hoá: Kể từ Chrome 156, trình duyệt sẽ kiểm tra để đảm bảo yêu cầu email EVT khớp với yêu cầu email từng byte. Nếu phần phụ trợ của bạn chuẩn hoá địa chỉ theo định dạng tài khoản chuẩn (chẳng hạn như trả về First.Last@example.com khi first.LAST@example.com được yêu cầu), thì Chrome sẽ loại bỏ mã thông báo. So khớp yêu cầu với tài khoản của người dùng, nhưng trả về chính xác chuỗi email nhận được trong nội dung yêu cầu.
  • Thiếu dấu ngã ở cuối (~): Ngay cả khi không có thông tin nào được tiết lộ, issuance_token vẫn phải là một SD-JWT hợp lệ kết thúc bằng dấu ngã ở cuối (<Issuer-signed-JWT>~) để trình duyệt có thể thêm <KB-JWT>.
  • iss hoặc cnf.jwk không khớp: Đảm bảo rằng yêu cầu iss EVT là nguồn HTTPS chính xác (https://<issuer-domain>, không có dấu gạch chéo ở cuối) khớp với siêu dữ liệu .well-known/email-verification của bạn và cnf.jwk nhúng khoá công khai tạm thời của trình duyệt từ tiêu đề Signature-Key.