Get your free and exclusive 80-page Banking Passkey Report
Back to Overview

كيفية بناء جهة إصدار بيانات اعتماد رقمية (دليل المطورين)

تعرّف على كيفية بناء جهة إصدار بيانات اعتماد قابلة للتحقق (Verifiable Credential) متوافقة مع معايير W3C باستخدام بروتوكول OpenID4VCI. يشرح هذا الدليل خطوة بخطوة كيفية إنشاء تطبيق Next.js يقوم بإصدار بيانات اعتماد موقّعة تشفيرياً ومتوافقة مع المحافظ الرقمي

Amine

Created: August 20, 2025

Updated: August 21, 2025

Blog-Post-Header-Image

See the original blog version in English here.

DigitalCredentialsDemo Icon

Want to experience digital credentials in action?

Try Digital Credentials

1. مقدمة#

تُعد بيانات الاعتماد الرقمية طريقة قوية لإثبات الهوية والادعاءات بطريقة آمنة تحافظ على الخصوصية. ولكن كيف يحصل المستخدمون على بيانات الاعتماد هذه في المقام الأول؟ هنا يصبح دور جهة الإصدار (Issuer) حاسماً. جهة الإصدار هي كيان موثوق — مثل وكالة حكومية، أو جامعة، أو بنك — وهي مسؤولة عن إنشاء وتوزيع بيانات اعتماد موقّعة رقمياً للمستخدمين.

يقدم هذا الدليل برنامجًا تعليميًا شاملاً خطوة بخطوة لبناء جهة إصدار بيانات اعتماد رقمية. سنركز على بروتوكول OpenID for Verifiable Credential Issuance (OpenID4VCI)، وهو معيار حديث يحدد كيف يمكن للمستخدمين الحصول على بيانات الاعتماد من جهة الإصدار وتخزينها بأمان في محافظهم الرقمية.

النتيجة النهائية ستكون تطبيق Next.js وظيفي يمكنه:

  1. قبول بيانات المستخدم من خلال نموذج ويب بسيط.
  2. إنشاء عرض بيانات اعتماد آمن لمرة واحدة.
  3. عرض العرض كـرمز QR ليقوم المستخدم بمسحه باستخدام محفظته المحمولة.
  4. إصدار بيانات اعتماد موقّعة تشفيرياً يمكن للمستخدم تخزينها وتقديمها للتحقق.

1.1 فهم المصطلحات: بيانات الاعتماد الرقمية مقابل بيانات الاعتماد القابلة للتحقق#

قبل أن نمضي قدمًا، من المهم توضيح الفرق بين مفهومين مرتبطين ولكنهما مختلفان:

  • بيانات الاعتماد الرقمية (مصطلح عام): هذه فئة واسعة تشمل أي شكل رقمي من بيانات الاعتماد أو الشهادات أو الإقرارات (attestations). يمكن أن تشمل هذه الشهادات الرقمية البسيطة، أو الشارات الرقمية الأساسية، أو أي بيانات اعتماد مخزنة إلكترونياً قد تحتوي أو لا تحتوي على ميزات أمان تشفيرية.

  • بيانات الاعتماد القابلة للتحقق (VCs - معيار W3C): هذا نوع محدد من بيانات الاعتماد الرقمية التي تتبع معيار نموذج بيانات بيانات الاعتماد القابلة للتحقق من W3C. بيانات الاعتماد القابلة للتحقق هي بيانات اعتماد موقّعة تشفيرياً، ومقاومة للتلاعب، وتحترم الخصوصية، ويمكن التحقق منها بشكل مستقل. وهي تشمل متطلبات فنية محددة مثل:

    • التوقيعات التشفيرية للأصالة والسلامة
    • نموذج بيانات وتنسيقات موحدة
    • آليات عرض تحافظ على الخصوصية
    • بروتوكولات تحقق قابلة للتشغيل المتبادل

في هذا الدليل، سنقوم تحديدًا ببناء جهة إصدار بيانات اعتماد قابلة للتحقق تتبع معيار W3C، وليس مجرد أي نظام لبيانات الاعتماد الرقمية. بروتوكول OpenID4VCI الذي نستخدمه مصمم خصيصًا لإصدار بيانات الاعتماد القابلة للتحقق، وتنسيق JWT-VC الذي سننفذه هو تنسيق متوافق مع W3C لبيانات الاعتماد القابلة للتحقق.

1.2 كيف تعمل#

يكمن سحر بيانات الاعتماد الرقمية في نموذج "مثلث الثقة" البسيط والقوي الذي يضم ثلاثة لاعبين رئيسيين:

  • جهة الإصدار (Issuer): سلطة موثوقة (مثل وكالة حكومية، جامعة، أو بنك) تقوم بتوقيع وإصدار بيانات اعتماد مشفرة للمستخدم. هذا هو الدور الذي نبنيه في هذا الدليل.
  • الحامل (Holder): المستخدم، الذي يتلقى بيانات الاعتماد ويخزنها بأمان في محفظة رقمية شخصية على جهازه.
  • جهة التحقق (Verifier): تطبيق أو خدمة تحتاج إلى التحقق من بيانات اعتماد المستخدم.

تدفق الإصدار هو الخطوة الأولى في هذا النظام البيئي. تقوم جهة الإصدار بالتحقق من معلومات المستخدم وتزويده ببيانات الاعتماد. بمجرد أن يحصل الحامل على بيانات الاعتماد هذه في محفظته، يمكنه تقديمها إلى جهة التحقق لإثبات هويته أو ادعاءاته، وبذلك يكتمل المثلث.

إليكم نظرة سريعة على التطبيق النهائي أثناء عمله:

الخطوة 1: إدخال بيانات المستخدم يقوم المستخدم بملء نموذج بمعلوماته الشخصية لطلب بيانات اعتماد جديدة.

الخطوة 2: إنشاء عرض بيانات الاعتماد يقوم التطبيق بإنشاء عرض بيانات اعتماد آمن، يتم عرضه كـرمز QR ورمز مصرّح به مسبقًا.

الخطوة 3: التفاعل مع المحفظة يقوم المستخدم بمسح رمز QR باستخدام محفظة متوافقة (مثل Sphereon Wallet) ويدخل رقم تعريف شخصي (PIN) للسماح بالإصدار.

الخطوة 4: إصدار بيانات الاعتماد تتلقى المحفظة وتخزن بيانات الاعتماد الرقمية الصادرة حديثًا، وتكون جاهزة للاستخدام في المستقبل.

2. المتطلبات الأساسية لبناء جهة الإصدار#

قبل أن نتعمق في الكود، دعنا نغطي المعرفة والأدوات الأساسية التي ستحتاجها. يفترض هذا الدليل أن لديك إلمامًا أساسيًا بمفاهيم تطوير الويب، لكن المتطلبات التالية ضرورية لبناء جهة إصدار بيانات الاعتماد.

2.1 خيارات البروتوكول#

تعتمد جهة الإصدار لدينا على مجموعة من المعايير المفتوحة التي تضمن قابلية التشغيل البيني بين المحافظ وخدمات الإصدار. في هذا البرنامج التعليمي، سنركز على ما يلي:

المعيار / البروتوكولالوصف
OpenID4VCIOpenID لإصدار بيانات الاعتماد القابلة للتحقق. هذا هو البروتوكول الأساسي الذي سنستخدمه. يحدد تدفقًا قياسيًا لكيفية طلب المستخدم (عبر محفظته) واستلام بيانات الاعتماد من جهة الإصدار.
JWT-VCبيانات الاعتماد القابلة للتحقق المستندة إلى JWT. تنسيق بيانات الاعتماد التي سنصدرها. وهو معيار من W3C يقوم بترميز بيانات الاعتماد القابلة للتحقق كرموز ويب JSON (JWTs)، مما يجعلها مدمجة وصديقة للويب.
ISO mDocISO/IEC 18013-5. المعيار الدولي لرخص القيادة المحمولة (mDLs). بينما نصدر JWT-VC، فإن الادعاءات الموجودة فيه مهيكلة لتكون متوافقة مع نموذج بيانات mDoc (على سبيل المثال، eu.europa.ec.eudi.pid.1).
OAuth 2.0إطار التفويض الأساسي الذي يستخدمه OpenID4VCI. سنقوم بتنفيذ تدفق pre-authorized_code، وهو نوع منح محدد مصمم لإصدار بيانات اعتماد آمن وسهل الاستخدام.

