# 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
(External) participant Nginx as Nginx Proxy
10.144.13.213:443 participant Django as Django App
192.168.100.100:8001 participant SSOServer as SSO Validate Server
10.144.47.157 participant DB as MySQL Database
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=&host= 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
Authorization: Bearer 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
(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=&host= ``` 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 ✅ ```