207 lines
12 KiB
Markdown
207 lines
12 KiB
Markdown
# اتصال gold-bank به سامانه هویت متمرکز (Central IAM)
|
||||
|
|
|
|||
|
|
> **نحوه ارتباط (مسیر داده، اندپوینتها، رفتار در خطا):** [`iam-connection.md`](iam-connection.md)
|
|||
|
|
> **مرجع تصویری API سامانه هویت:** `<آدرس IAM>/docs`
|
|||
|
|
|
|||
|
|
این سند توضیح میدهد ورود ادمین چطور میتواند بهجای رمز محلی، از سامانه هویت
|
|||
|
|
سازمانی اعتبارسنجی شود — **بدون هیچ تغییری در رابط کاربری**.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۱. خلاصه تصمیم
|
|||
|
|
|
|||
|
|
| موضوع | تصمیم |
|
|||
|
|
| --- | --- |
|
|||
|
|
| رابط کاربری | **هیچ تغییری نمیکند**؛ نه دکمه SSO، نه ریدایرکت، نه صفحه جدید |
|
|||
|
|
| جای اتصال | فقط همان یک نقطهای که رمز بررسی میشد (`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
|
|||
|
|
}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> `redirect_uris` در این حالت استفاده نمیشود (جریان مرورگری نداریم) ولی
|
|||
|
|
> IAM آن را اجباری میداند؛ یک آدرس معتبر بگذارید.
|
|||
|
|
|
|||
|
|
### گام ۲ — تنظیم `.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` این سامانه ثبت
|
|||
|
|
میشود، پس گزارشهای امنیتی متمرکز کامل باقی میمانند.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۶. تست
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd apps/api
|
|||
|
|
pnpm test # ۱۱۵ تست، بدون دیتابیس و بدون شبکه
|
|||
|
|
pnpm test -- iam-logout.service # تأیید توکن خروج سراسری با کلید RSA واقعی
|
|||
|
|
pnpm test -- admin-session # نشستهای قابل ابطال
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
تستها بدون دیتابیس و بدون شبکه اجرا میشوند و مهمترین چیزی که تضمین میکنند
|
|||
|
|
این است: **با خاموش بودن قابلیت، هیچ درخواستی به IAM نمیرود و ورود دقیقاً مثل
|
|||
|
|
قبل کار میکند**؛ و با روشن بودن، خرابی IAM ورود ادمین را قطع نمیکند.
|