2.1.1 تدفقات التفويض: الرمز المصرّح به مسبقًا مقابل رمز التفويض#

يدعم OpenID4VCI تدفقين رئيسيين للتفويض لإصدار بيانات الاعتماد:

  1. تدفق الرمز المصرّح به مسبقًا (Pre-Authorized Code Flow): في هذا التدفق، تقوم جهة الإصدار بإنشاء رمز قصير العمر للاستخدام مرة واحدة (pre-authorized_code) يكون متاحًا للمستخدم على الفور. يمكن لمحفظة المستخدم بعد ذلك استبدال هذا الرمز مباشرة ببيانات الاعتماد. هذا التدفق مثالي للسيناريوهات التي يكون فيها المستخدم مصادقًا عليه بالفعل وموجودًا على موقع جهة الإصدار، حيث يوفر تجربة إصدار فورية وسلسة دون إعادة توجيه.

  2. تدفق رمز التفويض (Authorization Code Flow): هذا هو تدفق OAuth 2.0 القياسي، حيث يتم إعادة توجيه المستخدم إلى خادم تفويض لمنح الموافقة. بعد الموافقة، يرسل الخادم authorization_code مرة أخرى إلى redirect_uri مسجل. هذا التدفق أكثر ملاءمة لتطبيقات الطرف الثالث التي تبدأ عملية الإصدار نيابة عن المستخدم.

في هذا الدليل التعليمي، سنستخدم تدفق pre-authorized_code. اخترنا هذا النهج لأنه أبسط ويوفر تجربة مستخدم أكثر مباشرة لحالة الاستخدام الخاصة بنا: مستخدم يطلب مباشرة بيانات اعتماد من موقع جهة الإصدار نفسه. إنه يلغي الحاجة إلى عمليات إعادة التوجيه المعقدة وتسجيل العميل، مما يجعل منطق الإصدار الأساسي أسهل في الفهم والتنفيذ.

هذا المزيج من المعايير يسمح لنا ببناء جهة إصدار متوافقة مع مجموعة واسعة من المحافظ الرقمية ويضمن عملية آمنة وموحدة للمستخدم.

2.2 خيارات الحزمة التقنية#

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

2.2.1 اللغة: TypeScript#

سنستخدم TypeScript لكل من الواجهة الأمامية والخلفية. إن الكتابة الثابتة (static typing) لا تقدر بثمن في تطبيق حاسم للأمان مثل جهة الإصدار، حيث تساعد على منع الأخطاء الشائعة وتحسين الجودة العامة وصيانة الكود.

2.2.2 إطار العمل: Next.js#

Next.js هو إطار العمل المفضل لدينا لأنه يوفر تجربة سلسة ومتكاملة لبناء تطبيقات متكاملة (full-stack).

  • للوجهة الأمامية: سنستخدم Next.js مع React لبناء واجهة المستخدم حيث يمكن للمستخدمين إدخال بياناتهم لطلب بيانات الاعتماد.
  • للوجهة الخلفية: سنستفيد من Next.js API Routes لإنشاء نقاط النهاية من جانب الخادم التي تتعامل مع تدفق OpenID4VCI، من إنشاء عروض بيانات الاعتماد إلى إصدار بيانات الاعتماد الموقّعة النهائية.

2.2.3 المكتبات الرئيسية#

سيعتمد تنفيذنا على عدد قليل من المكتبات الرئيسية للتعامل مع مهام محددة:

  • next، react، و react-dom: المكتبات الأساسية لتطبيق Next.js الخاص بنا.
  • mysql2: عميل MySQL لـNode.js، يستخدم لتخزين رموز التفويض وبيانات الجلسة.
  • uuid: مكتبة لتوليد معرّفات فريدة، والتي سنستخدمها لإنشاء قيم pre-authorized_code.
  • jose: مكتبة قوية للتعامل مع توقيعات ويب JSON (JWS)، والتي سنستخدمها لتوقيع بيانات الاعتماد التي نصدرها تشفيرياً.

2.3 الحصول على محفظة اختبار#

لاختبار جهة الإصدار الخاصة بك، ستحتاج إلى محفظة محمولة تدعم بروتوكول OpenID4VCI. لهذا البرنامج التعليمي، نوصي بـ Sphereon Wallet، وهي متاحة لكل من Android و iOS.

كيفية تثبيت Sphereon Wallet:

  1. قم بتنزيل المحفظة من متجر Google Play أو متجر Apple App Store.
  2. قم بتثبيت التطبيق على جهازك المحمول.
  3. بمجرد التثبيت، تكون المحفظة جاهزة لتلقي عروض بيانات الاعتماد عن طريق مسح رمز QR.

2.4 المعرفة بالتشفير#

يعد إصدار بيانات الاعتماد عملية حاسمة للأمان تعتمد على مفاهيم تشفير أساسية لضمان الثقة والأصالة.

2.4.1 التوقيعات الرقمية#

في جوهرها، بيانات الاعتماد القابلة للتحقق هي مجموعة من الادعاءات التي تم توقيعها رقمياً من قبل جهة الإصدار. يوفر هذا التوقيع ضمانين:

  • الأصالة: يثبت أن بيانات الاعتماد تم إنشاؤها من قبل جهة إصدار شرعية.
  • السلامة: يثبت أن بيانات الاعتماد لم يتم التلاعب بها منذ إصدارها.

2.4.2 تشفير المفتاح العام/الخاص#

يتم إنشاء التوقيعات الرقمية باستخدام تشفير المفتاح العام/الخاص. وإليك كيفية عمل ذلك:

  1. جهة الإصدار لديها زوج مفاتيح: مفتاح خاص، يتم الحفاظ على سريته وأمانه، ومفتاح عام مقابل، يتم إتاحته للعامة.
  2. التوقيع: عندما تنشئ جهة الإصدار بيانات اعتماد، فإنها تستخدم مفتاحها الخاص لإنشاء توقيع رقمي فريد لبيانات الاعتماد.
  3. التحقق: يمكن لجهة التحقق لاحقًا استخدام المفتاح العام لـجهة الإصدار للتحقق من التوقيع. إذا نجح التحقق، تعرف جهة التحقق أن بيانات الاعتماد أصلية ولم يتم تغييرها.

في تنفيذنا، سنقوم بإنشاء زوج مفاتيح من نوع المنحنى الإهليلجي (EC) ونستخدم خوارزمية ES256 لتوقيع JWT-VC. يتم تضمين المفتاح العام في DID الخاص بجهة الإصدار (did:web)، مما يسمح لأي جهة تحقق باكتشافه والتحقق من صحة توقيع بيانات الاعتماد.
ملاحظة: تم حذف ادعاء aud (الجمهور) عمدًا في JWTs الخاصة بنا، حيث تم تصميم بيانات الاعتماد لتكون ذات غرض عام وغير مرتبطة بمحفظة معينة.
إذا كنت ترغب في تقييد الاستخدام لجمهور معين، فقم بتضمين ادعاء aud وقم بتعيينه وفقًا لذلك.

3. نظرة عامة على البنية الهيكلية#

