feat: D5 — OTP متعدد المزوّدين حسب الدولة (من سيرو) + قرار بلا كلمة مرور
مراجعة سيرو (auth/otp/): مصر تستعمل Kazumi SMS مع failover لواتساب، وسوريا/الأردن Nabeh. عندنا كان مزوّد واحد فقط. - integrations/otp/: واجهة OtpProvider + OtpDispatcher يوجّه حسب tenant.countryPack بسلسلة failover (eg: [kazumi, nabeh] · jo/sy: [nabeh]) - KazumiSmsProvider (مصر SMS) + NabehOtpProvider (غلاف على NabehService) - كل مزوّد يرسل رمزاً نولّده نحن (Redis) — استبعدنا نمط Intaleq (يولّد الرمز بنفسه) حفاظاً على مصدر واحد للرمز - موحَّد: AuthService.sendOtp و PayoutsService.issueOtp يمرّان بنفس المُوزِّع الآن — أُزيل كل استدعاء Nabeh مباشر. أي مسار يحتاج رسالة يستدعي الخدمة الموحّدة (طلب المالك) - فشل السلسلة كاملة = ServiceUnavailable صريح، لا صمت قرار المالك (2026-07-17): بلا كلمة مرور إطلاقاً. المصادقة = هاتف + OTP مرة → جلسة مربوطة بالجهاز (D2). التسجيل بالهاتف لا Google/Apple (محظوران في بعض الدول). سيرو نفسه لا كلمة مرور حقيقية له (password = hash(email) وهمي، ومقارنة نصّية غير آمنة في مسار التجربة). يطابق أوبر/كريم/inDrive. مراجعة حدّ الطلبات في سيرو (RateLimiter.php): حدود مسمّاة لكل نوع + fallback بملف عند تعطّل Redis (fail-closed). عندنا مسار OTP fail-closed أصلاً؛ تخزين Throttler على Redis مؤجَّل للتوسّع الأفقي الفعلي (موثّق في D4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c258a8c4b8
commit
8f6c46f0ac
@@ -74,6 +74,15 @@
|
||||
| D2 | **بصمة الجهاز** | 🟡 **مبنيّة ومطفأة** (`AUTH_REQUIRE_DEVICE_BINDING=false`) حتى يرسل فلاتر `x-device-id`. **مُنفَّذة داخل `JwtStrategy` نفسها لا كحارس منفصل** — يُفرض على كل نقطة محميّة تلقائياً، فلا نقطة منسيّة. توكن الدخول يحمل `hash(deviceId)` لا القيمة الخام؛ توكن مسروق من جهاز آخر يُرفض بمجرد تفعيل العلم. |
|
||||
| D3 | **HMAC للعمليات الحساسة** | ✅ **منفَّذ ضمن I6** — `SigningService`/`SignatureGuard`، نفس البند لا تكرار. |
|
||||
| D4 | **حدّ الطلبات (rate limiting)** | ✅ **إصلاح ثغرة قائمة** — راجع أدناه. |
|
||||
| D5 | **OTP متعدد المزوّدين حسب الدولة** | ✅ — راجع أدناه. |
|
||||
|
||||
### D5 — توجيه OTP حسب الدولة + failover (من سيرو `auth/otp/`)
|
||||
مراجعة سيرو أظهرت: مصر تستعمل **Kazumi SMS** (`sms.kazumi.me`) مع failover لواتساب، وسوريا/الأردن **Nabeh** (واتساب، توكن مُخزَّن 24س). عندنا كان مزوّد واحد (Nabeh) فقط.
|
||||
- `integrations/otp/` — واجهة `OtpProvider` + `OtpDispatcher` يوجّه حسب `tenant.countryPack` بسلسلة failover (`eg: [kazumi, nabeh]` · `jo/sy: [nabeh]`). إضافة دولة/مزوّد = صنف موفّر + سطر في السلسلة.
|
||||
- كل مزوّد يرسل **رمزاً نولّده نحن** (مخزَّن في Redis) — لا يولّده المزوّد. استبعدنا نمط Intaleq (يولّد الرمز ويرجعه) حفاظاً على مصدر واحد.
|
||||
- **موحَّد لكل المسارات**: تسجيل الدخول (`AuthService`) والسحب (`PayoutsService`) يمرّان بنفس المُوزِّع الآن — لا استدعاء Nabeh مباشر في أي مكان.
|
||||
- **قرار المالك (2026-07-17): بلا كلمة مرور إطلاقاً.** المصادقة = هاتف + OTP مرة → جلسة مربوطة بالجهاز (D2). لا حقل `password`، لا «نسيت كلمة المرور»، لا عبء دعم. سيرو نفسه لا كلمة مرور حقيقية له (`password = hash(email)` وهمي). يطابق أوبر/كريم/inDrive — المعيار في هذه الأسواق. **التسجيل بالهاتف لا Google/Apple** (محظوران في بعض الدول).
|
||||
- **تصحيح لسيرو**: رمزه من **3 خانات** (1000 احتمال — ضعيف). عندنا 4 (قابل للرفع عبر `OTP_LENGTH`)، وعدّاد 5 محاولات (D4) يغلق التخمين.
|
||||
|
||||
### D4 — حدّ الطلبات: كان مُعطَّلاً كلياً رغم أنه يبدو مفعَّلاً
|
||||
`ThrottlerModule.forRoot([{ ttl: 60000, limit: 120 }])` كان مسجَّلاً في `app.module.ts` منذ البداية — لكن **بلا أي حارس يطبّقه**. لا `APP_GUARD`، ولا `@UseGuards(ThrottlerGuard)` في أي متحكّم. أي أن كل نقطة في الـAPI، بما فيها `verify-otp` و`payouts/*`، كانت **بلا أي حدّ طلبات إطلاقاً** — التسجيل وحده لا يفعل شيئاً في NestJS.
|
||||
@@ -84,6 +93,8 @@
|
||||
- ✅ `HealthController` مُستثنى (`@SkipThrottle()`) — مراقبة التشغيل بلا بيانات حساسة.
|
||||
- 🟡 **قرار وعي بالمخاطرة**: فكّرت في تتبّع بالمستخدم المصادَق لا بالـIP وحده (مهم لتطبيق موبايل — عناوين NAT عند مشغّلي الجوّال تجمع آلاف المستخدمين خلف IP واحد). التنفيذ يحتاج حارساً مخصَّصاً بحقن يدوي دقيق (`InjectThrottlerOptions`/`InjectThrottlerStorage`)، وخطأ فيه **يمنع إقلاع التطبيق كاملاً** — ولا بيئة هنا لتشغيل `NestFactory.create()` والتحقق قبل الدفع (اختبارات jest تُنشئ الخدمات يدوياً، فلا تكشف أخطاء DI للحرّاس العالميين). رجّحت الأمان: تُرك بالتتبّع الافتراضي (IP)، والفكرة موثّقة هنا لتُنفَّذ حين يمكن اختبارها فعلياً على السيرفر قبل الدفع.
|
||||
|
||||
**مقارنة بسيرو** (`core/Auth/RateLimiter.php`): سيرو يتتبّع بـ`IP:userId` (النمط الذي أجّلناه أعلاه) وله حدود مسمّاة لكل نوع (login 5/د · otp 3/5د · register 3/ساعة · api 120/د)، **وأهم ميزة فيه: fallback بملف مؤقّت عند تعطّل Redis بدل تمرير كل الطلبات** (fail-closed). عندنا **مسار OTP fail-closed أصلاً**: تخزين الرمز والعدّاد عبر Redis مباشرةً، فتعطّل Redis يرمي ويُفشل الطلب لا يمرّره. لكن حارس الطلبات العام (ThrottlerGuard) يستعمل ذاكرة داخلية لكل نسخة — لا يشارك بين النسخ. **بند مؤجَّل**: تخزين Throttler على Redis + fallback fail-closed عند الحاجة للتوسّع الأفقي الفعلي.
|
||||
|
||||
---
|
||||
|
||||
## المجموعة E — كشف الاحتيال (من `driver_ride_scam`) — ✅ منفَّذة
|
||||
|
||||
Reference in New Issue
Block a user