Files

204 lines
12 KiB
Markdown
Raw Permalink Normal View History

2026-09-25 19:04:48 +03:30
# نحوه ارتباط gold-bank با سامانه هویت متمرکز (IAM)
2026-10-18 22:45:47 +03:30
> ورود مشترک بدون پرش مرورگر به `/auth/bootstrap` کامل نمی‌شود. اگر این متن جایی
> گفته مرورگر هرگز به IAM نمی‌رود، آن جمله کهنه است. قاعدهٔ فعلی برای هر اپ:
> [`iam/docs/onboarding-new-system.md`](../../iam/docs/onboarding-new-system.md).
2026-09-25 19:04:48 +03:30
این سند **مسیر داده** را توضیح می‌دهد: چه کسی، چه زمانی، چه چیزی را به کجا
می‌فرستد و اگر آن طرف جواب نداد چه اتفاقی می‌افتد.
برای پیکربندی و فعال‌سازی: [`iam-sso.md`](iam-sso.md) ·
مرجع تصویری اندپوینت‌های IAM: `http://<آدرس IAM>/docs`
---
## ۱. خلاصه در یک تصویر
```
پنل ادمین (Vue) apps/api (NestJS) Central IAM
─────────────── ───────────────── ───────────
فرم ورودِ همیشگی
(موبایل + رمز)
│ POST /api/admin/auth/login
├──────────────────────────────▶
│ AuthService.verifyPassword
│ │ POST /api/v1/auth/login
│ │ + client_id/secret + client_ip
│ ├────────────────────────────────▶
│ │◀────────────────────────────────┤
│ │ SUCCESS (sub، sid) │ 401 │ خطا
│ │
│ AdminSession: ثبت jti + sid + sub
│◀─────────────────────────────┤ همان پاسخ همیشگی (api_token, roles)
```
سه نکته که کل طراحی را توضیح می‌دهد:
1. **مرورگر ادمین هرگز به IAM نمی‌رود.** نه ریدایرکت، نه دکمه SSO، نه صفحه جدید.
2. **قالب پاسخ ورود تغییر نکرده است** (`api_token`, `roles`, …)، پس پنل تفاوتی نمی‌بیند.
3. **نگاشت کاربر با `mobile` انجام می‌شود.** هیچ ستونی به `AdminUser` اضافه نشده؛
شناسه‌های IAM روی جدول **جدید** `AdminSession` می‌نشینند.
> **اپ کاربر (`bnk.montzar.ir`) هم متصل است**، ولی از مسیر دیگری: ورودش فقط با کد
> یکبارمصرف است و رمزی ندارد که به IAM فرستاده شود. کد را همین سامانه تولید،
> ارسال و بررسی می‌کند و سپس نتیجه را به IAM اعلام می‌کند
> (`/api/v1/auth/login/delegated-otp`) تا نشست مرکزی، لاگ امنیتی و خروج سراسری
> شامل آن ورود هم بشود. توکن اپ هم مثل توکن پنل `jti` می‌گیرد و در جدول
> `AppSession` ثبت می‌شود، پس قابل ابطال است.
---
## ۲. کانال‌های ارتباطی (همه چیزی که رد و بدل می‌شود)
| # | جهت | مسیر | چه زمانی | چه چیزی می‌رود | اگر خطا بدهد |
| --- | --- | --- | --- | --- | --- |
| ۱ | ما → IAM | `POST /api/v1/auth/login` | هر ورود ادمین | موبایل، رمز، `client_id`، `client_secret`، `client_ip` | طبق `IAM_LOGIN_DELEGATION` (پیش‌فرض: `bcrypt` محلی) |
| ۱.۵ | ما → IAM | `POST /api/v1/auth/login/delegated-otp` | **ورود اپ کاربر** (`/auth/verify-otp`) — این ورود رمز ندارد | موبایل، اعتبارنامه کلاینت، `client_ip`، شماره ماسک‌شده | کاربر آنجا نیست یا سرویس down ⇒ ورود محلی ادامه؛ قفل/غیرفعال/2FA ⇒ ورود رد می‌شود |
| ۲ | ما → IAM | `GET /.well-known/jwks.json` | تأیید توکن خروج سراسری (کش ۲۴ ساعته، `kid` ناشناس ⇒ یک‌بار refetch) | — | اعلان خروج پذیرفته نمی‌شود (۲۰۰ + لاگ) |
| ۳ | **IAM → ما** | `POST /api/iam/backchannel-logout` | خروج سراسری یا ابطال نشست توسط ادمینِ IAM | `logout_token` امضاشده (RS256) | همیشه ۲۰۰؛ دلیل رد فقط در لاگ |
فقط همین سه کانال. جریان مرورگری OAuth در این سامانه استفاده نمی‌شود، چون قرار
است رابط کاربری ورود دست‌نخورده بماند.
---
## ۳. جریان ۱ — ورود ادمین
```
POST /api/admin/auth/login { phone, password }
│
├─ AdminUser با این موبایل هست و فعال است؟ (نه ⇒ ۴۰۱ مبهم، مثل قبل)
│
├─ AuthService.verifyPassword
│ ├─ IAM خاموش ⇒ یک bcrypt.compare (دقیقاً رفتار قبلی، بدون هیچ درخواست شبکه)
│ ├─ IAM تأیید کرد ⇒ { ok: true, identity: { sub, sessionId } }
│ ├─ IAM رد کرد (کاربر آنجا نیست/رمز فرق دارد)
│ │ ├─ fallback ⇒ bcrypt محلی
│ │ └─ exclusive ⇒ ۴۰۱
│ ├─ حساب در IAM قفل است ⇒ **رد قطعی** (قفل IAM دور زده نمی‌شود)
│ ├─ IAM در دسترس نیست / ۴۲۹
│ │ ├─ fallback ⇒ bcrypt محلی
│ │ └─ exclusive ⇒ ۴۰۱ با پیام روشن
│ └─ حساب دو مرحله‌ای است ⇒ پیام روشن (این فرم مرحله کد ندارد)
│
├─ jti تازه (فقط وقتی ردیابی نشست روشن است)
├─ امضای همان JWT همیشگی { sub, mobile, jti? }
├─ AdminSession: { jti, adminUserId, iamSessionId, iamSub, ip, userAgent }
│
└─ پاسخ: همان قالب قبلی
```
**چرا `client_ip` حیاتی است:** محدودیت نرخ IAM روی IP اعمال می‌شود. بدون IP کاربر
نهایی، همه ورودها از IP همین سرور دیده می‌شوند و بعد از چند تلاش ناموفق، **کل**
پنل ادمین ۴۲۹ می‌گیرد. اگر API پشت پراکسی است، `trust proxy` را تنظیم کنید وگرنه
`@Ip()` همیشه IP پراکسی را می‌دهد و همان مشکل برمی‌گردد.
---
## ۴. جریان ۲ — اعتبارسنجی هر درخواست (نشست قابل ابطال)
```
هر درخواست با Bearer توکن
│
├─ JwtStrategy: امضا و انقضا (مثل قبل)
├─ توکن از نوع اپ مشتری نباشد (typ)
│
├─ AdminSessionService.isActive(jti)
│ ├─ ردیابی خاموش ⇒ true، بدون هیچ کوئری (هزینه دقیقاً مثل قبل)
│ ├─ توکن بدون jti (صادرشده پیش از فعال‌سازی) ⇒ true تا انقضا
│ ├─ ردیف AdminSession باطل شده یا وجود ندارد ⇒ ۴۰۱
│ └─ سالم ⇒ ادامه
│
└─ AdminUser + مجوزها از دیتابیس (مثل قبل، تا تغییر دسترسی فوری اثر کند)
```
`AUTH_SESSION_TRACKING`: خالی = همراه `IAM_ENABLED` · `true` = روشن بدون IAM ·
`false` = خاموش (ابطال مرکزی دیگر به اینجا نمی‌رسد).
---
## ۵. جریان ۳ — خروج سراسری (تنها مسیر ورودی از IAM)
```
IAM: خروج کاربر یا «بستن همه نشست‌ها» توسط ادمین
│ logout_token امضاشده با RS256 (application/x-www-form-urlencoded)
▼
POST /api/iam/backchannel-logout ← بدون احراز هویت، عمداً
│
├─ IamLogoutService.verify
│ ├─ alg باید RS256 باشد (alg=none رد می‌شود)
│ ├─ امضا با کلید عمومی JWKS
│ ├─ iss برابر issuer پیکربندی‌شده
│ ├─ aud شامل client_id ما
│ ├─ events شامل رخداد back-channel logout (تا ID Token جای آن نرود)
│ ├─ nonce نباید وجود داشته باشد
│ └─ exp/iat با تحمل ۶۰ ثانیه انحراف ساعت
│
└─ AdminSessionService
├─ sid دارد ⇒ همان نشست باطل می‌شود
└─ فقط sub دارد ⇒ همه نشست‌های آن هویت
⇒ درخواست بعدی با آن توکن ۴۰۱ می‌گیرد
```
پاسخ **همیشه ۲۰۰** است (حتی برای توکن نامعتبر) تا IdP بی‌نهایت تلاش مجدد نکند؛
دلیل رد شدن در لاگ سرویس می‌آید.
آدرسی که باید در IAM برای این کلاینت ثبت شود:
```
backchannel_logout_uri = https://<آدرس این API>/api/iam/backchannel-logout
```
ابطال محلی هم به همین مسیر وصل است: غیرفعال کردن حساب یا تغییر رمز یک ادمین
(`AdminUsersService.update`) نشست‌های او را می‌بندد.
---
## ۶. نگاشت هویت‌ها
| در IAM | در gold-bank | قاعده |
| --- | --- | --- |
| موبایل | `AdminUser.mobile` | تنها معیار نگاشت کاربر — هیچ ستون جدیدی روی این جدول نیست |
| `sub` | `AdminSession.iamSub` | برای خروج سراسریِ «همه نشست‌های این هویت» |
| `sid` | `AdminSession.iamSessionId` | برای ابطال دقیقاً همان نشست |
| نقش‌ها | `UserType` / `Permission` محلی | از IAM **خوانده نمی‌شود**؛ دسترسی‌ها مال همین سامانه است |
**چرا شناسه‌های IAM روی جدول جدید نشستند و نه روی `AdminUser`:** تا فعال کردن این
قابلیت هیچ تغییری در جدول‌های موجود لازم نداشته باشد و برگرداندنش هم به همان
سادگی باشد. اگر روزی فیلد دیگری از IAM لازم شد، همین الگو ادامه یابد.
---
## ۷. رفتار در خطا (خلاصه تصمیم‌ها)
| وضعیت | `fallback` (پیش‌فرض) | `exclusive` |
| --- | --- | --- |
| IAM خاموش/ناقص | یک bcrypt، بدون شبکه — رفتار کاملاً قبلی | همان |
| IAM در دسترس نیست | bcrypt محلی؛ ورود ادمین‌ها قطع نمی‌شود | ۴۰۱ «سرویس هویت در دسترس نیست» |
| کاربر در IAM نیست | bcrypt محلی (مناسب مهاجرت تدریجی) | ۴۰۱ |
| رمز در IAM غلط است | bcrypt محلی | ۴۰۱ |
| حساب در IAM قفل است | **۴۰۱** — قفل IAM دور زده نمی‌شود | ۴۰۱ |
| حساب دو مرحله‌ای است | ۴۰۱ با پیام روشن (این فرم مرحله کد ندارد) | همان |
| ثبت `AdminSession` ناموفق | ورود انجام می‌شود (فقط هشدار لاگ) — بدترین حالت: نشستی که زودتر قابل ابطال نیست | همان |
| توکن خروج نامعتبر | ۲۰۰ + هشدار لاگ؛ هیچ نشستی بسته نمی‌شود | همان |
---
## ۸. بررسی سریع اتصال
```bash
cd apps/api
npx jest src/modules/iam src/modules/auth src/modules/sessions # ۶۱ تست
npx prisma migrate deploy # جدول AdminSession
```
مشکلات پرتکرار:
| نشانه | علت |
| --- | --- |
| بعد از چند ورود همه ۴۲۹ می‌گیرند | `client_ip` نمی‌رسد؛ `trust proxy` تنظیم نشده |
| خروج سراسری بی‌اثر است | `backchannel_logout_uri` ثبت نشده، یا `AUTH_SESSION_TRACKING=false` |
| همه ادمین‌ها ۴۰۱ می‌گیرند | جدول `AdminSession` پاک شده — ردیف نبود یعنی «باطل» |
| `unsupported logout token algorithm` در لاگ | توکن با RS256 امضا نشده (تلاش جعل یا پیکربندی اشتباه IdP) |
| حساب دو مرحله‌ای وارد نمی‌شود | این پنل مرحله «ورود کد» ندارد — بخش ۷ سند `iam-sso.md` |