تم بناء تطبيق جهة الإصدار الخاص بنا كمشروع Next.js متكامل، مع فصل واضح بين منطق الواجهة الأمامية والخلفية. تسمح لنا هذه البنية بإنشاء تجربة مستخدم سلسة مع التعامل مع جميع العمليات الحاسمة للأمان على الخادم.
هام: جداول verification_sessions و verified_credentials المضمنة في SQL غير مطلوبة لجهة الإصدار هذه ولكن تم تضمينها للتكامل.

  • الواجهة الأمامية (src/app/issue/page.tsx): صفحة React واحدة تسمح للمستخدمين بإدخال بياناتهم لطلب بيانات الاعتماد. تقوم بإجراء استدعاءات API إلى الواجهة الخلفية لدينا لبدء عملية الإصدار.
  • مسارات API الخلفية (src/app/api/issue/...): مجموعة من نقاط النهاية من جانب الخادم التي تنفذ بروتوكول OpenID4VCI.
    • /.well-known/openid-credential-issuer: نقطة نهاية بيانات وصفية عامة. هذا هو أول عنوان URL ستتحقق منه المحفظة لاكتشاف قدرات جهة الإصدار، بما في ذلك خادم التفويض ونقطة نهاية الرمز ونقطة نهاية بيانات الاعتماد وأنواع بيانات الاعتماد التي تقدمها.
    • /.well-known/openid-configuration: نقطة نهاية اكتشاف OpenID Connect قياسية. على الرغم من ارتباطها الوثيق بالنقطة السابقة، فإن هذه النقطة تخدم تكوينات أوسع متعلقة بـ OIDC وغالبًا ما تكون مطلوبة للتشغيل المتبادل مع عملاء OpenID القياسيين.
    • /.well-known/did.json: وثيقة DID لجهة الإصدار الخاصة بنا. عند استخدام طريقة did:web، يتم استخدام هذا الملف لنشر المفاتيح العامة لجهة الإصدار، والتي يمكن لجهات التحقق استخدامها للتحقق من صحة توقيعات بيانات الاعتماد التي تصدرها.
    • authorize/route.ts: ينشئ pre-authorized_code وعرض بيانات اعتماد.
    • token/route.ts: يستبدل pre-authorized_code بـرمز وصول.
    • credential/route.ts: يصدر JWT-VC النهائي الموقع تشفيرياً.
    • schemas/pid/route.ts: يعرض مخطط JSON لبيانات اعتماد PID. يسمح هذا لأي مستهلك لبيانات الاعتماد بفهم هيكلها وأنواع بياناتها.
  • المكتبة (src/lib/):
    • database.ts: يدير جميع تفاعلات قاعدة البيانات، مثل تخزين رموز التفويض ومفاتيح جهة الإصدار.
    • crypto.ts: يعالج جميع عمليات التشفير، بما في ذلك إنشاء المفاتيح وتوقيع JWT.

إليكم مخطط يوضح تدفق الإصدار:

4. بناء جهة الإصدار#

الآن بعد أن أصبح لدينا فهم قوي للمعايير والبروتوكولات والبنية الهيكلية، يمكننا البدء في بناء جهة الإصدار الخاصة بنا.

تابع معنا أو استخدم الكود النهائي

سنستعرض الآن الإعداد وتنفيذ الكود خطوة بخطوة. إذا كنت تفضل الانتقال مباشرة إلى المنتج النهائي، يمكنك استنساخ المشروع الكامل من مستودع GitHub الخاص بنا وتشغيله محليًا.

git clone https://github.com/corbado/digital-credentials-example.git

4.1 إعداد المشروع#

أولاً، سنقوم بتهيئة مشروع Next.js جديد، وتثبيت الاعتماديات اللازمة، وبدء تشغيل قاعدة بياناتنا.

4.1.1 تهيئة تطبيق Next.js#

افتح الطرفية، انتقل إلى الدليل الذي تريد إنشاء مشروعك فيه، وقم بتشغيل الأمر التالي. نحن نستخدم App Router و TypeScript و Tailwind CSS لهذا المشروع.

npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm

يقوم هذا الأمر بإنشاء تطبيق Next.js جديد في دليلك الحالي.

4.1.2 تثبيت الاعتماديات#

بعد ذلك، نحتاج إلى تثبيت المكتبات التي ستتعامل مع JWTs واتصالات قاعدة البيانات وتوليد UUID.

npm install jose mysql2 uuid @types/uuid

يقوم هذا الأمر بتثبيت:

  • jose: لتوقيع والتحقق من رموز ويب JSON (JWTs).
  • mysql2: عميل MySQL لقاعدة بياناتنا.
  • uuid: لتوليد سلاسل تحدي فريدة.
  • @types/uuid: أنواع TypeScript لمكتبة uuid.

4.1.3 بدء تشغيل قاعدة البيانات#

تتطلب الواجهة الخلفية لدينا قاعدة بيانات MySQL لتخزين رموز التفويض وجلسات الإصدار ومفاتيح جهة الإصدار. لقد قمنا بتضمين ملف docker-compose.yml لتسهيل ذلك.

إذا قمت باستنساخ المستودع، يمكنك ببساطة تشغيل docker-compose up -d. إذا كنت تبني من الصفر، فقم بإنشاء ملف باسم docker-compose.yml بالمحتوى التالي:

services: mysql: image: mysql:8.0 restart: always environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: digital_credentials MYSQL_USER: app_user MYSQL_PASSWORD: app_password ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 20s retries: 10 volumes: mysql_data:

يتطلب إعداد Docker Compose هذا أيضًا نصًا برمجيًا لتهيئة SQL. قم بإنشاء دليل باسم sql وداخله، ملف باسم init.sql بالمحتوى التالي لإعداد الجداول اللازمة لكل من جهة التحقق وجهة الإصدار:

-- Create database if not exists CREATE DATABASE IF NOT EXISTS digital_credentials; USE digital_credentials; -- Table for storing challenges CREATE TABLE IF NOT EXISTS challenges ( id VARCHAR(36) PRIMARY KEY, challenge VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, used BOOLEAN DEFAULT FALSE, INDEX idx_challenge (challenge), INDEX idx_expires_at (expires_at) ); -- Table for storing verification sessions CREATE TABLE IF NOT EXISTS verification_sessions ( id VARCHAR(36) PRIMARY KEY, challenge_id VARCHAR(36), status ENUM('pending', 'verified', 'failed', 'expired') DEFAULT 'pending', presentation_data JSON, verified_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE, INDEX idx_challenge_id (challenge_id), INDEX idx_status (status) ); -- Table for storing verified credentials data (optional) CREATE TABLE IF NOT EXISTS verified_credentials ( id VARCHAR(36) PRIMARY KEY, session_id VARCHAR(36), credential_type VARCHAR(255), issuer VARCHAR(255), subject VARCHAR(255), claims JSON, verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (session_id) REFERENCES verification_sessions(id) ON DELETE CASCADE, INDEX idx_session_id (session_id), INDEX idx_credential_type (credential_type) ); -- ISSUER TABLES -- Table for storing authorization codes in OpenID4VCI flow CREATE TABLE IF NOT EXISTS authorization_codes ( id VARCHAR(36) PRIMARY KEY, code VARCHAR(255) NOT NULL UNIQUE, client_id VARCHAR(255), scope VARCHAR(255), code_challenge VARCHAR(255), code_challenge_method VARCHAR(50), redirect_uri TEXT, user_pin VARCHAR(10), expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, used BOOLEAN DEFAULT FALSE, INDEX idx_code (code), INDEX idx_expires_at (expires_at) ); -- Table for storing issuance sessions CREATE TABLE IF NOT EXISTS issuance_sessions ( id VARCHAR(36) PRIMARY KEY, authorization_code_id VARCHAR(36), access_token VARCHAR(255), token_type VARCHAR(50) DEFAULT 'Bearer', expires_in INT DEFAULT 3600, c_nonce VARCHAR(255), c_nonce_expires_at TIMESTAMP, status ENUM('pending', 'authorized', 'credential_issued', 'expired', 'failed') DEFAULT 'pending', user_data JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (authorization_code_id) REFERENCES authorization_codes(id) ON DELETE CASCADE, INDEX idx_access_token (access_token), INDEX idx_c_nonce (c_nonce), INDEX idx_status (status) ); -- Table for storing issued credentials CREATE TABLE IF NOT EXISTS issued_credentials ( id VARCHAR(36) PRIMARY KEY, session_id VARCHAR(36), credential_id VARCHAR(255), credential_type VARCHAR(255) DEFAULT 'jwt_vc', doctype VARCHAR(255) DEFAULT 'eu.europa.ec.eudi.pid.1', credential_data LONGTEXT, -- Base64 encoded mDoc credential_claims JSON, issuer_did VARCHAR(255), subject_id VARCHAR(255), issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP, revoked BOOLEAN DEFAULT FALSE, revoked_at TIMESTAMP NULL, FOREIGN KEY (session_id) REFERENCES issuance_sessions(id) ON DELETE CASCADE, INDEX idx_credential_id (credential_id), INDEX idx_session_id (session_id), INDEX idx_doctype (doctype), INDEX idx_subject_id (subject_id), INDEX idx_issued_at (issued_at) ); -- Table for storing issuer keys (simplified for demo) CREATE TABLE IF NOT EXISTS issuer_keys ( id VARCHAR(36) PRIMARY KEY, key_id VARCHAR(255) NOT NULL UNIQUE, key_type VARCHAR(50) NOT NULL, -- 'EC', 'RSA' algorithm VARCHAR(50) NOT NULL, -- 'ES256', 'RS256', etc. public_key TEXT NOT NULL, -- JWK format private_key TEXT NOT NULL, -- JWK format (encrypted in production) is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_key_id (key_id), INDEX idx_is_active (is_active) );

