200 lines
12 KiB
Markdown
200 lines
12 KiB
Markdown
# نحوه ارتباط gold-bank با سامانه هویت متمرکز (IAM)
|
||||
|
|
|
|||
|
|
این سند **مسیر داده** را توضیح میدهد: چه کسی، چه زمانی، چه چیزی را به کجا
|
|||
|
|
میفرستد و اگر آن طرف جواب نداد چه اتفاقی میافتد.
|
|||
|
|
|
|||
|
|
برای پیکربندی و فعالسازی: [`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` |
|