تنفيذ مقدّم خدمة البريد الإلكتروني (جهة الإصدار)

للحصول على تفاصيل إضافية، يمكنك الاطّلاع على الرمز التجريبي لموفّر البريد الإلكتروني الوهمي والرجوع إلى خطوات الجهة المصدرة في اقتراحي واجهة برمجة التطبيقات الخاصة بميزة "إثبات ملكية عنوان البريد الإلكتروني" وبروتوكول ميزة "إثبات ملكية عنوان البريد الإلكتروني".

بصفتك جهة إصدار، لن تحتاج إلى الاشتراك في مرحلة التجربة والتقييم أو تقديم رمز مميّز، لأنّ الموقع الإلكتروني الخاص بالجهة المعتمِدة هو الذي يؤدي إلى تشغيل سلوك المتصفّح. تأكَّد من إعداد نقاط النهاية للردّ على هذه الطلبات.

ضبط ميزة "اكتشاف جهة الإصدار"

للسماح للمتصفّحات باكتشاف نقاط نهاية التحقّق تلقائيًا عند اختيار عنوان بريد إلكتروني تابع لنطاقك، يجب عرض إعداداتك باستخدام نظام أسماء النطاقات ونقطة نهاية HTTP .well-known.

ضبط سجلّ تفويض نظام أسماء النطاقات

اضبط سجلّ TXT لنظام أسماء النطاقات على نطاق بريدك الإلكتروني الذي يفوّض سلطة التحقّق إلى معرّف الجهة المُصدرة. يمكن أن تستخدم هذه المعرّفات النطاق نفسه حسب البنية الأساسية.

تنسيق السجلّ: _email-verification.<email-domain>

مثال على ملف المنطقة:

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

استضافة نقطة نهاية .well-known/email-verification

استضافة ملف بيانات وصفية بتنسيق JSON على نطاق الجهة المصدرة ضمن المسار /.well-known/ يوضّح هذا الملف إمكانات الإصدار وخوارزميات التوقيع المشفر التي تتوافق مع بنيتك الأساسية.

نقطة النهاية: https://<issuer-domain>/.well-known/email-verification

مثال على الردّ:

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

استضافة نقطة نهاية .well-known/web-identity

ربما سبق لك تنفيذ مصدر .well-known JSON إضافي كجزء من Federated Credentials (FedCM) API. يوفر هذا المرجع روابط إلى نقطة نهاية حساباتك وعنوان URL لتسجيل الدخول.

نقطة النهاية: https://<domain>/.well-known/web-identity

مثال على الردّ:

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

استخدام نقطة نهاية للحسابات

تقدّم نقطة نهاية الحسابات من واجهة برمجة التطبيقات FedCM قائمة بالحسابات التي تم تسجيل الدخول إليها في الوقت الحالي. يعرض المثال التالي ردًا بسيطًا. لمزيد من التفاصيل، يُرجى الرجوع إلى دليل تنفيذ موفِّر الهوية.

نقطة النهاية: كما هو محدّد في .well-known/web-identity

في ما يلي مثال على الرد:

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

تحقيق التكامل مع Login Status API

يجب أن تكون لدى المستخدم جلسة نشطة مع مقدّم الخدمة، ويجب أن تشير إلى ذلك للمتصفّح باستخدام Login Status API.

عندما يسجّل المستخدم دخوله أو خروجه بنجاح، اعرض عنوان استجابة HTTP المطابق:

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

بدلاً من ذلك، يمكنك تعديل الحالة باستخدام JavaScript في سياق تطبيق الويب الخاص بك:

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

التعامل مع طلبات الإصدار

يتلقّى issuance_endpoint طلب application/json POST يتضمّن المفتاح email وعناوين توقيعات رسائل HTTP الخاصة بـ Signature وSignature-Input وSignature-Key.

استخدِم مكتبة تتوافق مع العناوين المنظَّمة وتوقيعات رسائل HTTP في بيئتك. في Node.js، يمكنك استخدام structured-headers و http-message-sig.

تنسيق الطلب الكامل:

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

تحليل الطلب والتحقّق من صحته:

  • مصادقة الجلسة: تأكَّد من صحة ملفات تعريف الارتباط الخاصة بالجلسة التابعة للطرف الأول والتي تم إرسالها مع الطلب. يجب أن تتم مصادقة المستخدم.
  • عنوان Sec-Fetch-Dest: اضبطه على email-verification.
  • توقيعات رسائل HTTP: تحقَّق من توقيع الطلب باستخدام المفتاح العام المؤقت في Signature-Key وتحقَّق من صحة Content-Digest.
  • الحِمل: يحتوي نص JSON على السلسلة email المطلوبة للتحقّق.

ردّ بشأن إصدار الشهادة