بمجرد أن يكون كلا الملفين في مكانهما، افتح الطرفية في جذر المشروع وقم بتشغيل:

docker-compose up -d

سيقوم هذا الأمر ببدء حاوية MySQL في الخلفية، جاهزة للاستخدام من قبل تطبيقنا.

4.2 تنفيذ المكتبات المشتركة#

قبل أن نبني نقاط نهاية API، دعنا ننشئ المكتبات المشتركة التي ستتعامل مع منطق الأعمال الأساسي. يحافظ هذا النهج على نظافة مسارات API الخاصة بنا وتركيزها على معالجة طلبات HTTP، بينما يتم تفويض العمل المعقد إلى هذه الوحدات.

4.2.1 مكتبة قاعدة البيانات (src/lib/database.ts)#

هذا الملف هو المصدر الوحيد للحقيقة لجميع تفاعلات قاعدة البيانات. يستخدم مكتبة mysql2 للاتصال بحاوية MySQL الخاصة بنا ويوفر مجموعة من الوظائف المصدرة لإنشاء وقراءة وتحديث السجلات في جداولنا. تجعل هذه الطبقة المجردة الكود الخاص بنا أكثر نمطية وأسهل في الصيانة.

أنشئ الملف src/lib/database.ts بالمحتوى التالي:

// src/lib/database.ts import mysql from "mysql2/promise"; // Database connection configuration const dbConfig = { host: process.env.DATABASE_HOST || "localhost", port: parseInt(process.env.DATABASE_PORT || "3306"), user: process.env.DATABASE_USER || "app_user", password: process.env.DATABASE_PASSWORD || "app_password", database: process.env.DATABASE_NAME || "digital_credentials", timezone: "+00:00", }; let connection: mysql.Connection | null = null; export async function getConnection(): Promise<mysql.Connection> { if (!connection) { connection = await mysql.createConnection(dbConfig); } return connection; } // Data-Access-Object (DAO) functions for each table // ... (e.g., createChallenge, getChallenge, createAuthorizationCode, etc.)

ملاحظة: للاختصار، تم حذف القائمة الكاملة لوظائف DAO. يمكنك العثور على الكود الكامل في مستودع المشروع. يتضمن هذا الملف وظائف لإدارة التحديات وجلسات التحقق ورموز التفويض وجلسات الإصدار ومفاتيح جهة الإصدار.

4.2.2 مكتبة التشفير (src/lib/crypto.ts)#

يتعامل هذا الملف مع جميع عمليات التشفير الحاسمة للأمان. يستخدم مكتبة jose لإنشاء أزواج المفاتيح وتوقيع رموز ويب JSON (JWTs).

إنشاء المفاتيح تنشئ وظيفة generateIssuerKeyPair زوج مفاتيح جديدًا من نوع المنحنى الإهليلجي سيتم استخدامه لتوقيع بيانات الاعتماد. يتم تصدير المفتاح العام بتنسيق JSON Web Key (JWK) بحيث يمكن نشره في وثيقة did.json الخاصة بنا.

// src/lib/crypto.ts import { generateKeyPair, exportJWK, SignJWT } from "jose"; export async function generateIssuerKeyPair(keyId: string, issuerDid: string) { const { publicKey, privateKey } = await generateKeyPair("ES256", { crv: "P-256", extractable: true, }); const publicKeyJWK = await exportJWK(publicKey); publicKeyJWK.kid = keyId; // Assign a unique key ID // ... (private key export and other setup) return { publicKey, privateKey, publicKeyJWK /* ... */ }; }

إنشاء بيانات اعتماد JWT وظيفة createJWTVerifiableCredential هي جوهر عملية الإصدار. تأخذ ادعاءات المستخدم، وزوج مفاتيح جهة الإصدار، وبيانات وصفية أخرى، وتستخدمها لإنشاء JWT-VC موقّع.

// src/lib/crypto.ts export async function createJWTVerifiableCredential( claims: MDocClaims, issuerKeyPair: IssuerKeyPair, subjectId: string, audience: string, ): Promise<string> { const now = Math.floor(Date.now() / 1000); const oneYear = 365 * 24 * 60 * 60; const vcPayload = { // The issuer's DID iss: issuerKeyPair.issuerDid, // The subject's (holder's) DID sub: subjectId, // The time the credential was issued (iat) and when it expires (exp) iat: now, exp: now + oneYear, // The Verifiable Credential data model vc: { "@context": [ "https://www.w3.org/2018/credentials/v1", "https://europa.eu/eudi/pid/v1", ], type: ["VerifiableCredential", "eu.europa.ec.eudi.pid.1"], issuer: issuerKeyPair.issuerDid, issuanceDate: new Date(now * 1000).toISOString(), credentialSubject: { id: subjectId, ...claims, }, }, }; // Sign the payload with the issuer's private key return await new SignJWT(vcPayload) .setProtectedHeader({ alg: issuerKeyPair.algorithm, kid: issuerKeyPair.keyId, typ: "JWT", }) .sign(issuerKeyPair.privateKey); }

تقوم هذه الوظيفة ببناء حمولة JWT وفقًا لنموذج بيانات بيانات الاعتماد القابلة للتحقق من W3C وتوقعها بالمفتاح الخاص لجهة الإصدار، مما ينتج عنه بيانات اعتماد قابلة للتحقق آمنة.

4.2 نظرة عامة على بنية تطبيق Next.js#

