عند جمع عنوان بريد إلكتروني أثناء الاشتراك أو تسجيل الدخول أو الدفع أو استرداد الحساب، يتطلّب إثبات الملكية عادةً إرسال رمز مرور صالح لمرة واحدة (OTP) أو رابط سحري خارج النطاق. يؤدي ذلك إلى إجبار المستخدمين على مغادرة صفحتك وانتظار تسليم الرسالة الإلكترونية ونسخ الرموز، ما يؤدي إلى حدوث احتكاك وانقطاع وخطر التصيّد الاحتيالي، مع الكشف أيضًا لمزوِّد خدمة البريد الإلكتروني عن الجهة المعتمدة التي يزورها المستخدم.
تتيح واجهة برمجة التطبيقات لتأكيد عنوان البريد الإلكتروني (إضافة HTML من جهة العميل) وبروتوكول تأكيد عنوان البريد الإلكتروني (EVP) (بروتوكول تشفير في الخلفية) للجهة المعتمدة التأكّد من أنّ المستخدم يتحكّم في عنوان بريد إلكتروني بدون إرسال رسالة تأكيد. يتوسّط المتصفّح بين الطرف المعتمِد ومزوِّد خدمة البريد الإلكتروني للمستخدم (جهة الإصدار):
- تتضمّن الجهة المعتمدة
<input>مخفيًا معautocomplete="email-verification-token"وnonceتشفيرًا للاستخدام الواحد في<form>نفسه مثل إدخال البريد الإلكتروني. - عندما يختار المستخدم عنوان بريد إلكتروني أو يُدخله، يبحث المتصفّح عن سجلّ
_email-verification.<domain>نظام أسماء النطاقاتTXTللنطاق، ويتحقّق من أنّ المستخدم لديه جلسة نشطة مع جهة الإصدار الموثوقة، ويطلب رمزًا مميزًا لتأكيد عنوان البريد الإلكتروني (EVT) بدون الإفصاح عن هوية الجهة المعتمدة لجهة الإصدار. - يربط المتصفّح رمز EVT بمصدر الطرف المعتمِد وجلسة
nonceفي رمز JWT لربط المفاتيح (KB-JWT) الموقَّع باستخدام مفتاحَي تشفير متصفّح مؤقت (EVT + KB-JWT)، ويملأ حقل الإدخال المخفي، ويرسل النموذج. - تتحقّق الواجهة الخلفية للجهة المعتمدة من سلسلة التشفير، وإذا كانت صالحة، يتم تخطّي خطوة كلمة المرور الصالحة لمرة واحدة (OTP) أو الرابط السحري.
1. تفعيل مرحلة التجربة والتقييم وعرض نموذج إثبات الهوية
للمشاركة كموقع إلكتروني يتحقّق من الهوية (الجهة المعتمدة):
- قدِّم الرمز المميّز لمرحلة التجربة والتقييم في الصفحة التي تستضيف النموذج باستخدام عنوان الاستجابة
Origin-TrialHTTP أو عنصر<meta http-equiv="origin-trial">.- مراحل التجربة والتقييم التابعة لجهات خارجية: يمكن لحِزم تطوير البرامج (SDK) المضمّنة للهوية إدخال رمز مميّز تجريبي تابع لجهة خارجية، شرط أن يتطابق مصدر المسجِّل في مرحلة التجربة والتقييم مع نطاق جهة الإصدار (الموقع الإلكتروني نفسه مع جهة الإصدار، مثل
https://issuer.example).
- مراحل التجربة والتقييم التابعة لجهات خارجية: يمكن لحِزم تطوير البرامج (SDK) المضمّنة للهوية إدخال رمز مميّز تجريبي تابع لجهة خارجية، شرط أن يتطابق مصدر المسجِّل في مرحلة التجربة والتقييم مع نطاق جهة الإصدار (الموقع الإلكتروني نفسه مع جهة الإصدار، مثل
- أنشِئ رقمًا خاصًا عشوائيًا مشفّرًا لكل عرض نموذج باستخدام 128 بت على الأقل من الإنتروبيا (على سبيل المثال،
randomBytes(32).toString('base64url')منnode:crypto)، وخزِّنه في حالة الجلسة من جهة الخادم التي تم إدخالها بواسطة ملف تعريف ارتباط معرّف جلسةHttpOnlyأوSecureأوSameSite=Lax(لا تخزِّن أبدًا قيمةnonceالمتوقعة مباشرةً داخل ملف تعريف ارتباط العميل حيث يمكن للمهاجم إعادة تشغيل كل من ملف تعريف الارتباط والرمز المميز)، واعرضه في سمةnonceلإدخال الرمز المميز المخفي. اعرض الصفحة باستخدامCache-Control: no-storeكي لا يعيد التنقّل للخلف أو للأمام استخدام رمز nonce سبق استخدامه. - أضِف حقل إدخال الرمز المميّز المخفي (
autocomplete="email-verification-token") داخل علامة<form>نفسها التي تحتوي على حقل إدخال البريد الإلكتروني (autocomplete="email").
<!-- Serve with:
Origin-Trial: <YOUR_ORIGIN_TRIAL_TOKEN>
Cache-Control: no-store -->
<form method="POST" action="/api/verify-email">
<label for="email">Email address:</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required
/>
<!-- MANDATORY: Must be in the same <form> as the email input.
The nonce value must be freshly generated on the server per form render. -->
<input
type="hidden"
name="token"
nonce="SERVER_GENERATED_PER_FORM_NONCE"
autocomplete="email-verification-token"
/>
<button type="submit">Continue</button>
</form>
2. التحقّق من الرمز المميّز المُرسَل على الخادم
عند إرسال النموذج، يجب التعامل مع حقل token كإدخال غير موثوق به. قراءة nonce المتوقّع وحذفه تلقائيًا من مخزن الجلسات من جهة الخادم عند الاستلام إذا كان الحقل token فارغًا (على سبيل المثال، في المتصفّحات التي لا تتيح استخدام EVP أو عندما يسجّل المستخدم الخروج من مزوِّد خدمة البريد الإلكتروني) أو إذا تعذّر إثبات الهوية، يمكنك العودة فورًا إلى مسار كلمة المرور العادية الصالحة لمرة واحدة (OTP) أو الرابط السحري:
// Replace with your public relying-party origin
const RP_ORIGIN = 'https://rp.example.com';
export async function handleSignupSubmission(request) {
const formData = await request.formData();
const submittedEmail = String(formData.get('email') || '').trim();
const rawToken = String(formData.get('token') || '').trim();
// Atomically read and delete the single-use nonce from server-side session state
const expectedNonce = await consumeSessionNonce(request);
if (rawToken && expectedNonce) {
try {
await verifyEmailVerificationToken({
rawToken,
submittedEmail,
expectedNonce,
expectedAudience: RP_ORIGIN,
});
// Fast path: email ownership is cryptographically verified; skip OTP email.
return completeAccountCreation(submittedEmail);
} catch (err) {
// Log token verification failures before falling back to standard email verification.
console.error('Email verification token check failed:', err);
}
}
// Fallback path: send standard OTP code or magic link email to submittedEmail.
return sendTraditionalVerificationEmail(submittedEmail);
}
استخدِم دائمًا مكتبات SD-JWT وJOSE/JWT المعتمدة لمنصة الخلفية (مثل @sd-jwt/core وjose في Node.js أو sd-jwt-python / jwcrypto في Python أو sd-jwt-java / Nimbus JOSE+JWT على جهاز Java الافتراضي) لتنفيذ verifyEmailVerificationToken بدلاً من استخدام رمز مخصّص لتحليل JWT أو SD-JWT.
الخطوة 1: تحليل مطالبات SD-JWT والتحقّق من عنوان البريد الإلكتروني
بعد استهلاك الرمز expectedNonce للاستخدام الواحد من مساحة تخزين الجلسة من جهة الخادم، عليك فك ترميز عرض SD-JWT (EVT + KB-JWT) والتحقّق مما يلي:
- يتوفّر كلّ من رمز JWT الخاص بجهة الإصدار (
jwt) ورمز JWT لربط المفتاح (kbJwt)، بالإضافة إلى مطالبة جهة الإصدار (iss). evtPayload.email_verified === true.- يتطابق
evtPayload.emailمع عنوان البريد الإلكتروني في النموذج الذي تم إرساله باستخدام مقارنة غير حساسة لحالة الأحرف (toLowerCase()).
import { createHash } from 'node:crypto';
import { decodeSdJwtSync } from '@sd-jwt/core';
const hasher = (data, alg) =>
createHash(alg === 'sha-256' ? 'sha256' : alg).update(data).digest();
const decoded = decodeSdJwtSync(rawToken, hasher);
const evtPayload = decoded.jwt.payload;
const iss = evtPayload.iss;
if (!expectedNonce || !decoded.kbJwt || !iss || evtPayload.email_verified !== true) {
throw new Error('Missing nonce, KB-JWT, issuer, or email_verified !== true.');
}
// MANDATORY: Compare email addresses case-insensitively
if (evtPayload.email?.toLowerCase() !== submittedEmail.toLowerCase()) {
throw new Error('Submitted email does not match the EVT email claim.');
}
الخطوة 2: التحقّق من صحة تفويض سجلّ TXT في نظام أسماء النطاقات وتحميل JWKS لجهة الإصدار
قبل إرسال أي طلب شبكة إلى iss، يجب التأكّد من أنّ نطاق البريد الإلكتروني المُرسَل يفوّض جهة الإصدار هذه تحديدًا من خلال سجلّ TXT لنظام أسماء النطاقات _email-verification.<domain> (iss=<issuer-host> -> https://<issuer-host> بدون منفذ أو مسار أو شرطة مائلة لاحقة). عندما يتطابق تفويض نظام أسماء النطاقات مع iss بايت ببايت، يتم جلب ${iss}/.well-known/email-verification عبر HTTPS، والتأكّد من أنّ metadata.issuer === iss، وتحميل مجموعة مفاتيح الويب JSON (JWKS) الخاصة بجهة الإصدار.
ملاحظة حول معالجة
kid: يحذف بعض مزوّدي خدمة البريد الإلكتروني الرئيسيين (مثل Gmail) السمةkidمن عنوان EVT. عندما لا يتوفّرkidويحتوي JWKS على مفاتيح متعددة، يعرضjoseالخطأERR_JWKS_MULTIPLE_MATCHING_KEYS(AsyncIterableمنCryptoKeyالمطابقة). يمكنك تكرارfor await (const publicKey of err)للتحقّق من كل مفتاح تجريبيًا، كما هو موضّح في الخطوة 3.
import dns from 'node:dns/promises';
import { createRemoteJWKSet } from 'jose';
const domain = submittedEmail.split('@')[1];
const txtRecords = (await dns.resolveTxt(`_email-verification.${domain}`)).map((r) => r.join(''));
if (!txtRecords.some((txt) => txt.startsWith('iss=') && `https://${txt.slice(4).trim()}` === iss)) {
throw new Error(`Issuer "${iss}" is not delegated via DNS for "${domain}".`);
}
const discoveryUrl = new URL('/.well-known/email-verification', iss);
if (discoveryUrl.protocol !== 'https:') throw new Error('Issuer must use HTTPS.');
const metadata = await (await fetch(discoveryUrl)).json();
if (metadata.issuer !== iss || !metadata.jwks_uri?.startsWith('https://')) {
throw new Error('Invalid issuer metadata or non-HTTPS jwks_uri.');
}
const issuerJwks = createRemoteJWKSet(new URL(metadata.jwks_uri));
الخطوة 3: التحقّق من توقيع EVT ورمز JWT لربط المفتاح (KB-JWT)
أخيرًا، تحقَّق من كلا التوقيعَين والربط المشفَّر بين الرمزَين المميّزَين باستخدام القائمة المسموح بها الصريحة للخوارزمية غير المتماثلة (['Ed25519', 'EdDSA', 'ES256']):
- توقيع EVT (
verifier): يتم التحقّق منه من خلالissuerJwks(يتم رصدERR_JWKS_MULTIPLE_MATCHING_KEYSلتكرار مفاتيح المرشحين عند حذفkid)، مع فرضissونافذة تحديث ضيقة (maxTokenAge: '5m'وclockTolerance: '1m'). - ربط الحامل (الخياران
kbVerifierوsdJwt.verify): استخرِج المفتاح العام المؤقت للمتصفّح منevtPayload.cnf.jwk، وتأكَّد من أنّ كلّ منcnf.jwk.algوdecoded.kbJwt.header.algفيALLOWED_ALGSومتطابقان (مع التعامل مع'Ed25519'و'EdDSA'على أنّهما متكافئان أثناء انتقال عمليات تنفيذ "مرحلة التجربة والتقييم" إلى RFC 9864'Ed25519')، وتحقَّق من توقيعKB-JWTباستخدامtyp: 'kb+jwt'. يجب تمريرkeyBindingNonce: expectedNonceوexpectedKeyBindingAudience: expectedAudienceوkeyBindingMaxAgeSeconds: 300إلىsdJwt.verify()، لأنّ@sd-jwt/coreيستدعيkbVerifierفقط ويتحقّق من صحةnonceوaudوiatوsd_hashعند توفيرkeyBindingNonce.
import { SDJwtInstance } from '@sd-jwt/core';
import { importJWK, jwtVerify } from 'jose';
// IETF draft-hardt-email-verification-02 (§3.3, §5.1.1) specifies RFC 9864 'Ed25519' and 'ES256'.
// TODO: Remove 'EdDSA' once live issuers (e.g. Gmail) and Chrome Origin Trial builds finish
// transitioning from 'EdDSA' (with crv: 'Ed25519') to 'Ed25519'.
const ALLOWED_ALGS = ['Ed25519', 'EdDSA', 'ES256'];
const normalizeAlg = (alg) => (alg === 'Ed25519' ? 'EdDSA' : alg);
const evtOptions = {
typ: 'ev+sd-jwt',
issuer: iss,
algorithms: ALLOWED_ALGS,
maxTokenAge: '5m',
clockTolerance: '1m',
};
const sdJwt = new SDJwtInstance({
hasher,
verifier: async (data, sig) => {
const compactEvt = `${data}.${sig}`;
try {
await jwtVerify(compactEvt, issuerJwks, evtOptions);
} catch (err) {
// When `kid` is omitted (e.g. Gmail) and multiple keys match in the JWKS,
// jose throws JWKSMultipleMatchingKeys, which yields each matching CryptoKey.
if (err?.code !== 'ERR_JWKS_MULTIPLE_MATCHING_KEYS') throw err;
let matched = false;
for await (const publicKey of err) {
try {
await jwtVerify(compactEvt, publicKey, evtOptions);
matched = true;
break;
} catch {}
}
if (!matched) throw err;
}
return true;
},
kbVerifier: async (data, sig) => {
const holderJwk = evtPayload.cnf?.jwk;
const holderAlg = holderJwk?.alg;
const kbAlg = decoded.kbJwt.header.alg;
if (
!holderAlg ||
!kbAlg ||
!ALLOWED_ALGS.includes(holderAlg) ||
!ALLOWED_ALGS.includes(kbAlg) ||
normalizeAlg(kbAlg) !== normalizeAlg(holderAlg)
) {
throw new Error('Missing, disallowed, or mismatched KB-JWT algorithm.');
}
const holderKey = await importJWK(holderJwk, kbAlg);
await jwtVerify(`${data}.${sig}`, holderKey, {
typ: 'kb+jwt',
audience: expectedAudience,
algorithms: [kbAlg],
maxTokenAge: '5m',
clockTolerance: '1m',
});
return true;
},
});
const verified = await sdJwt.verify(rawToken, {
requiredClaimKeys: ['email', 'email_verified'],
keyBindingNonce: expectedNonce,
expectedKeyBindingAudience: expectedAudience,
keyBindingMaxAgeSeconds: 300,
skewSeconds: 60,
});
أفضل الممارسات
- يجب إجراء جميع عمليات التحقّق من الرموز المميّزة على الخادم فقط. لا تتحقّق من الرموز المميّزة للتحقق في JavaScript من جهة العميل أو تثق في أي حالة تحقّق من جهة العميل، إذ يمكن بسهولة تجاوز عمليات التحقّق من جهة العميل أو تزويرها، ولا توفّر أي ضمان ثقة للجهة المعتمِدة.
- يجب استخدام مكتبات SD-JWT وJOSE/JWT المعتمدة لمنصة الخلفية (على سبيل المثال،
@sd-jwt/coreوjoseفي Node.js) لتحليل عرضEVT~KB-JWTوالتحقّق من صحته، وتمريرkeyBindingNonceإلىsdJwt.verify()حتى لا يمكن تجاوز عملية التحقّق من ربط المفتاح. لا تستخدِم إجراءات مخصّصة لتحليل وتوقيع JWT أو SD-JWT أوsd_hash. - يجب إنشاء
nonceتشفير جديد (لا يقل عن 128 بت، مثلrandomBytes(32).toString('base64url')) على الخادم لكل عرض نموذج، وتخزينه في حالة جلسة من جهة الخادم (وليس مباشرةً في ملف تعريف ارتباط العميل)، وعرض صفحة النموذج باستخدامCache-Control: no-store. - يجب قراءة
nonceالمتوقَّع وحذفه بشكل ذري من مساحة تخزين الجلسة من جهة الخادم فور وصول طلبPOSTحتى لا تتم إعادة تشغيل الرمز المميّز الذي تم اعتراضه. - يجب إجراء مقارنة غير حساسة لحالة الأحرف بين
evtPayload.emailوعنوان البريد الإلكتروني في النموذج الذي تم إرساله (email.toLowerCase())، مع الحفاظ على سلسلة البريد الإلكتروني المُرسَلة. - يجب التحقّق من جميع المفاتيح في JWKS لجهة الإصدار عندما لا يتوفّر
kidفي عنوان EVT (تحذف بعض مقدّمي الخدمات، مثل Gmail،kid). - يجب التحقّق من تفويض نظام أسماء النطاقات (
_email-verification.<domain>) قبل الوثوق بأي مطالبةissفي EVT، وتأكيدmetadata.issuer === issفي ردّ.well-known/email-verification. - لا تتعامل مع عملية التحقّق من ملكية البريد الإلكتروني كدليل على إمكانية تسليم الرسائل إلى البريد الوارد. يُثبت EVP أنّ المستخدم يتحكّم في حساب البريد الإلكتروني لدى مقدّم الخدمة الموثوق به، ويستمر في إرسال رسائل الترحيب الإلكترونية المتعلّقة بالمعاملات والتعامل مع الرسائل المرتدة بشكل طبيعي.
- لا تحظر إنشاء الحساب أو تسجيل الدخول عندما يكون
tokenفارغًا أو عندما يتعذّر إثبات الهوية، بل انتقِل دائمًا بسلاسة إلى كلمة المرور الصالحة لمرة واحدة (OTP) العادية التي يتم إرسالها عبر البريد الإلكتروني أو إلى مسار الرابط السحري.
استراتيجية احتياطية
تم تصميم ميزة "تأكيد عنوان البريد الإلكتروني" (autocomplete="email-verification-token") كـ تحسين تدريجي مقارنةً بالطرق التقليدية لتأكيد عنوان البريد الإلكتروني خارج النطاق (رموز كلمات المرور الصالحة لمرة واحدة أو الروابط السحرية):
- المتصفّحات غير المتوافقة: تتجاهل المتصفّحات التي لا تتوافق مع
autocomplete="email-verification-token"الإدخال المخفي وترسل النموذج بقيمةtokenفارغة. - نطاقات البريد الإلكتروني غير المصادَق عليها أو غير المشارِكة: عندما يُدخل المستخدم عنوان بريد إلكتروني لا ينشر نطاقه سجلّ
TXTلنظام أسماء النطاقات (DNS) لـ_email-verification، أو عندما لا يكون لدى المستخدم جلسة نشطة مع مزوِّد خدمة البريد الإلكتروني في المتصفّح، يرسل المتصفّح النموذج مع قيمةtokenفارغة. - الرموز المميّزة منتهية الصلاحية أو غير الصالحة: إذا انتهت صلاحية
nonceأو تم استخدامه من قبل أو تعذّر تنفيذ أي خطوة من خطواتverifyEmailVerificationTokenفيhandleSignupSubmission، يرصد الخادم الخطأ ويرسل رسالة إلكترونية تقليدية تتضمّن كلمة مرور صالحة لمرة واحدة أو رابطًا سحريًا بدون مقاطعة عملية تسجيل المستخدم أو تسجيل دخوله.