بعد التحقّق بنجاح من صحة الرمز المميّز للجلسة ورمز الطلب، أنشئ رمز JWT للتصريح الانتقائي موقّعًا يتم عرضه بتنسيق JSON باستخدام المكتبات المناسبة لمنصتك. على سبيل المثال، يمكنك استخدام @sd-jwt/core و jose مع Node.

يجب أن يبدو تنسيق الحمولة الأولية على النحو التالي:

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

أنشئ الرمز المميّز ووقِّعه وأرجِعه:

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

يبدو نص الاستجابة الناتج مشابهًا لما يلي:

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

بعد إنشاء رمز مميّز لتأكيد عنوان البريد الإلكتروني وتوقيعه، يرسل المتصفّح هذا الرمز إلى الموقع الإلكتروني الذي يتحقّق من صحته.

تحديد المشاكل وحلّها

إذا لم يتواصل المتصفّح مع نقاط النهاية أو رفض الرموز المميزة الصادرة، تحقَّق من المشاكل الشائعة التالية:

لا يستدعي المتصفّح أبدًا accounts_endpoint أو issuance_endpoint

  • لم يتم ضبط حالة تسجيل الدخول: لا يطلب Chrome بيانات من نقاط النهاية إلا إذا كان يعرف أنّ المستخدم مسجّل الدخول. تأكَّد من أنّ عملية تسجيل الدخول تضبط عنوان HTTP الذي يتضمّن Set-Login: logged-in أو تستدعي navigator.login.setStatus("logged-in").
  • حظر ملفات تعريف الارتباط الخاصة بالجلسة (SameSite=None): يسترد المتصفّح accounts_endpoint وissuance_endpoint من جهة معتمدة على موقع إلكتروني مختلف. بما أنّ هذه الطلبات يتم إرسالها من مواقع إلكترونية مختلفة، يجب أن تتضمّن ملف تعريف الارتباط للجلسة SameSite=None; Secure. يعمل ملف تعريف الارتباط SameSite=Lax عند الاختبار على أداة التحقّق من الموقع الإلكتروني نفسه، ولكن يتم حذفه في الطلبات المُرسَلة من مواقع إلكترونية مختلفة.
  • تعذُّر العثور على الحساب أو عدم تطابقه: تأكَّد من أنّ _email-verification.<email-domain> يعرض سجلّ TXT واحدًا (iss=<issuer-domain>، بدون https://)، وأنّ كلتا نقطتَي النهاية .well-known تعرضان Content-Type: application/json، وأنّ استجابة accounts_endpoint تتضمّن حسابًا يتطابق email الخاص به مع العنوان الذي تم إدخاله.

تعذُّر التحقّق من صحة طلب الإصدار

  • تهجئة عنوان Sec-Fetch-Dest: يرسل الإصدار 154 من Chrome والإصدارات الأحدث Sec-Fetch-Dest: email-verification (مع شرطة)، بينما أرسل الإصدار 153 من Chrome emailverification (بدون شرطة). يجب قبول كلتا القيمتين أثناء طرح التغيير.
  • عدم تطابق توقيع رسالة HTTP‏ (@authority) خلف خادم وكيل: عند التحقّق من توقيع RFC 9421، يعكس المكوّن @authority المضيف المتاح للجميع. إذا كان الخادم يقع خلف خادم وكيل عكسي أو موازن تحميل، أعِد إنشاء عنوان URL لتأكيد رقم الهاتف باستخدام X-Forwarded-Host (أو المصدر العام) بدلاً من اسم المضيف الداخلي، واحتسِب Content-Digest على وحدات البايت الخاصة بنص الطلب الأولي قبل تحليل JSON.

يرفض المتصفّح issuance_token الذي تم إرجاعه

  • مطالبة email معدَّلة أو أساسية: بدءًا من الإصدار 156 من Chrome، يتحقّق المتصفّح من أنّ مطالبة EVT email تتطابق مع email المطلوبة بايت ببايت. إذا كان الخلفية الأساسية تعمل على تسوية العنوان إلى تنسيق حساب أساسي (مثل عرض First.Last@example.com عند طلب first.LAST@example.com)، سيتجاهل Chrome الرمز المميز. مطابقة الطلب مع حساب المستخدم، ولكن عرض سلسلة email نفسها التي تم تلقّيها في نص الطلب
  • عدم تضمين علامة المدّ الأخيرة (~): حتى في حال عدم تضمين أي معلومات مفصح عنها، يجب أن يكون issuance_token صالحًا بتنسيق SD-JWT وينتهي بعلامة مدّ أخيرة (<Issuer-signed-JWT>~) ليتمكّن المتصفّح من إلحاق <KB-JWT>.
  • عدم تطابق iss أو cnf.jwk: تأكَّد من أنّ مطالبة EVT iss هي مصدر HTTPS مطابق تمامًا (https://<issuer-domain>، بدون شرطة مائلة لاحقة) للبيانات الوصفية .well-known/email-verification، وأنّ cnf.jwk يتضمّن المفتاح العام المؤقت للمتصفّح من العنوان Signature-Key.