تم تصميم تطبيق Next.js الخاص بنا لفصل الاهتمامات بين الواجهة الأمامية والخلفية، على الرغم من أنهما جزء من نفس المشروع. يتم تحقيق ذلك من خلال الاستفادة من App Router لكل من صفحات واجهة المستخدم ونقاط نهاية API.

  • الواجهة الأمامية (src/app/issue/page.tsx): مكون صفحة React واحد يحدد واجهة المستخدم لمسار /issue. يتعامل مع إدخال المستخدم ويتواصل مع API الخلفية لدينا.

  • مسارات API الخلفية (src/app/api/...):

    • الاكتشاف (.well-known/.../route.ts): تعرض هذه المسارات نقاط نهاية بيانات وصفية عامة تسمح للمحافظ والعملاء الآخرين باكتشاف قدرات جهة الإصدار ومفاتيحها العامة.
    • الإصدار (issue/.../route.ts): تنفذ هذه النقاط منطق OpenID4VCI الأساسي، بما في ذلك إنشاء عروض بيانات الاعتماد، وإصدار الرموز، وتوقيع بيانات الاعتماد النهائية.
    • المخطط (schemas/pid/route.ts): يخدم هذا المسار مخطط JSON لبيانات الاعتماد، ويحدد هيكلها.
  • المكتبة (src/lib/): يحتوي هذا الدليل على منطق قابل لإعادة الاستخدام مشترك عبر الواجهة الخلفية.

    • database.ts: يدير جميع تفاعلات قاعدة البيانات، ويجرد استعلامات SQL.
    • crypto.ts: يعالج جميع عمليات التشفير، مثل إنشاء المفاتيح وتوقيع JWT.

هذا الفصل الواضح يجعل التطبيق نمطيًا وأسهل في الصيانة.

ملاحظة: يجب أن تعيد وظيفة generateIssuerDid() did:web صالحًا يتطابق مع نطاق جهة الإصدار الخاصة بك.
عند النشر، يجب تقديم .well-known/did.json عبر HTTPS على هذا النطاق حتى تتمكن جهات التحقق من التحقق من صحة بيانات الاعتماد.

4.3 بناء الواجهة الأمامية#

واجهتنا الأمامية هي صفحة React واحدة توفر نموذجًا بسيطًا للمستخدمين لطلب بيانات اعتماد رقمية جديدة. مسؤولياتها هي:

  • التقاط بيانات المستخدم (الاسم، تاريخ الميلاد، إلخ).
  • إرسال هذه البيانات إلى الواجهة الخلفية لدينا لإنشاء عرض بيانات اعتماد.
  • عرض رمز QR ورقم التعريف الشخصي (PIN) الناتج ليقوم المستخدم بمسحه باستخدام محفظته.

يتم التعامل مع المنطق الأساسي في وظيفة handleSubmit، والتي يتم تشغيلها عندما يرسل المستخدم النموذج.

// src/app/issue/page.tsx const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); setCredentialOffer(null); try { // 1. Validate required fields if (!userData.given_name || !userData.family_name || !userData.birth_date) { throw new Error("Please fill in all required fields"); } // 2. Request a credential offer from the backend const response = await fetch("/api/issue/authorize", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ user_data: userData, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error( errorData.error_description || "Failed to create credential offer", ); } // 3. Set the credential offer in state to display the QR code const result = await response.json(); setCredentialOffer(result); } catch (err) { const errorMessage = (err as Error).message || "Unknown error occurred"; setError(errorMessage); } finally { setLoading(false); } };

تؤدي هذه الوظيفة ثلاثة إجراءات رئيسية:

  1. التحقق من صحة بيانات النموذج للتأكد من ملء جميع الحقول المطلوبة.
  2. إرسال طلب POST إلى نقطة نهاية /api/issue/authorize لدينا مع بيانات المستخدم.
  3. تحديث حالة المكون بعرض بيانات الاعتماد المستلم من الواجهة الخلفية، مما يؤدي إلى عرض واجهة المستخدم لرمز QR ورمز المعاملة.

يحتوي باقي الملف على كود React قياسي لعرض النموذج وشاشة عرض رمز QR. يمكنك عرض الملف الكامل في مستودع المشروع.

4.4 إعداد البيئة والاكتشاف#

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

4.4.1 إنشاء ملف البيئة#

أنشئ ملفًا باسم .env.local في جذر مشروعك وأضف السطر التالي. يجب أن يكون هذا العنوان URL متاحًا للعامة حتى تتمكن محفظة الهاتف المحمول من الوصول إليه. للتطوير المحلي، يمكنك استخدام خدمة نفق مثل ngrok لكشف localhost الخاص بك.

NEXT_PUBLIC_BASE_URL=http://localhost:3000

4.4.2 تنفيذ نقاط الاكتشاف#

تكتشف المحافظ قدرات جهة الإصدار عن طريق الاستعلام عن عناوين URL قياسية .well-known. نحتاج إلى إنشاء ثلاث من هذه النقاط.

1. بيانات وصفية لجهة الإصدار (/.well-known/openid-credential-issuer)

هذا هو ملف الاكتشاف الأساسي لـ OpenID4VCI. يخبر المحفظة بكل ما تحتاج لمعرفته حول جهة الإصدار، بما في ذلك نقاط النهاية الخاصة بها، وأنواع بيانات الاعتماد التي تقدمها، والخوارزميات التشفيرية المدعومة.

أنشئ الملف src/app/.well-known/openid-credential-issuer/route.ts:

// src/app/.well-known/openid-credential-issuer/route.ts import { NextResponse } from "next/server"; export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const issuerMetadata = { // The issuer's unique identifier. issuer: baseUrl, // The URL of the authorization server. For simplicity, our issuer is its own authorization server. authorization_servers: [baseUrl], // The URL of the credential issuer. credential_issuer: baseUrl, // The endpoint where the wallet will POST to receive the actual credential. credential_endpoint: `${baseUrl}/api/issue/credential`, // The endpoint where the wallet exchanges an authorization code for an access token. token_endpoint: `${baseUrl}/api/issue/token`, // The endpoint for the authorization flow (not used in our pre-authorized flow, but good practice to include). authorization_endpoint: `${baseUrl}/api/issue/authorize`, // Indicates support for the pre-authorized code flow without requiring client authentication. pre_authorized_grant_anonymous_access_supported: true, // Human-readable information about the issuer. display: [ { name: "Corbado Credentials Issuer", locale: "en-US", }, ], // A list of the credential types this issuer can issue. credential_configurations_supported: { "eu.europa.ec.eudi.pid.1": { // The format of the credential (e.g., jwt_vc, mso_mdoc). format: "jwt_vc", // The specific document type, conforming to ISO mDoc standards. doctype: "eu.europa.ec.eudi.pid.1", // The OAuth 2.0 scope associated with this credential type. scope: "eu.europa.ec.eudi.pid.1", // Methods the wallet can use to prove possession of its key. cryptographic_binding_methods_supported: ["jwk"], // Signing algorithms the issuer supports for this credential. credential_signing_alg_values_supported: ["ES256"], // Proof-of-possession types the wallet can use. proof_types_supported: { jwt: { proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"], }, }, // Display properties for the credential. display: [ { name: "Corbado Credential Issuer", locale: "en-US", logo: { uri: `${baseUrl}/logo.png`, alt_text: "EU Digital Identity", }, background_color: "#003399", text_color: "#FFFFFF", }, ], // A list of the claims (attributes) in the credential. claims: { "eu.europa.ec.eudi.pid.1": { given_name: { mandatory: true, display: [{ name: "Given Name", locale: "en-US" }], }, family_name: { mandatory: true, display: [{ name: "Family Name", locale: "en-US" }], }, birth_date: { mandatory: true, display: [{ name: "Date of Birth", locale: "en-US" }], }, }, }, }, }, // Authentication methods supported by the token endpoint. 'none' means public client. token_endpoint_auth_methods_supported: ["none"], // PKCE code challenge methods supported. code_challenge_methods_supported: ["S256"], // OAuth 2.0 grant types the issuer supports. grant_types_supported: [ "authorization_code", "urn:ietf:params:oauth:grant-type:pre-authorized_code", ], }; return NextResponse.json(issuerMetadata, { headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0", }, }); }

2. تكوين OpenID (/.well-known/openid-configuration)

