OAuth و Social Login — Authentication & Security
OAuth و تسجيل الدخول الاجتماعي
"سجّل الدخول بـ Google" يعني أن Google تتحقق من هوية المستخدم، وتطبيقك يثق بنتيجتها — دون أن يرى كلمة مرور Google أبداً.
مصطلحات
| المصطلح | المعنى |
|---|---|
| OAuth 2.0 | بروتوكول تفويض: السماح لتطبيقك بالوصول لموارد المستخدم عند مزود (تقويمه، مستودعاته) |
| OpenID Connect (OIDC) | طبقة مصادقة فوق OAuth: تعطيك id_token يثبت من هو المستخدم |
| Client ID / Secret | هوية تطبيقك عند المزود؛ السر على الخادم فقط |
| Redirect URI | العنوان الذي يعود إليه المستخدم بعد الموافقة؛ يُسجَّل مسبقاً لدى المزود |
| Scopes | الصلاحيات المطلوبة: openid email profile — اطلب الحد الأدنى |
تدفق رمز التفويض (Authorization Code)
1) المستخدم يضغط "الدخول بـ Google" ← تطبيقك يحوّله إلى Google مع: client_id، redirect_uri، scope، state، code_challenge
2) المستخدم يسجل الدخول في Google ويوافق
3) Google تعيده إلى redirect_uri ومعه ?code=...&state=...
4) خادمك يتحقق من state، ويستبدل code (مع client_secret و code_verifier) بالرموز — اتصال خادم-لخادم
5) خادمك يقرأ هوية المستخدم، ينشئ/يربط الحساب، ويبدأ جلسته الخاصة- state: قيمة عشوائية تُحفظ قبل التحويل وتُقارن عند العودة ← تمنع CSRF في التدفق.
- PKCE (code_challenge/code_verifier): يمنع من يعترض الـ code من استخدامه. إلزامي للتطبيقات الجوالة وتطبيقات المتصفح، ومُستحسن للجميع.
- التدفق الضمني القديم (Implicit، الرمز في الرابط) مُهمل — لا تستخدمه.
تسليم الجلسة للواجهة
بعد الخطوة 5، لا تضع الرمز في رابط إعادة التوجيه (?token=...): يبقى في سجل المتصفح ويُرسل في ترويسة Referer ويظهر في سجلات الخوادم. الأفضل: ملف تعريف ارتباط HttpOnly (كما في المثال الكامل بعد التصحيح)، أو رمز لمرة واحدة قصير جداً تستبدله الواجهة بطلب POST.
ربط الحسابات
- اربط بالمعرّف الثابت من المزود (sub أو profile.id)، لا بالبريد وحده.
- إذا وُجد حساب بنفس البريد مسجل بكلمة مرور، لا تدمجهما تلقائياً إلا إذا كان البريد موثّقاً لدى المزود (email_verified) — وإلا يستطيع مهاجم إنشاء حساب عند مزود ببريدك وأخذ حسابك.
Passport.js
المثال الكامل يستخدم Passport مع استراتيجيات Google و GitHub — تخفي تفاصيل الخطوات 1–4. مكتبات أحدث مثل Auth.js، أو خدمات مثل Supabase Auth و Firebase Auth، تقدم الشيء نفسه بإعداد أقل.
تمرين
سجّل تطبيق OAuth تجريبياً في GitHub (Settings ← Developer settings)، وحدد Redirect URI محلياً، وشغّل المثال. افحص الطلبات في أدوات المطور: أين يظهر state؟ وأين يظهر code؟ ولماذا لا يظهر client_secret أبداً في المتصفح؟
// npm install passport passport-google-oauth20
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// ===== Google OAuth Strategy =====
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/api/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
try {
// البحث عن المستخدم أو إنشاؤه
let user = await User.findOne({ googleId: profile.id });
if (!user) {
// مستخدم جديد
user = await User.create({
googleId: profile.id,
name: profile.displayName,
email: profile.emails[0].value,
avatar: profile.photos[0].value,
provider: 'google'
});
}
return done(null, user);
} catch (err) {
return done(err, null);
}
}
));
// ===== Routes =====
// بدء OAuth flow
app.get('/api/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
// Callback بعد موافقة المستخدم
app.get('/api/auth/google/callback',
passport.authenticate('google', { session: false }),
(req, res) => {
// إنشاء JWT
const token = generateToken(req.user);
// إعادة التوجيه للـ Frontend مع الـ Token
// لا نضع الرمز في الرابط (يتسرب في السجل والـ Referer وسجلات الخوادم): ملف تعريف ارتباط HttpOnly
res.cookie('accessToken', token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 15 * 60 * 1000 });
res.redirect(`${process.env.FRONTEND_URL}/auth/callback`);
}
);
// ===== GitHub OAuth =====
const GitHubStrategy = require('passport-github2').Strategy;
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: '/api/auth/github/callback'
},
async (accessToken, refreshToken, profile, done) => {
let user = await User.findOne({ githubId: profile.id });
if (!user) {
user = await User.create({
githubId: profile.id,
name: profile.displayName || profile.username,
email: profile.emails?.[0]?.value,
avatar: profile.photos?.[0]?.value,
provider: 'github'
});
}
return done(null, user);
}
));
app.get('/api/auth/github',
passport.authenticate('github', { scope: ['user:email'] })
);
app.get('/api/auth/github/callback',
passport.authenticate('github', { session: false }),
(req, res) => {
const token = generateToken(req.user);
res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${token}`);
}
);اختبر فهمك