16 KiB
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(subnet192.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
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
hostredirect 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
hostkhông nằm trongALLOWED_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:
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:
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:
{
"message": "success",
"data": {
"email": "nguoidung@vnptmedia.vn",
"fullname": "Nguyễn Văn A",
"unit_id": 2,
"exp": 1745000000
}
}
Django tách username từ email:
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 |
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:
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:
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_userslà 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:
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:
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):
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:
@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:
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
return redirect(host or "leakaccountinfo")
- Nếu có
hosttrong query params → Redirect đến URL đó (sau khi đã validate). - Nếu không có
host→ Redirect đến viewleakaccountinfo(trang chính).
4. Cấu Hình Liên Quan
4.1 Nginx Proxy (default.conf)
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
# 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)
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_exemptvì 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=Falsetrong 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ớimanaged = 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 ✅