هذا مستند اكتشاف OIDC قياسي يوفر مجموعة أوسع من تفاصيل التكوين.

أنشئ الملف src/app/.well-known/openid-configuration/route.ts:

// src/app/.well-known/openid-configuration/route.ts import { NextResponse } from "next/server"; export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const openidConfiguration = { // The issuer's unique identifier. credential_issuer: baseUrl, // The endpoint where the wallet will POST to receive the actual credential. credential_endpoint: `${baseUrl}/api/issue/credential`, // The endpoint for the authorization flow. authorization_endpoint: `${baseUrl}/api/issue/authorize`, // The endpoint where the wallet exchanges an authorization code for an access token. token_endpoint: `${baseUrl}/api/issue/token`, // A list of the credential types this issuer can issue. credential_configurations_supported: { "eu.europa.ec.eudi.pid.1": { format: "jwt_vc", scope: "eu.europa.ec.eudi.pid.1", cryptographic_binding_methods_supported: ["jwk"], credential_signing_alg_values_supported: ["ES256", "ES384", "ES512"], proof_types_supported: { jwt: { proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"], }, }, }, }, // OAuth 2.0 grant types the issuer supports. grant_types_supported: [ "authorization_code", "urn:ietf:params:oauth:grant-type:pre-authorized_code", ], // Indicates support for the pre-authorized code flow. pre_authorized_grant_anonymous_access_supported: true, // PKCE code challenge methods supported. code_challenge_methods_supported: ["S256"], // Authentication methods supported by the token endpoint. token_endpoint_auth_methods_supported: ["none"], // OAuth 2.0 scopes the issuer supports. scopes_supported: ["eu.europa.ec.eudi.pid.1"], }; return NextResponse.json(openidConfiguration, { headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0", }, }); }

3. مستند DID (/.well-known/did.json)

ينشر هذا الملف المفتاح العام لجهة الإصدار باستخدام طريقة did:web، مما يسمح لأي شخص بالتحقق من توقيع بيانات الاعتماد الصادرة عنها.

أنشئ الملف src/app/.well-known/did.json/route.ts:

// src/app/.well-known/did.json/route.ts import { NextResponse } from "next/server"; import { getActiveIssuerKey } from "../../../lib/database"; import { generateIssuerDid } from "../../../lib/crypto"; export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const issuerKey = await getActiveIssuerKey(); if (!issuerKey) { return NextResponse.json( { error: "No active issuer key found" }, { status: 404 }, ); } const publicKeyJWK = JSON.parse(issuerKey.public_key); const didId = generateIssuerDid(); const didDocument = { // The context defines the vocabulary used in the document. "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1", ], // The DID URI, which is the unique identifier for the issuer. id: didId, // The DID controller, which is the entity that controls the DID. Here, it's the issuer itself. controller: didId, // A list of public keys that can be used to verify signatures from the issuer. verificationMethod: [ { // A unique identifier for the key, scoped to the DID. id: `${didId}#${issuerKey.key_id}`, // The type of the key. type: "JsonWebKey2020", // The DID of the key's controller. controller: didId, // The public key in JWK format. publicKeyJwk: publicKeyJWK, }, ], // Specifies which keys can be used for authentication (proving control of the DID). authentication: [`${didId}#${issuerKey.key_id}`], // Specifies which keys can be used for creating verifiable credentials. assertionMethod: [`${didId}#${issuerKey.key_id}`], // A list of services provided by the DID subject, such as the issuer endpoint. service: [ { id: `${didId}#openid-credential-issuer`, type: "OpenIDCredentialIssuer", serviceEndpoint: `${baseUrl}/.well-known/openid-credential-issuer`, }, ], }; return NextResponse.json(didDocument, { headers: { "Content-Type": "application/did+json", "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0", }, }); }

لماذا لا يوجد تخزين مؤقت؟ ستلاحظ أن جميع هذه النقاط الثلاث تعيد ترويسات تمنع التخزين المؤقت بقوة (Cache-Control: no-cache، Pragma: no-cache، Expires: 0). هذه ممارسة أمنية حاسمة لوثائق الاكتشاف. يمكن أن تتغير تكوينات جهة الإصدار - على سبيل المثال، قد يتم تدوير مفتاح تشفير. إذا قامت محفظة أو عميل بتخزين نسخة قديمة من ملف did.json أو openid-credential-issuer، فسيفشل في التحقق من صحة بيانات الاعتماد الجديدة أو التفاعل مع نقاط النهاية المحدثة. من خلال إجبار العملاء على جلب نسخة جديدة في كل طلب، نضمن أن لديهم دائمًا أحدث المعلومات.

4.4.3 تنفيذ نقطة نهاية مخطط بيانات الاعتماد#

الجزء الأخير من البنية التحتية العامة لدينا هو نقطة نهاية مخطط بيانات الاعتماد. يخدم هذا المسار مخطط JSON الذي يحدد رسميًا الهيكل وأنواع البيانات والقيود الخاصة ببيانات اعتماد PID التي نصدرها. يمكن للمحافظ وجهات التحقق استخدام هذا المخطط للتحقق من صحة محتويات بيانات الاعتماد.

أنشئ الملف src/app/api/schemas/pid/route.ts بالمحتوى التالي:

// src/app/api/schemas/pid/route.ts import { NextResponse } from "next/server"; export async function GET() { const schema = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "https://example.com/schemas/pid", // Replace with your actual domain title: "PID Credential", description: "A schema for a Verifiable Credential representing a Personal Identification Document (PID).", type: "object", properties: { credentialSubject: { type: "object", properties: { given_name: { type: "string" }, family_name: { type: "string" }, birth_date: { type: "string", format: "date" }, // ... other properties of the credential subject }, required: ["given_name", "family_name", "birth_date"], }, // ... other top-level properties of a Verifiable Credential }, }; return NextResponse.json(schema, { headers: { "Content-Type": "application/schema+json", "Access-Control-Allow-Origin": "*", // Allow cross-origin requests }, }); }

ملاحظة: يمكن أن يكون مخطط JSON لبيانات اعتماد PID كبيرًا ومفصلاً للغاية. للاختصار، تم اقتطاع المخطط الكامل. يمكنك العثور على الملف الكامل في مستودع المشروع.

4.5 بناء نقاط النهاية الخلفية (Backend Endpoints)#

مع وجود الواجهة الأمامية، نحتاج الآن إلى منطق جانب الخادم للتعامل مع تدفق OpenID4VCI. سنبدأ بأول نقطة نهاية تستدعيها الواجهة الأمامية: /api/issue/authorize.

4.5.1 /api/issue/authorize: إنشاء عرض بيانات الاعتماد#

هذه النقطة مسؤولة عن أخذ بيانات المستخدم، وإنشاء رمز آمن للاستخدام مرة واحدة، وبناء credential_offer يمكن لمحفظة المستخدم فهمه.

إليك المنطق الأساسي:

