Files
2026-10-18 22:45:47 +03:30

204 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# نحوه ارتباط gold-bank با سامانه هویت متمرکز (IAM)
> ورود مشترک بدون پرش مرورگر به `/auth/bootstrap` کامل نمی‌شود. اگر این متن جایی
> گفته مرورگر هرگز به IAM نمی‌رود، آن جمله کهنه است. قاعدهٔ فعلی برای هر اپ:
> [`iam/docs/onboarding-new-system.md`](../../iam/docs/onboarding-new-system.md).
این سند **مسیر داده** را توضیح می‌دهد: چه کسی، چه زمانی، چه چیزی را به کجا
می‌فرستد و اگر آن طرف جواب نداد چه اتفاقی می‌افتد.
برای پیکربندی و فعال‌سازی: [`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` |