OpenID Connect و id_token
كيف يثبت لك «الدخول بـ Google» هوية المستخدم في رمز موقّع؟ ⏱ نحو 45 دقيقة.
- تشرح ما يضيفه
OIDCفوقOAuth. - تصف
id_tokenومطالباته. - تتحقق من
JWTبالكامل قبل الوثوق به. - تعرف دور
nonceوuserinfo.
١من التفويض إلى الهوية
OAuth يجيب «ماذا يُسمح لتطبيقك»، لكنه لا يقول من المستخدم بطريقة موحّدة. OpenID Connect (OIDC) طبقة رقيقة فوق OAuth تضيف الهوية: بالتدفق نفسه (Authorization Code + PKCE)، لكن مع نتيجة إضافية هي id_token يصف المستخدم بصيغة قياسية.
| OAuth 2.0 | OpenID Connect | |
|---|---|---|
| يجيب | ماذا يُسمح؟ (تفويض) | من المستخدم؟ (هوية) |
| النتيجة | access token | access token + id_token |
| الاستخدام | الوصول إلى واجهات | «تسجيل الدخول بـ…» |
هذا ما خلف أزرار «سجّل الدخول بـ Google/Apple»: OIDC يسلّمك هوية المستخدم موثّقة دون أن تدير أنت كلمات المرور.
٢ما id_token؟ إنه JWT
id_token هو JWT (رمز ويب JSON): ثلاثة أجزاء مفصولة بنقاط — ترويسة، وحمولة (المطالبات)، وتوقيع — كلها مُرمّزة base64url:
| المطالبة Claim | المعنى |
|---|---|
sub | معرّف المستخدم الفريد (الأهم للهوية) |
iss | المُصدِر (خادم الهوية) |
aud | الجمهور (تطبيقك المقصود) |
exp | وقت الانتهاء |
nonce | قيمة تربط الرمز بطلب دخولك (ضد إعادة التشغيل) |
انظر خادمًا يصدر id_token ثم يتحقق منه. التوقيع يضمن أن الحمولة لم تُعبث، والتحقق يكشف أي تلاعب:
const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const jwt = require('jsonwebtoken');const SECRET = 'oidc-secret'; app.get('/callback', (req, res) => { // خادم الهوية يصدر id_token موقّعًا يصف المستخدم const idToken = jwt.sign( { sub: 'user-42', name: 'سارة', email: 'sara@mail.sa', iss: 'https://id.example', aud: 'rafee-store' }, SECRET, { expiresIn: '1h', noTimestamp: true }); res.json({ idToken });}); app.post('/verify', (req, res) => { try { const claims = jwt.verify(req.body.idToken, SECRET); // يتحقق من التوقيع res.json({ user: claims.sub, name: claims.name }); } catch (e) { res.status(401).json({ error: 'id_token غير صالح' }); }}); app.listen(3000);
الخطوة الأولى تصدر id_token يصف سارة. والتحقق من رمز مزوّر يفشل 401؛ لأن توقيعه لا يطابق. المطالبات موثوقة فقط بعد التحقق من التوقيع.
٣لا تثق قبل أن تتحقق
قراءة محتوى JWT سهلة (base64url غير مشفّر)، فلا يكفي فك الرمز؛ يجب التحقق منه كاملًا قبل الوثوق بمطالباته:
ونقطة userinfo في OIDC تعطي تفاصيل أكثر عن المستخدم مقابل رمز الوصول، حين تحتاج أكثر مما في id_token.
توقّع وتحقّق
OIDC يضيف فوق OAuth:
id_token هو:
المطالبة التي تحمل معرّف المستخدم:
قبل الوثوق بمطالبات JWT يجب:
الخلاصة
OIDCطبقة هوية فوقOAuth، نتيجتهاid_tokenيصف المستخدم.id_tokenهوJWT: ترويسة وحمولة وتوقيع؛ أهم مطالباتهsubوissوaudوexp.- فكّ
JWTسهل؛ لا تثق بمطالباته قبل التحقق من التوقيع وissوaudوexp. nonceيمنع إعادة تشغيل رمز قديم، وuserinfoيعطي تفاصيل أكثر.
تحدٍّ برمجي: تحقق من id_token
متقدمنقطة التحقق تقرأ مطالبات id_token بفكّه (decode) فقط دون التحقق من التوقيع، فيقبل أي رمج مزوّر. أصلحها لتتحقق من التوقيع، وترفض المزوّر.
- استخدم
jwt.verifyبالمفتاح بدلjwt.decode. - الرمز الصحيح يعيد اسم المستخدم.
- الرمز المزوّر يُرفض 401.
👀 اعرض الاستجابات المطلوبة
GET /tokenPOST /me{"idToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJoYWNrZXIiLCJuYW1lIjoi2YXYrdin2KzZhSJ9.X"}
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- الرمز المزوّر يُرفض ← 401
- يستخدم jwt.verify
- لا يعتمد على jwt.decode للتحقق
💡 تلميح 1
jwt.decode بـ jwt.verify(token, SECRET) داخل try/catch.💡 تلميح 2
💡 تلميح 3
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const jwt = require('jsonwebtoken');const SECRET = 'oidc-secret';app.get('/token', (req, res) => res.json({ idToken: jwt.sign({ sub: 'u1', name: 'سارة' }, SECRET, { noTimestamp: true }) }));app.post('/me', (req, res) => { try { const claims = jwt.verify(req.body.idToken, SECRET); res.json({ name: claims.name }); } catch (e) { res.status(401).json({ error: 'غير صالح' }); }});app.listen(3000);