// src/app/api/issue/authorize/route.ts import { NextRequest, NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { createAuthorizationCode } from "@/lib/database"; export async function POST(request: NextRequest) { try { const body = await request.json(); const { user_data } = body; // 1. Validate user data if ( !user_data || !user_data.given_name || !user_data.family_name || !user_data.birth_date ) { return NextResponse.json({ error: "missing_user_data" }, { status: 400 }); } // 2. Generate a pre-authorized code and a PIN const code = uuidv4(); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes const txCode = Math.floor(1000 + Math.random() * 9000).toString(); // 4-digit PIN // 3. Store the code and user data await createAuthorizationCode(uuidv4(), code, expiresAt); // Note: This uses an in-memory store for demo purposes only. // In production, persist data securely in a database with proper expiry. if (!(global as any).userDataStore) (global as any).userDataStore = new Map(); (global as any).userDataStore.set(code, user_data); if (!(global as any).txCodeStore) (global as any).txCodeStore = new Map(); (global as any).txCodeStore.set(code, txCode); // 4. Create the credential offer object const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const credentialOffer = { // The issuer's identifier, which is its base URL. credential_issuer: baseUrl, // An array of credential types the issuer is offering. credential_configuration_ids: ["eu.europa.ec.eudi.pid.1"], // Specifies the grant types the wallet can use. grants: { // We are using the pre-authorized code flow. "urn:ietf:params:oauth:grant-type:pre-authorized_code": { // The one-time code the wallet will exchange for a token. "pre-authorized_code": code, // Indicates that the user must enter a PIN (tx_code) to redeem the code. user_pin_required: true, }, }, }; // 5. Create the full credential offer URI (a deep link for wallets) const credentialOfferUri = `openid-credential-offer://?credential_offer=${encodeURIComponent( JSON.stringify(credentialOffer), )}`; // The final response to the frontend. return NextResponse.json({ // The deep link for the QR code. credential_offer_uri: credentialOfferUri, // The raw pre-authorized code, for display or manual entry. pre_authorized_code: code, // The 4-digit PIN the user must enter in their wallet. tx_code: txCode, }); } catch (error) { console.error("Authorization error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }

