Feat add iam auth
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# نحوه ارتباط 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` |
|
||||
Reference in New Issue
Block a user