Files
audit-web/portal_auth_flow_documentation.md
2026-08-26 14:11:37 +07:00

522 lines
16 KiB
Markdown

# Tài Liệu Luồng Xác Thực Qua Portal SSO Bên Ngoài
**Hệ thống:** CTI Web Services (Threat Intelligence Platform)
**Phiên bản:** CTI v4
**Cập nhật lần cuối:** 2026-04-22
---
## 1. Tổng Quan Kiến Trúc
Hệ thống CTI Web hỗ trợ **hai phương thức xác thực** song song:
| Phương thức | Endpoint | Mô tả |
|---|---|---|
| **RADIUS Login** | `POST /cti/login/` | Đăng nhập trực tiếp qua trang login nội bộ, xác thực với FreeRADIUS |
| **SSO Portal Auth** | `GET /cti/portal_auth/` | Đăng nhập qua cổng thông tin SSO bên ngoài bằng JWT token |
Tài liệu này tập trung vào **luồng SSO Portal Auth**.
---
## 2. Kiến Trúc Hệ Thống
```
Internet / Mạng nội bộ
┌─────────────────────────────┐
│ Nginx Reverse Proxy │
│ Host: 10.144.13.213 │
│ Port: 443 (HTTPS/TLS) │
│ → Redirect HTTP → HTTPS │
└──────────────┬──────────────┘
│ proxy_pass → http://192.168.100.100:8001/cti
┌─────────────────────────────┐
│ Django Web App (Gunicorn) │
│ Internal: 192.168.100.100 │
│ Port: 8001 │
│ Root path: /cti │
└──────────────┬──────────────┘
┌───────┴───────┐
│ │
▼ ▼
┌────────────┐ ┌──────────────────┐
│ MySQL DB │ │ SSO Server │
│ 192.168. │ │ 10.144.47.157 │
│ 100.101 │ │ HTTPS │
└────────────┘ └──────────────────┘
```
**Cấu hình Docker Network:**
- Network: `cti_network` (subnet `192.168.100.0/24`)
- Proxy container: Nginx
- Web container: Django + Gunicorn
- DB container: MySQL 9.2.0
---
## 3. Luồng Xác Thực SSO Chi Tiết
### 3.1 Sơ Đồ Luồng Tổng Quát
```mermaid
sequenceDiagram
actor User as Người dùng
participant Portal as SSO Portal<br/>(External)
participant Nginx as Nginx Proxy<br/>10.144.13.213:443
participant Django as Django App<br/>192.168.100.100:8001
participant SSOServer as SSO Validate Server<br/>10.144.47.157
participant DB as MySQL Database<br/>192.168.100.101
User->>Portal: Đăng nhập SSO Portal
Portal-->>User: JWT Token được cấp phát
Portal->>Nginx: Redirect: GET /cti/portal_auth/?jwt=<token>&host=<url>
Nginx->>Django: Forward request (X-Forwarded-For, X-Forwarded-Proto)
Django->>Django: Bước 0: Kiểm tra user đã authenticated chưa?
alt Đã đăng nhập
Django-->>User: Redirect tới host hoặc leakaccountinfo
else Chưa đăng nhập
Django->>Django: Bước 1: Kiểm tra JWT token có trong query params?
Django->>SSOServer: GET /api/auth/validate-token<br/>Authorization: Bearer <token>
SSOServer-->>Django: 200 OK + JSON payload
Django->>Django: Bước 2: Validate payload (message, exp, email, unit_id)
Django->>Django: Map SSO unit_id → Django unit_id
Django->>DB: Query ThreatIntelExceptionUser<br/>(username + unit_id)
DB-->>Django: Exception user record
Django->>DB: Query Django User table
DB-->>Django: User object
Django->>Django: Bước 3: login() với ModelBackend
Django->>Django: Lưu session (unitID, fullname, sso_authenticated)
Django-->>User: Redirect tới host hoặc leakaccountinfo
end
```
---
### 3.2 Mô Tả Chi Tiết Từng Bước
#### **Bước 0 — Kiểm Tra Trạng Thái Phiên Hiện Tại**
| Thuộc tính | Giá trị |
|---|---|
| View | `portal_auth(request)` |
| File | `threatintel/views.py` line 53 |
```
GET /cti/portal_auth/?jwt=<token>&host=<redirect_url>
```
Django kiểm tra `request.user.is_authenticated`:
- **Nếu đã đăng nhập** → Kiểm tra `host` redirect có hợp lệ không → Redirect ngay, **bỏ qua toàn bộ quá trình SSO**.
- **Nếu chưa đăng nhập** → Tiếp tục luồng SSO.
> [!NOTE]
> Nếu `host` không nằm trong `ALLOWED_HOSTS`, Django trả về `400 Bad Request` để chống **Open Redirect Attack**.
---
#### **Bước 1 — Nhận và Kiểm Tra JWT Token**
Django lấy `jwt` từ query string:
```python
token = request.GET.get("jwt")
if not token:
return HttpResponseBadRequest("Missing JWT token")
```
Nếu không có token → Trả `400 Bad Request`.
---
#### **Bước 2 — Gọi SSO Validate Token**
Django gửi HTTP request đến SSO Validation Server:
```python
resp = requests.get(
SSO_VALIDATE_URL, # https://10.144.47.157/api/auth/validate-token
headers={"Authorization": f"Bearer {token}"},
timeout=SSO_TIMEOUT, # 10 giây
verify=False # Bỏ qua SSL verification (self-signed cert)
)
```
**Kiểm tra kết quả:**
| Kết quả | Hành động |
|---|---|
| `requests.RequestException` (timeout, unreachable) | `503 Service Unavailable` |
| HTTP status ≠ 200 | `401 Unauthorized` - "Invalid or expired token" |
| JSON parse lỗi | `502 Bad Gateway` - "Invalid SSO response format" |
| `payload.message``"success"` | `401 Unauthorized` - "SSO authentication failed" |
| `data.exp` < thời gian hiện tại | `401 Unauthorized` - "Token expired" |
| `data.email` bị thiếu | `401 Unauthorized` - "Missing email in token" |
**Cấu trúc JSON response mong đợi từ SSO Server:**
```json
{
"message": "success",
"data": {
"email": "nguoidung@vnptmedia.vn",
"fullname": "Nguyễn Văn A",
"unit_id": 2,
"exp": 1745000000
}
}
```
Django tách username từ email:
```python
username = email.split('@')[0] # "nguoidung"
```
---
#### **Bước 3 — Mapping Unit ID**
SSO và Django dùng hệ thống `unit_id` riêng biệt. Django thực hiện ánh xạ:
| SSO unit_id | Django unit_id | Đơn vị |
|---|---|---|
| 1 | 1 | ANTT (Admin) |
| 2 | 4 | Fintech |
| 3 | 5 | MyTV |
| 4 | 3 | VAS |
| 5 | 2 | TSC |
| 6 | 6 | MyVNPT |
```python
django_unit_id = UNIT_ID_MAP.get(sso_unit_id)
if not django_unit_id:
return JsonResponse({"error": f"SSO unit_id {sso_unit_id} not authorized"}, status=403)
```
Nếu `sso_unit_id` không có trong bảng mapping → `403 Forbidden`.
---
#### **Bước 4 — Kiểm Tra Bảng Exception Users**
Django kiểm tra user có tồn tại trong bảng `threatintel_exception_users` với cặp `(username, unit_id)` khớp:
```python
exception_user = ThreatIntelExceptionUser.objects.filter(
username=username,
unit_id=django_unit_id
).first()
if not exception_user:
return JsonResponse({"error": "User not found in exception users table"}, status=403)
```
**Mô hình dữ liệu:**
```python
class ThreatIntelExceptionUser(models.Model):
username = models.CharField(max_length=255, primary_key=True)
unit_id = models.IntegerField()
class Meta:
db_table = 'threatintel_exception_users'
managed = False # Bảng được quản lý bên ngoài Django
```
> [!IMPORTANT]
> Bảng `threatintel_exception_users` là **danh sách trắng (whitelist)**. Chỉ những user được phép truy cập hệ thống mới có record trong bảng này. Đây là lớp kiểm soát truy cập thứ hai sau SSO.
---
#### **Bước 5 — Xác Minh User Trong Django**
Sau khi kiểm tra exception table, Django xác minh user có tồn tại và đang hoạt động:
```python
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return JsonResponse({"error": f"User '{username}' not found in system"}, status=404)
if not user.is_active:
return JsonResponse({"error": f"User '{username}' is inactive"}, status=403)
```
---
#### **Bước 6 — Đăng Nhập và Lưu Session**
Django thực hiện đăng nhập, **bỏ qua xác thực RADIUS**, sử dụng trực tiếp `ModelBackend`:
```python
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
# Lưu thông tin vào session
request.session["unitID"] = str(django_unit_id)
request.session["sso_unit_id"] = sso_unit_id
request.session["fullname"] = data.get("fullname", "")
request.session["sso_authenticated"] = True # Đánh dấu đăng nhập qua SSO
```
**Cấu hình session (settings.py):**
```python
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SESSION_COOKIE_AGE = 1800 # 30 phút
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_SAVE_EVERY_REQUEST = True
SESSION_COOKIE_HTTPONLY = True
```
---
#### **Bước 7 — Signal Logging**
Sau khi `login()` được gọi, Django tự động phát ra signal `user_logged_in`, được bắt bởi `signals.py`:
```python
@receiver(user_logged_in)
def log_login(sender, request, user, **kwargs):
ip = get_client_ip(request) # Lấy IP thực từ X-Forwarded-For
ua = request.META.get('HTTP_USER_AGENT', '')
login_logger.info(f"User '{user.username}' logged in from IP: {ip}, UA: {ua}")
```
**Lấy IP qua Reverse Proxy:**
```python
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0] # IP đầu tiên là IP thực của client
return request.META.get('REMOTE_ADDR')
```
Log được ghi vào: `logs/authenticate.log` (rotate hàng ngày, giữ tối đa 365 file).
---
#### **Bước 8 — Redirect Cuối**
```python
return redirect(host or "leakaccountinfo")
```
- Nếu có `host` trong query params → Redirect đến URL đó (sau khi đã validate).
- Nếu không có `host` → Redirect đến view `leakaccountinfo` (trang chính).
---
## 4. Cấu Hình Liên Quan
### 4.1 Nginx Proxy (default.conf)
```nginx
server {
listen 80;
server_name 10.144.13.213 sec.vnptmedia.vn;
return 301 https://$host/cti; # Redirect HTTP → HTTPS
}
server {
listen 443 ssl;
server_name 10.144.13.213 sec.vnptmedia.vn;
ssl_certificate /etc/ssl/certs/ctip.crt;
ssl_certificate_key /etc/ssl/certs/ctip.key;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 4G;
location /cti {
limit_req zone=ip burst=12 delay=8;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://192.168.100.100:8001/cti;
}
}
```
### 4.2 Django Settings Liên Quan
```python
# Nhận biết HTTPS qua Reverse Proxy
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True
USE_X_FORWARDED_PORT = True
# SSO Configuration
SSO_VALIDATE_URL = 'https://10.144.47.157/api/auth/validate-token'
SSO_TIMEOUT = 10 # giây
# Authentication Backends
AUTHENTICATION_BACKENDS = [
'axes.backends.AxesStandaloneBackend', # Chống brute-force
'threatintel.backends.RemoteFreeRADIUSBackend', # RADIUS (cho login thông thường)
'django.contrib.auth.backends.ModelBackend', # Django default (cho SSO)
]
```
### 4.3 Environment Variables (.env)
```ini
SSO_VALIDATE_URL = 'https://10.144.47.157/api/auth/validate-token'
SSO_TIMEOUT = 10
FREERADIUS_SERVER = 10.144.13.184
FREERADIUS_SECRET = 'vNpt@1234'
```
---
## 5. URL Endpoint
| URL | Method | Decorator | Mô tả |
|---|---|---|---|
| `/cti/portal_auth/` | `GET` | `@csrf_exempt` | Entry point SSO authentication |
| `/cti/login/` | `GET/POST` | — | Trang đăng nhập RADIUS thông thường |
| `/cti/logout/` | `GET` | `@login_required` | Đăng xuất |
| `/cti/leakaccountinfo/` | `GET/POST` | `@login_required` | Trang chính sau đăng nhập |
> [!NOTE]
> `portal_auth` được đánh dấu `@csrf_exempt` vì request đến từ SSO Portal bên ngoài không mang CSRF token của ứng dụng. Việc bảo mật được thực hiện thông qua JWT token validation thay thế.
---
## 6. Phân Tích Bảo Mật
### 6.1 Các Lớp Bảo Vệ
```
Layer 1: HTTPS/TLS (Nginx) — Mã hóa transport
Layer 2: JWT Token Validation (SSO Server) — Xác thực danh tính
Layer 3: Token Expiration Check — Chống replay attack
Layer 4: Unit ID Authorization — Kiểm soát phân quyền tổ chức
Layer 5: Exception User Whitelist — Kiểm soát danh sách trắng
Layer 6: Django User Active Check — Kiểm tra trạng thái tài khoản
Layer 7: Open Redirect Protection — Chống redirect độc hại
Layer 8: Rate Limiting (Nginx) — Chống tấn công brute-force
Layer 9: Django Axes — Chống brute-force đăng nhập thường
```
### 6.2 Điểm Cần Lưu Ý
> [!WARNING]
> `verify=False` trong lệnh gọi SSO Validate URL bỏ qua kiểm tra SSL certificate của SSO Server (do server dùng self-signed cert). Cần cân nhắc thêm CA certificate vào trust store trong môi trường production thực tế.
> [!WARNING]
> Bảng `threatintel_exception_users` được khai báo với `managed = False`, nghĩa là Django **không tự tạo/migrate** bảng này. Bảng phải được tạo và duy trì thủ công trong MySQL.
---
## 7. Xử Lý Lỗi Tổng Hợp
| Tình huống | HTTP Status | Thông báo lỗi |
|---|---|---|
| `host` redirect không hợp lệ | `400` | "Invalid redirect URL" |
| Thiếu JWT token | `400` | "Missing JWT token" |
| SSO Server không phản hồi | `503` | "SSO service unreachable: ..." |
| Token không hợp lệ / đã hết hạn (HTTP) | `401` | "Invalid or expired token" |
| SSO trả JSON lỗi format | `502` | "Invalid SSO response format" |
| SSO trả `message` ≠ success | `401` | "SSO authentication failed" |
| Token đã hết hạn (exp check) | `401` | "Token expired" |
| Thiếu email trong token | `401` | "Missing email in token" |
| unit_id không được ánh xạ | `403` | "SSO unit_id X not authorized for this system" |
| User không có trong exception table | `403` | "User 'X' with unit_id Y not found in exception users table" |
| User không tồn tại trong Django | `404` | "User 'X' not found in system" |
| User bị vô hiệu hóa | `403` | "User 'X' is inactive" |
| Lỗi không xác định | `500` | "Authentication failed: ..." |
---
## 8. So Sánh Hai Luồng Xác Thực
| Tiêu chí | RADIUS Login | SSO Portal Auth |
|---|---|---|
| **Entry point** | `POST /cti/login/` | `GET /cti/portal_auth/` |
| **Phương thức xác thực** | Username/Password → FreeRADIUS | JWT Token → SSO Validate API |
| **Backend Django** | `RemoteFreeRADIUSBackend` | `ModelBackend` (trực tiếp) |
| **CSRF** | Bắt buộc | Exempt (`@csrf_exempt`) |
| **Thông tin session** | `unitID` | `unitID`, `sso_unit_id`, `fullname`, `sso_authenticated=True` |
| **Kiểm tra whitelist** | Có (exception_users) | Có (exception_users) |
| **Brute-force protection** | Django Axes + RADIUS timeout | Không áp dụng (token-based) |
| **Khi đã authenticated** | Redirect về leakaccountinfo | Redirect về host hoặc leakaccountinfo |
---
## 9. Sơ Đồ Quyết Định (Decision Tree)
```
GET /cti/portal_auth/?jwt=TOKEN&host=URL
Đã authenticated?
┌──Yes──┐
│ │
▼ ▼
host hợp lệ? Redirect leakaccountinfo
No → 400 Bad Request
Yes → Redirect host
No (chưa auth)
jwt có trong params?
No → 400 Missing JWT
Yes
host hợp lệ? (nếu có)
No → 400 Invalid redirect
Yes/Không có host
Gọi SSO Validate API
Lỗi kết nối → 503
HTTP != 200 → 401
JSON lỗi → 502
message != success → 401
exp expired → 401
email thiếu → 401
Map unit_id?
Không có mapping → 403
Có mapping
Trong exception_users?
Không → 403
User tồn tại trong Django?
Không → 404
User đang active?
Không → 403
login() với ModelBackend
Lưu session
Signal log
Redirect host hoặc leakaccountinfo ✅
```