# نحوه ارتباط 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` |