Files
tehran_tala/docs/iam-sso.md
T

207 lines
12 KiB
Markdown
Raw Normal View History

2026-09-25 19:04:48 +03:30
# اتصال 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 ورود ادمین را قطع نمی‌کند.