v2.3
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
# 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
|
||||
│
|
||||
Có
|
||||
▼
|
||||
User tồn tại trong Django?
|
||||
│
|
||||
Không → 404
|
||||
│
|
||||
Có
|
||||
▼
|
||||
User đang active?
|
||||
│
|
||||
Không → 403
|
||||
│
|
||||
Có
|
||||
▼
|
||||
login() với ModelBackend
|
||||
Lưu session
|
||||
Signal log
|
||||
│
|
||||
▼
|
||||
Redirect host hoặc leakaccountinfo ✅
|
||||
```
|
||||
Reference in New Issue
Block a user