تدفق Authorization Code مع PKCE
كيف يضمن التدفق أن من يبدّل الرمز هو من بدأ الطلب، لا معترِض في الطريق؟ ⏱ نحو 50 دقيقة.
- تشرح خطوات تدفق
Authorization Code. - تعلّل لماذا رمز التفويض قبل رمز الوصول.
- تطبّق
PKCEبالتحقق من بصمة الـverifier. - تستخدم
stateلمنعCSRFعلى التدفق.
١خطوات التدفق
تدفق Authorization Code هو كيف يطبَّق OAuth عمليًّا. الفكرة المحورية: لا يُسلَّم رمز الوصول عبر المتصفح، بل يُسلَّم أولًا رمز تفويض قصير العمر، ثم يبدّله العميل برمز الوصول من الخادم إلى الخادم مباشرة:
لماذا هذه اللفة؟ لأن ما يمر عبر المتصفح (في الرابط، في السجل، في التاريخ) معرّض للكشف. رمز التفويض وحده يمر هناك، وهو قصير العمر ويُستخدم مرة، ولا ينفع وحده دون الخطوة الخلفية.
٢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: يرسل العميل بصمة سرّ، ويثبته لاحقًا بالسرّ الأصلي؛ فالرمز المعترَض بلا قيمة.stateيربط الرد بالطلب فيمنع حقن رمز غريب.- استخدم مكتبة
OAuthمعتمدة لا تكتب التدفق يدويًّا.
تحدٍّ برمجي: أكمل تحقق PKCE
متقدمخادم التفويض المبسّط يصدر رمز الوصول لأي طلب تبديل دون التحقق من PKCE، فأي معترِض لرمز التفويض يحصل على رمز وصول. أكمل تحقق PKCE: قارن بصمة الـ verifier بالـ challenge المحفوظ.
- في
/token: احسبsha256(codeVerifier)بترميز base64url. - إن لم يطابق الـ challenge المحفوظ: ردّ 400.
- التبديل بالـ verifier الصحيح ينجح ويصدر رمز الوصول.
👀 اعرض الاستجابات المطلوبة
POST /authorize{"codeChallenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"}POST /token{"codeVerifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"}POST /token{"codeVerifier": "wrong-verifier"}
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- التبديل الصحيح يصدر الرمز
- verifier خاطئ يُرفض ← 400
- يحسب sha256
- يستخدم base64url
💡 تلميح 1
crypto.createHash('sha256').update(req.body.codeVerifier).digest('base64url').💡 تلميح 2
💡 تلميح 3
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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);