Files
hedena_gold/docs/iam-connection.md
s.esmaeili ea86636027 Initial commit: Hedena Gold monorepo.
First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
2026-10-15 17:19:10 +03:30

12 KiB
Raw Permalink Blame History

نحوه ارتباط gold-bank با سامانه هویت متمرکز (IAM)

این سند مسیر داده را توضیح می‌دهد: چه کسی، چه زمانی، چه چیزی را به کجا می‌فرستد و اگر آن طرف جواب نداد چه اتفاقی می‌افتد.

برای پیکربندی و فعال‌سازی: 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 ناموفق ورود انجام می‌شود (فقط هشدار لاگ) — بدترین حالت: نشستی که زودتر قابل ابطال نیست همان
توکن خروج نامعتبر ۲۰۰ + هشدار لاگ؛ هیچ نشستی بسته نمی‌شود همان

۸. بررسی سریع اتصال

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