الخطوات الرئيسية في هذه النقطة:

  1. التحقق من البيانات: تتأكد أولاً من وجود بيانات المستخدم المطلوبة.
  2. إنشاء الرموز: تنشئ pre-authorized_code فريدًا (UUID) و tx_code مكونًا من 4 أرقام (PIN) لطبقة إضافية من الأمان.
  3. تخزين البيانات: يتم تخزين pre-authorized_code في قاعدة البيانات مع وقت انتهاء صلاحية قصير. يتم تخزين بيانات المستخدم ورقم التعريف الشخصي في الذاكرة، مرتبطة بالرمز.
  4. بناء العرض: تبني كائن credential_offer وفقًا لمواصفات OpenID4VCI. يخبر هذا الكائن المحفظة بمكان جهة الإصدار، وما هي بيانات الاعتماد التي تقدمها، والرمز اللازم للحصول عليها.
  5. إرجاع URI: أخيرًا، تنشئ رابطًا عميقًا URI (openid-credential-offer://...) وتعيده إلى الواجهة الأمامية، مع tx_code ليراه المستخدم.

4.5.2 /api/issue/token: استبدال الرمز برمز وصول#

بمجرد أن يمسح المستخدم رمز QR ويدخل رقم التعريف الشخصي الخاص به، تقوم المحفظة بإجراء طلب POST إلى هذه النقطة. وظيفتها هي التحقق من صحة pre-authorized_code و user_pin (PIN)، وإذا كانت صالحة، تصدر رمز وصول قصير العمر.

أنشئ الملف src/app/api/issue/token/route.ts بالمحتوى التالي:

// src/app/api/issue/token/route.ts import { NextRequest, NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { getAuthorizationCode, markAuthorizationCodeAsUsed, createIssuanceSession, } from "@/lib/database"; export async function POST(request: NextRequest) { try { const formData = await request.formData(); const grant_type = formData.get("grant_type") as string; const code = formData.get("pre-authorized_code") as string; const user_pin = formData.get("user_pin") as string; // 1. Validate the grant type if (grant_type !== "urn:ietf:params:oauth:grant-type:pre-authorized_code") { return NextResponse.json( { error: "unsupported_grant_type" }, { status: 400 }, ); } // 2. Validate the pre-authorized code const authCode = await getAuthorizationCode(code); if (!authCode) { return NextResponse.json( { error: "invalid_grant", error_description: "Invalid or expired code", }, { status: 400 }, ); } // 3. Validate the PIN (tx_code) const expectedTxCode = (global as any).txCodeStore?.get(code); if (expectedTxCode !== user_pin) { return NextResponse.json( { error: "invalid_grant", error_description: "Invalid PIN" }, { status: 400 }, ); } // 4. Generate access token and c_nonce const accessToken = uuidv4(); const cNonce = uuidv4(); const cNonceExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes // 5. Create a new issuance session const userData = (global as any).userDataStore?.get(code); await createIssuanceSession( uuidv4(), authCode.id, accessToken, cNonce, cNonceExpiresAt, userData, ); // 6. Mark the code as used and clean up temporary data await markAuthorizationCodeAsUsed(code); (global as any).txCodeStore?.delete(code); (global as any).userDataStore?.delete(code); // 7. Return the access token response return NextResponse.json({ access_token: accessToken, token_type: "Bearer", expires_in: 3600, // 1 hour c_nonce: cNonce, c_nonce_expires_in: 300, // 5 minutes }); } catch (error) { console.error("Token endpoint error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }

الخطوات الرئيسية في هذه النقطة:

  1. التحقق من نوع المنح: تتأكد من أن المحفظة تستخدم نوع منح pre-authorized_code الصحيح.
  2. التحقق من الرمز: تتحقق من أن pre-authorized_code موجود في قاعدة البيانات، وأنه لم تنته صلاحيته، ولم يتم استخدامه من قبل.
  3. التحقق من PIN: تقارن user_pin من المحفظة مع tx_code الذي قمنا بتخزينه سابقًا لضمان أن المستخدم قد أذن بالمعاملة.
  4. إنشاء الرموز: تنشئ access_token آمنًا و c_nonce (nonce بيانات الاعتماد)، وهي قيمة تستخدم لمرة واحدة لمنع هجمات إعادة التشغيل على نقطة نهاية بيانات الاعتماد.
  5. إنشاء جلسة: تنشئ سجل issuance_sessions جديدًا في قاعدة البيانات، وتربط رمز الوصول ببيانات المستخدم.
  6. تمييز الرمز كمستخدم: لمنع استخدام نفس العرض مرتين، يتم تمييز pre-authorized_code على أنه مستخدم.
  7. إرجاع الرمز: تعيد access_token و c_nonce إلى المحفظة.

4.5.3 /api/issue/credential: إصدار بيانات الاعتماد الموقّعة#

هذه هي النقطة النهائية والأكثر أهمية. تستخدم المحفظة رمز الوصول الذي تلقته من نقطة نهاية /token لإجراء طلب POST مصادق عليه إلى هذا المسار. وظيفة هذه النقطة هي إجراء التحقق النهائي، وإنشاء بيانات الاعتماد الموقّعة تشفيرياً، وإعادتها إلى المحفظة.

أنشئ الملف src/app/api/issue/credential/route.ts بالمحتوى التالي:

// src/app/api/issue/credential/route.ts import { NextRequest, NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { getIssuanceSessionByToken, updateIssuanceSession, createIssuedCredential, getActiveIssuerKey, } from "@/lib/database"; import { createJWTVerifiableCredential, importIssuerKeyPair, generateIssuerDid, } from "@/lib/crypto"; export async function POST(request: NextRequest) { try { // 1. Validate the Bearer token const authHeader = request.headers.get("authorization"); const accessToken = authHeader?.substring(7); const session = await getIssuanceSessionByToken(accessToken); if (!session) { return NextResponse.json({ error: "invalid_token" }, { status: 401 }); } // 2. Get the user data from the session const userData = session.user_data; if (!userData) { return NextResponse.json({ error: "missing_user_data" }, { status: 400 }); } // 3. Get the active issuer key const issuerKey = await getActiveIssuerKey(); if (!issuerKey) { // In a real application, you would have a more robust key management system. // For this demo, we can generate a key on the fly if one doesn't exist. // This part is omitted for brevity but is in the repository. return NextResponse.json( { error: "server_error", error_description: "Failed to get issuer key", }, { status: 500 }, ); } // 4. Create the JWT-VC const issuerDid = generateIssuerDid(); const keyPair = await importIssuerKeyPair( issuerKey.key_id, issuerKey.public_key, issuerKey.private_key, issuerDid, ); const subjectId = `did:example:${uuidv4()}`; const credentialData = await createJWTVerifiableCredential( userData, keyPair, subjectId, process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000", ); // 5. Store the issued credential in the database await createIssuedCredential(/* ... credential details ... */); await updateIssuanceSession(session.id, "credential_issued"); // 6. Return the signed credential return NextResponse.json({ format: "jwt_vc", credential: credentialData, c_nonce: uuidv4(), // A new nonce for subsequent requests c_nonce_expires_in: 300, }); } catch (error) { console.error("Credential endpoint error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }

الخطوات الرئيسية في هذه النقطة:

  1. التحقق من الرمز: تتحقق من وجود رمز Bearer صالح في ترويسة Authorization وتستخدمه للبحث عن جلسة الإصدار النشطة.
  2. استرداد بيانات المستخدم: تسترد بيانات ادعاءات المستخدم، والتي تم تخزينها في الجلسة عند إنشاء الرمز.
  3. تحميل مفتاح جهة الإصدار: تقوم بتحميل مفتاح التوقيع النشط لجهة الإصدار من قاعدة البيانات. في سيناريو واقعي، سيتم إدارة هذا بواسطة نظام إدارة مفاتيح آمن.
  4. إنشاء بيانات الاعتماد: تستدعي وظيفة المساعدة createJWTVerifiableCredential من src/lib/crypto.ts لبناء وتوقيع JWT-VC.
  5. تسجيل الإصدار: تحفظ سجلاً لبيانات الاعتماد الصادرة في قاعدة البيانات لأغراض التدقيق والإلغاء.
  6. إرجاع بيانات الاعتماد: تعيد بيانات الاعتماد الموقّعة إلى المحفظة في استجابة JSON. تكون المحفظة بعد ذلك مسؤولة عن تخزينها بأمان.

5. تشغيل جهة الإصدار والخطوات التالية#

لديك الآن تنفيذ كامل وشامل لجهة إصدار بيانات اعتماد رقمية. إليك كيفية تشغيله محليًا وما تحتاج إلى مراعاته لنقله من إثبات مفهوم إلى تطبيق جاهز للإنتاج.

5.1 كيفية تشغيل المثال#

  1. استنساخ المستودع:

    git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
  2. تثبيت الاعتماديات:

    npm install
  3. بدء تشغيل قاعدة البيانات: تأكد من أن Docker قيد التشغيل، ثم ابدأ حاوية MySQL:

    docker-compose up -d
  4. تكوين البيئة وتشغيل النفق: هذه هي الخطوة الأكثر أهمية للاختبار المحلي. نظرًا لأن محفظتك المحمولة تحتاج إلى الاتصال بجهاز التطوير الخاص بك عبر الإنترنت، يجب عليك كشف خادمك المحلي بعنوان URL عام HTTPS. سنستخدم ngrok لهذا الغرض.

    أ. بدء ngrok:

    ngrok http 3000

    ب. انسخ عنوان URL HTTPS من مخرجات ngrok (على سبيل المثال، https://random-string.ngrok.io). ج. أنشئ ملف .env.local وقم بتعيين عنوان URL:

    NEXT_PUBLIC_BASE_URL=https://<your-ngrok-url>
  5. تشغيل التطبيق:

    npm run dev

    افتح متصفحك على http://localhost:3000/issue. يمكنك الآن ملء النموذج، وسيشير رمز QR الذي تم إنشاؤه بشكل صحيح إلى عنوان URL العام لـ ngrok، مما يسمح لمحفظتك المحمولة بالاتصال واستلام بيانات الاعتماد.

5.2 أهمية HTTPS و ngrok#

تم بناء بروتوكولات بيانات الاعتماد الرقمية مع إعطاء الأمان أولوية قصوى. لهذا السبب، سترفض المحافظ دائمًا تقريبًا الاتصال بجهة إصدار عبر اتصال غير آمن (http://). تعتمد العملية بأكملها على اتصال HTTPS آمن، والذي يتم تمكينه بواسطة شهادة SSL.

تحل خدمة نفق مثل ngrok كلتا المشكلتين عن طريق إنشاء عنوان URL عام HTTPS آمن (مع شهادة SSL صالحة) يعيد توجيه كل حركة المرور إلى خادم التطوير المحلي الخاص بك.
تتطلب المحافظ HTTPS وسترفض الاتصال بنقاط النهاية غير الآمنة (http://). هذه أداة أساسية لاختبار أي خدمة ويب تحتاج إلى التفاعل مع الأجهزة المحمولة أو webhooks الخارجية.

5.3 ما هو خارج نطاق هذا الدليل#

يركز هذا المثال عن قصد على تدفق الإصدار الأساسي لجعله سهل الفهم. تعتبر الموضوعات التالية خارج النطاق:

  • الأمان الجاهز للإنتاج: جهة الإصدار هذه للأغراض التعليمية. سيتطلب نظام الإنتاج نظام إدارة مفاتيح آمن (KMS) بدلاً من تخزين المفاتيح في قاعدة بيانات، ومعالجة أخطاء قوية، وتحديد معدل الطلبات، وتسجيل تدقيق شامل.
  • إلغاء بيانات الاعتماد: لا ينفذ هذا الدليل آلية لإلغاء بيانات الاعتماد الصادرة.
    على الرغم من أن المخطط يتضمن علامة revoked للاستخدام في المستقبل، إلا أنه لم يتم توفير أي منطق للإلغاء هنا.
  • تدفق رمز التفويض: ركزنا حصريًا على تدفق pre-authorized_code. سيتطلب التنفيذ الكامل لتدفق authorization_code شاشة موافقة المستخدم ومنطق OAuth 2.0 أكثر تعقيدًا.
  • إدارة المستخدم: لا يتضمن الدليل أي مصادقة أو إدارة للمستخدم لجهة الإصدار نفسها. يُفترض أن المستخدم مصادق عليه بالفعل ومصرح له بتلقي بيانات الاعتماد.

6. الخاتمة#

هذا كل شيء! ببضع صفحات من الكود، لدينا الآن جهة إصدار بيانات اعتماد رقمية كاملة وشاملة تقوم بما يلي:

  1. توفر واجهة أمامية سهلة الاستخدام لطلب بيانات الاعتماد.
  2. تنفذ تدفق OpenID4VCI pre-authorized_code بالكامل.
  3. تكشف عن جميع نقاط نهاية الاكتشاف اللازمة للتشغيل المتبادل للمحافظ.
  4. تنشئ وتوقع بيانات اعتماد قابلة للتحقق آمنة ومتوافقة مع المعايير بتنسيق JWT.

بينما يوفر هذا الدليل أساسًا متينًا، فإن جهة الإصدار الجاهزة للإنتاج ستتطلب ميزات إضافية مثل إدارة المفاتيح القوية، والتخزين الدائم بدلاً من المتاجر الموجودة في الذاكرة، وإلغاء بيانات الاعتماد، والتقوية الأمنية الشاملة.
يختلف توافق المحافظ أيضًا؛ يوصى باستخدام Sphereon Wallet للاختبار، ولكن قد لا تدعم المحافظ الأخرى تدفق الرمز المصرّح به مسبقًا كما تم تنفيذه هنا. ومع ذلك، فإن اللبنات الأساسية وتدفق التفاعل سيبقيان كما هما. باتباع هذه الأنماط، يمكنك بناء جهة إصدار آمنة وقابلة للتشغيل المتبادل لأي نوع من بيانات الاعتماد الرقمية.

7. الموارد#

إليك بعض الموارد والمواصفات والأدوات الرئيسية المستخدمة أو المشار إليها في هذا البرنامج التعليمي:

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start Free Trial

Share this article


LinkedInTwitterFacebook

Table of Contents