طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 243 · أمن الويب
الدرس 16 · الوحدة 7: تقنية OAuth

تدفق Authorization Code مع PKCE

كيف يضمن التدفق أن من يبدّل الرمز هو من بدأ الطلب، لا معترِض في الطريق؟ ⏱ نحو 50 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح خطوات تدفق Authorization Code.
  • تعلّل لماذا رمز التفويض قبل رمز الوصول.
  • تطبّق PKCE بالتحقق من بصمة الـ verifier.
  • تستخدم state لمنع CSRF على التدفق.

١خطوات التدفق

تدفق Authorization Code هو كيف يطبَّق OAuth عمليًّا. الفكرة المحورية: لا يُسلَّم رمز الوصول عبر المتصفح، بل يُسلَّم أولًا رمز تفويض قصير العمر، ثم يبدّله العميل برمز الوصول من الخادم إلى الخادم مباشرة:

خادم التفويضالعميل (متجرك)المستخدم1) توجيه: code_challenge + state2) سجّل الدخول وامنح الإذن3) رمز تفويض (code) + state4) code + code_verifier (من الخادم مباشرة)5) رمز الوصول (access token)
رمز التفويض يمر بالمتصفح؛ ورمز الوصول يُجلب من الخادم مباشرة فلا يمر بالمتصفح.

لماذا هذه اللفة؟ لأن ما يمر عبر المتصفح (في الرابط، في السجل، في التاريخ) معرّض للكشف. رمز التفويض وحده يمر هناك، وهو قصير العمر ويُستخدم مرة، ولا ينفع وحده دون الخطوة الخلفية.

٢PKCE: ربط الطلب بمن بدأه

في تطبيقات الواجهة والجوال لا يوجد «سرّ عميل» يمكن إخفاؤه. فلو اعترض مهاجم رمز التفويض، بدّله هو برمز وصول. PKCE (مفتاح إثبات لتبادل الرمز) يسدّ هذا:

الخطوةماذا يحدث
1. يولّد العميلcode_verifier: سرّ عشوائي يبقى في الخادم
2. يرسل مع الطلبcode_challenge = sha256(verifier) فقط
3. عند تبديل الرمزيرسل العميل الـ verifier الأصلي
4. يتحقق خادم التفويضsha256(verifier) == challenge؟

انظرها عاملة: الطلب الأول يسجّل البصمة، والتبديل الصحيح ينجح. أما من يعترض الرمز دون الـ verifier الأصلي فيفشل:

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const crypto = require('crypto');let issuedChallenge = null; // العميل يحسب verifier عشوائيًّا، و challenge = sha256(verifier)app.post('/authorize', (req, res) => {  issuedChallenge = req.body.codeChallenge;       // خادم التفويض يحفظ البصمة  res.json({ code: 'AUTH_CODE_123', state: req.body.state });  // (الرمز ثابت للتوضيح)}); app.post('/token', (req, res) => {  // يتحقق أن sha256(verifier) يطابق challenge المحفوظ  const h = crypto.createHash('sha256').update(req.body.codeVerifier).digest('base64url');  if (h !== issuedChallenge)    return res.status(400).json({ error: 'PKCE فشل: المعترِض لا يملك الـ verifier' });  res.json({ accessToken: 'ACCESS_TOKEN_abc', expiresIn: 3600 });}); app.listen(3000);

الطلب الثاني (الـ verifier الصحيح) أصدر رمز الوصول. والثالث (verifier خاطئ، كما يملك المعترِض) فشل 400؛ لأن بصمته لا تطابق challenge المحفوظ. الرمز المعترَض وحده بلا قيمة.

٣state ضد CSRF

مع PKCE نرسل قيمة state عشوائية في بداية التدفق، ونتحقق أنها تعود كما هي. هذا يربط الرد ببداية صحيحة، فيمنع مهاجمًا من حقن رمز تفويضه في جلستك (CSRF على التدفق):

وفي العمل الحقيقي لا تكتب هذا بنفسك؛ تستخدم مكتبة OAuth معتمدة تطبّق التدفق و PKCE و state بصورة صحيحة ومختبَرة.

توقّع وتحقّق

لماذا رمز تفويض قبل رمز الوصول؟

PKCE يحمي من:

ما الذي يُرسل في بداية التدفق؟

قيمة state تمنع:

الخلاصة

تحدٍّ برمجي: أكمل تحقق PKCE

متقدم

خادم التفويض المبسّط يصدر رمز الوصول لأي طلب تبديل دون التحقق من PKCE، فأي معترِض لرمز التفويض يحصل على رمز وصول. أكمل تحقق PKCE: قارن بصمة الـ verifier بالـ challenge المحفوظ.

المطلوب:
  • في /token: احسب sha256(codeVerifier) بترميز base64url.
  • إن لم يطابق الـ challenge المحفوظ: ردّ 400.
  • التبديل بالـ verifier الصحيح ينجح ويصدر رمز الوصول.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. POST /authorize {"codeChallenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"}
  2. POST /token {"codeVerifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"}
  3. POST /token {"codeVerifier": "wrong-verifier"}
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • التبديل الصحيح يصدر الرمز
  • verifier خاطئ يُرفض ← 400
  • يحسب sha256
  • يستخدم base64url

💡 تلميح 1
البصمة: crypto.createHash('sha256').update(req.body.codeVerifier).digest('base64url').
💡 تلميح 2
قارنها بـ challenge المحفوظ؛ إن اختلفت ردّ 400.
💡 تلميح 3
الـ verifier الصحيح بصمته تطابق challenge المُرسَل في /authorize.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const crypto = require('crypto');let challenge = null;app.post('/authorize', (req, res) => { challenge = req.body.codeChallenge; res.json({ code: 'C1' }); });app.post('/token', (req, res) => {  const h = crypto.createHash('sha256').update(req.body.codeVerifier || '').digest('base64url');  if (h !== challenge) return res.status(400).json({ error: 'PKCE فشل' });  res.json({ accessToken: 'TOKEN' });});app.listen(3000);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس