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

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 (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

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:

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_usersdanh 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ó 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)

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_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 ✅