2026-09-25 19:04:48 +03:30
|
|
|
|
# اتصال gold-bank به سامانه هویت متمرکز (Central IAM)
|
|
|
|
|
|
|
|
|
|
|
|
> **نحوه ارتباط (مسیر داده، اندپوینتها، رفتار در خطا):** [`iam-connection.md`](iam-connection.md)
|
|
|
|
|
|
> **مرجع تصویری API سامانه هویت:** `<آدرس IAM>/docs`
|
|
|
|
|
|
|
|
|
|
|
|
این سند توضیح میدهد ورود ادمین چطور میتواند بهجای رمز محلی، از سامانه هویت
|
|
|
|
|
|
سازمانی اعتبارسنجی شود — **بدون هیچ تغییری در رابط کاربری**.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ۱. خلاصه تصمیم
|
|
|
|
|
|
|
|
|
|
|
|
| موضوع | تصمیم |
|
|
|
|
|
|
| --- | --- |
|
2026-10-07 16:45:36 +03:30
|
|
|
|
| رابط کاربری | فرم ورود همان است. با IAM روشن، یک پرش بیصدای `prompt=none` و صفحه `/auth/iam/callback` اضافه شده تا ورود مشترک با همکار کار کند |
|
2026-09-25 19:04:48 +03:30
|
|
|
|
| جای اتصال | فقط همان یک نقطهای که رمز بررسی میشد (`AuthService.login`) |
|
|
|
|
|
|
| پیشفرض | خاموش (`IAM_ENABLED=false`) — رفتار سامانه دقیقاً مثل امروز |
|
|
|
|
|
|
| در دسترس نبودن IAM | بازگشت خودکار به بررسی رمز محلی ⇒ ورود ادمینها قطع نمیشود |
|
|
|
|
|
|
| تغییر دیتابیس | دو جدول **جدید** (`AdminSession`، `AppSession`) برای ابطال نشست؛ هیچ جدول موجودی تغییر نکرده |
|
|
|
|
|
|
| ورود مشتری (OTP پیامکی) | کد همچنان اینجا تولید و بررسی میشود؛ نتیجه به IAM اعلام میگردد تا نشست و خروج سراسری مرکزی شود |
|
|
|
|
|
|
|
|
|
|
|
|
جریان ورود ادمین پس از فعال شدن:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
پنل ادمین ──POST /api/admin/auth/login──▶ API (NestJS)
|
|
|
|
|
|
(همان فرم و همان بدنه) │
|
|
|
|
|
|
├─ IAM روشن؟ ──خیر──▶ bcrypt محلی (رفتار فعلی)
|
|
|
|
|
|
│
|
|
|
|
|
|
└─بله─▶ POST {IAM}/api/v1/auth/login
|
|
|
|
|
|
│
|
|
|
|
|
|
┌─────────────────┴─────────────────┐
|
|
|
|
|
|
تأیید شد رد/خطا/در دسترس نبود
|
|
|
|
|
|
│ │
|
|
|
|
|
|
صدور همان JWT محلی fallback ⇒ bcrypt محلی
|
|
|
|
|
|
exclusive ⇒ رد ورود
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
پاسخ اندپوینت در هر دو حالت **دقیقاً یکسان** است (`api_token`, `roles`, …)،
|
|
|
|
|
|
بنابراین پنل ادمین هیچ تفاوتی نمیبیند.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ۲. فایلها
|
|
|
|
|
|
|
|
|
|
|
|
| مسیر | نقش |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| `apps/api/src/modules/iam/iam.service.ts` | ارتباط با IAM و نگاشت خطاها |
|
|
|
|
|
|
| `apps/api/src/modules/iam/iam-logout.service.ts` | تأیید توکن خروج سراسری با JWKS (بدون وابستگی جدید) |
|
|
|
|
|
|
| `apps/api/src/modules/iam/iam.controller.ts` | دریافت اعلان خروج سراسری |
|
|
|
|
|
|
| `apps/api/src/modules/iam/iam.module.ts` | ماژول Global (تا زمان روشن شدن، بیاثر) |
|
|
|
|
|
|
| `apps/api/src/modules/sessions/admin-session.service.ts` | نشستهای قابل ابطال (jti) |
|
|
|
|
|
|
| `apps/api/src/modules/sessions/admin-session.module.ts` | ماژول Global نشستها |
|
|
|
|
|
|
| `prisma/migrations/20260925120000_admin_session/` | جدول `AdminSession` |
|
|
|
|
|
|
| `apps/api/src/modules/**/*.spec.ts` | تست هر چهار سرویس بالا |
|
|
|
|
|
|
|
|
|
|
|
|
تغییر روی کد موجود، حداقلی است:
|
|
|
|
|
|
|
|
|
|
|
|
- `auth.service.ts` — یک `bcrypt.compare` به متد `verifyPassword` منتقل شد و توکن
|
|
|
|
|
|
صادرشده (در صورت روشن بودن ردیابی نشست) ثبت میشود
|
|
|
|
|
|
- `auth.controller.ts` — افزودن `@Ip()` و `user-agent`
|
|
|
|
|
|
- `jwt.strategy.ts` — یک بررسی «نشست باطل نشده باشد» (در حالت خاموش، بدون کوئری)
|
|
|
|
|
|
- `admin-users.service.ts` — غیرفعال کردن حساب یا تغییر رمز، نشستها را میبندد
|
|
|
|
|
|
- `app.module.ts` — ثبت `IamModule` و `AdminSessionModule`
|
|
|
|
|
|
- `prisma/schema.prisma` — فقط مدل جدید `AdminSession` (+ رابطه روی `AdminUser`)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ۳. فعالسازی
|
|
|
|
|
|
|
|
|
|
|
|
### گام ۱ — ثبت این سامانه بهعنوان کلاینت در IAM
|
|
|
|
|
|
|
|
|
|
|
|
باید کلاینت از نوع `confidential` و **`is_trusted: true`** باشد؛ فقط چنین
|
|
|
|
|
|
کلاینتی اجازه «ورود واگذارشده» دارد.
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
curl -X POST https://auth.example.com/api/v1/admin/clients \
|
|
|
|
|
|
-H "Authorization: Bearer $IAM_ADMIN_TOKEN" \
|
|
|
|
|
|
-H 'content-type: application/json' \
|
|
|
|
|
|
-d '{
|
|
|
|
|
|
"client_id": "goldbank_api",
|
|
|
|
|
|
"client_name": "سامانه بانک طلا",
|
|
|
|
|
|
"client_type": "confidential",
|
|
|
|
|
|
"redirect_uris": ["https://goldbank.example.com/unused"],
|
|
|
|
|
|
"allowed_scopes": ["openid", "profile", "email"],
|
|
|
|
|
|
"is_trusted": true
|
|
|
|
|
|
}'
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-10-07 16:45:36 +03:30
|
|
|
|
> `redirect_uris` آدرس callback فرانت است و باید دقیقاً با صفحهای که مرورگر
|
|
|
|
|
|
> باز میکند یکی باشد. برای اپ کاربر و پنل ادمین، و برای دولوپ و پروداکشن،
|
|
|
|
|
|
> هر کدام یک خط جدا. جزئیات در بخش «ورود مشترک با همکار».
|
2026-09-25 19:04:48 +03:30
|
|
|
|
|
|
|
|
|
|
### گام ۲ — تنظیم `.env`
|
|
|
|
|
|
|
|
|
|
|
|
```dotenv
|
|
|
|
|
|
IAM_ENABLED=true
|
|
|
|
|
|
IAM_ISSUER=https://auth.example.com
|
|
|
|
|
|
IAM_CLIENT_ID=goldbank_api
|
|
|
|
|
|
IAM_CLIENT_SECRET=<راز دریافتی>
|
|
|
|
|
|
IAM_TENANT_SLUG=montzar
|
|
|
|
|
|
IAM_LOGIN_DELEGATION=fallback
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
همین. سرویس را ریاستارت کنید؛ هیچ مایگریشنی لازم نیست.
|
|
|
|
|
|
|
|
|
|
|
|
### گام ۳ — بازگشت به عقب
|
|
|
|
|
|
|
|
|
|
|
|
`IAM_ENABLED=false` و ریاستارت. تمام.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ۴. حالتهای `IAM_LOGIN_DELEGATION`
|
|
|
|
|
|
|
|
|
|
|
|
| مقدار | رفتار |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| `off` | IAM اصلاً در ورود دخالت نمیکند |
|
|
|
|
|
|
| `fallback` (پیشفرض) | اول IAM؛ اگر کاربر آنجا نبود یا سرویس در دسترس نبود، رمز محلی بررسی میشود — مناسب مهاجرت تدریجی |
|
|
|
|
|
|
| `exclusive` | فقط IAM؛ رمز محلی پذیرفته نمیشود و خرابی IAM یعنی توقف ورود |
|
|
|
|
|
|
|
|
|
|
|
|
**پیشنهاد مهاجرت:** با `fallback` شروع کنید، بعد از انتقال همه ادمینها به IAM
|
|
|
|
|
|
و اطمینان از پایداری، به `exclusive` بروید.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ۵. نکات عملیاتی
|
|
|
|
|
|
|
|
|
|
|
|
### محدودیت نرخ
|
|
|
|
|
|
|
|
|
|
|
|
IAM روی «IP» و «هویت» محدودیت نرخ دارد. چون درخواستها از سرور API میآیند،
|
|
|
|
|
|
IP کاربر نهایی با فیلد `client_ip` فرستاده میشود؛ در غیر این صورت پس از چند
|
|
|
|
|
|
ورود، کل پنل ادمین ۴۲۹ میگرفت.
|
|
|
|
|
|
|
|
|
|
|
|
اگر API پشت پراکسی است، مطمئن شوید Nest `trust proxy` دارد وگرنه `@Ip()`
|
|
|
|
|
|
همیشه IP پراکسی را میدهد و همان مشکل برمیگردد.
|
|
|
|
|
|
|
|
|
|
|
|
### احراز دو مرحلهای
|
|
|
|
|
|
|
|
|
|
|
|
فرم ورود این پنل فقط «موبایل + رمز» است و **هیچ مرحلهی ورود کدی ندارد**؛ پس جایی
|
|
|
|
|
|
برای گرفتن کد عامل دوم وجود ندارد و این مسیر نمیتواند آن را تکمیل کند. ورود چنین
|
|
|
|
|
|
حسابی با پیام روشن رد میشود (نه خطای مرموز). سه گزینه:
|
|
|
|
|
|
|
|
|
|
|
|
1. برای حسابهایی که از این پنل وارد میشوند 2FA را در IAM فعال نکنید (سادهترین)؛
|
|
|
|
|
|
2. مرحلهی «ورود کد» را به همین فرم اضافه کنید؛ آنوقت میتوان مثل سامانه «همکار»
|
|
|
|
|
|
کد را واسطه کرد یا با کانال خودِ سامانه فرستاد
|
|
|
|
|
|
(`/api/v1/auth/mfa/verify` و `/api/v1/auth/mfa/delegate` در IAM آمادهاند)؛
|
|
|
|
|
|
3. یا جریان مرورگری SSO را برای این پنل به کار بگیرید که خودش صفحه کد را میدهد.
|
|
|
|
|
|
|
|
|
|
|
|
> در سامانه «همکار» این محدودیت وجود ندارد، چون ورود کاربر عادی آنجا از قبل مرحلهی
|
|
|
|
|
|
> «ورود کد» (پیامک) دارد و همان مرحله برای عامل دوم استفاده میشود.
|
|
|
|
|
|
|
|
|
|
|
|
### ابطال نشست
|
|
|
|
|
|
|
|
|
|
|
|
توکن ادمین خودبسنده و ۱۲ ساعته است، پس تا پیش از این **هیچچیز** نمیتوانست نشست را
|
|
|
|
|
|
زودتر تمام کند: نه غیرفعال کردن حساب، نه خروج سراسری در IAM. حالا هر توکن صادرشده
|
|
|
|
|
|
یک `jti` دارد که در جدول `AdminSession` ثبت میشود و درخواست بعدیِ نشستِ باطلشده
|
|
|
|
|
|
۴۰۱ میگیرد.
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
IAM: ابطال نشست / خروج سراسری
|
|
|
|
|
|
│ logout_token (RS256، امضاشده)
|
|
|
|
|
|
▼
|
|
|
|
|
|
POST /api/iam/backchannel-logout ← این آدرس را در IAM بهعنوان
|
|
|
|
|
|
│ backchannel_logout_uri کلاینت ثبت کنید
|
|
|
|
|
|
├─ تأیید امضا با JWKS، بررسی iss/aud/events و نبودن nonce
|
|
|
|
|
|
└─ ابطال نشستهای متناظر (sid، یا همه نشستهای sub)
|
|
|
|
|
|
│
|
|
|
|
|
|
▼
|
|
|
|
|
|
درخواست بعدی با آن توکن ⇒ ۴۰۱
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
کلید تنظیم: `AUTH_SESSION_TRACKING`
|
|
|
|
|
|
|
|
|
|
|
|
| مقدار | رفتار |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| خالی (پیشفرض) | همراه `IAM_ENABLED` روشن/خاموش میشود — تا ابطال مرکزی بیاثر نماند |
|
|
|
|
|
|
| `true` | روشن، حتی بدون IAM (ابطال محلی: غیرفعال کردن حساب، تغییر رمز) |
|
|
|
|
|
|
| `false` | خاموش؛ دقیقاً رفتار قبلی (بدون `jti`، بدون کوئری اضافه) و ابطال مرکزی بیاثر |
|
|
|
|
|
|
|
|
|
|
|
|
نکتههای عملی:
|
|
|
|
|
|
|
|
|
|
|
|
- توکنهایی که **پیش از** روشن کردن این قابلیت صادر شدهاند `jti` ندارند و تا انقضا
|
|
|
|
|
|
معتبر میمانند؛ یعنی روشن کردن این کلید کسی را یکباره بیرون نمیاندازد.
|
|
|
|
|
|
- ردیف نشستِ گمشده «باطل» تلقی میشود، پس پاک کردن جدول یعنی خروج همه.
|
|
|
|
|
|
- اندپوینت خروج سراسری عمداً بدون احراز هویت است (اعتبارش از امضای توکن میآید) و
|
|
|
|
|
|
همیشه ۲۰۰ برمیگرداند تا IdP بینهایت تلاش مجدد نکند؛ دلیل رد شدن در لاگ میآید.
|
|
|
|
|
|
- تنها تغییر شِما یک جدول **جدید** است؛ هیچ جدول موجودی ستون نگرفته. مایگریشن:
|
|
|
|
|
|
`pnpm --filter @gold-bank/api exec prisma migrate deploy`.
|
|
|
|
|
|
|
|
|
|
|
|
### لاگ امنیتی
|
|
|
|
|
|
|
|
|
|
|
|
هر ورود واگذارشده در IAM با IP واقعی کاربر و `client_id` این سامانه ثبت
|
|
|
|
|
|
میشود، پس گزارشهای امنیتی متمرکز کامل باقی میمانند.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-10-07 16:45:36 +03:30
|
|
|
|
## ۶. ورود مشترک با همکار
|
|
|
|
|
|
|
|
|
|
|
|
کوکی نشست روی خود IAM است (`iam.montest.ir`) و بین دامنهها مشترک نمیشود.
|
|
|
|
|
|
ورود در یک سامانه فقط وقتی سامانهٔ دیگر را هم باز میکند که:
|
|
|
|
|
|
|
|
|
|
|
|
1. ورود محلی بلیت مرورگر گرفته باشد و فرانت به `iamBootstrapUrl` رفته باشد.
|
|
|
|
|
|
2. سامانهٔ دوم بدون توکن محلی، `prompt=none` بزند و callback **همان صفحه** باشد.
|
|
|
|
|
|
3. همان شماره موبایل در سامانهٔ دوم از قبل حساب داشته باشد. IAM حساب gold-bank
|
|
|
|
|
|
یا همکار را نمیسازد.
|
|
|
|
|
|
|
|
|
|
|
|
کلاینت gold-bank جدا از `hamkar_server` است (`goldbank_api`). آدرسها عیناً،
|
|
|
|
|
|
هر کدام یک خط در فرم IAM و با کاما در env:
|
|
|
|
|
|
|
|
|
|
|
|
| نقش | callback (آدرسهای بازگشت) | بعد از خروج |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| اپ کاربر | `IAM_REDIRECT_URI` و `IAM_REDIRECT_URIS` | `IAM_POST_LOGOUT_REDIRECT_URI` و `IAM_ALLOWED_RETURN_URLS` |
|
|
|
|
|
|
| پنل ادمین | `IAM_ADMIN_REDIRECT_URI` و `IAM_ADMIN_REDIRECT_URIS` | `IAM_ADMIN_POST_LOGOUT_REDIRECT_URI` و `IAM_ADMIN_ALLOWED_RETURN_URLS` |
|
|
|
|
|
|
|
|
|
|
|
|
فرانت آدرس خودش را میفرستد (`origin` + `/auth/iam/callback`؛ پنل شعبه پیشوند
|
|
|
|
|
|
`/branch` دارد). اگر آن رشته در فهرست نباشد، بررسی بیصدا انجام نمیشود و
|
|
|
|
|
|
صفحهٔ لاگین محلی میماند. خروج هم اگر مقصدش ثبت نشده باشد به IAM نمیرود.
|
|
|
|
|
|
|
|
|
|
|
|
وقتی `IAM_ENABLED` خاموش است، `sso/start` مقدار `enabled: false` میدهد و
|
|
|
|
|
|
`sso/logout-url` مقدار `logout_url: null`. هیچ پیام خطایی برای خروج لازم نیست.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## ۷. تست
|
2026-09-25 19:04:48 +03:30
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
cd apps/api
|
|
|
|
|
|
pnpm test # ۱۱۵ تست، بدون دیتابیس و بدون شبکه
|
|
|
|
|
|
pnpm test -- iam-logout.service # تأیید توکن خروج سراسری با کلید RSA واقعی
|
|
|
|
|
|
pnpm test -- admin-session # نشستهای قابل ابطال
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
تستها بدون دیتابیس و بدون شبکه اجرا میشوند و مهمترین چیزی که تضمین میکنند
|
|
|
|
|
|
این است: **با خاموش بودن قابلیت، هیچ درخواستی به IAM نمیرود و ورود دقیقاً مثل
|
|
|
|
|
|
قبل کار میکند**؛ و با روشن بودن، خرابی IAM ورود ادمین را قطع نمیکند.
|