CHƯƠNG 09 · AUTH · ~120 phút

Cookies, Sessions,
JWT, OAuth

Authentication & Authorization. Phỏng vấn backend gần 100% sẽ hỏi: "Session vs JWT khác nhau?", "OAuth flow?", "JWT lưu ở localStorage hay cookie?". Học chương này, bạn nắm 4 cơ chế phổ biến + biết khi nào dùng cái nào.

9.1 Authentication vs Authorization

Authentication (AuthN)

"Bạn là ai?" — verify identity (login với username/password, biometric, ...)

Authorization (AuthZ)

"Bạn được phép làm gì?" — check permission (admin? owner of this resource?)

Status code phân biệt: 401 Unauthorized = AuthN fail (chưa login). 403 Forbidden = AuthZ fail (đã login nhưng không có quyền).

9.3 Session-based Authentication

Cách truyền thống — server lưu state.

Flow

  1. User login với username + password → server verify
  2. Server tạo session ID (random string), lưu vào DB/Redis: session_id → user_id
  3. Server gửi Set-Cookie: session=<session_id>; HttpOnly; Secure
  4. Mỗi request tiếp theo, browser gửi cookie
  5. Server đọc session_id → lookup DB → biết user là ai
  6. Logout: server xoá session khỏi DB → cookie cũ không còn valid
Browser Server Session Store (Redis/DB) ─────── ────── ──────────────────────── POST /login {user, pass} ──────────▶ Verify pass ✓ Tạo session_id ─────▶ Store: id123 → user42 (TTL 24h) ◀─── Set-Cookie: session=id123 ─── (HttpOnly, Secure, SameSite=Lax) GET /profile ────────────────────▶ Cookie: session=id123 Lookup id123 ───────▶ Returns: user42 Load user42 ◀─── 200 OK profile ──── POST /logout ────────────────────▶ Delete id123 ───────▶ Removed ◀─── Set-Cookie: session=; Max-Age=0 ───

Đặc điểm

  • Stateful: server lưu session → cần Redis/DB
  • Logout dễ: xoá session record → cookie cũ không dùng được
  • Scaling: cần shared session store giữa các server (sticky session hoặc Redis)
  • Cookie tự gửi: browser lo, dev không phải code

9.4 JWT — JSON Web Token

JWT (RFC 7519) — token chứa thông tin user + chữ ký. Server không cần lookup DB — verify chữ ký là tin được.

Cấu trúc

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiI0MiIsIm5hbWUiOiJBbGljZSIsImV4cCI6MTcwOX0 . dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
   ↑                                       ↑                                                              ↑
   Header (base64url JSON)                  Payload (base64url JSON)                                       Signature
          

3 phần ngăn cách bởi .. Header và payload là JSON base64url-encoded — đọc được, KHÔNG mã hoá. Signature ngăn tampering.

// Header
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload (claims)
{
  "sub": "42",            // subject (user ID)
  "name": "Alice",
  "role": "admin",
  "iat": 1700000000,      // issued at
  "exp": 1700003600       // expires at (1 hour later)
}

// Signature = HMACSHA256(base64(header) + "." + base64(payload), secret)

Verify

Server có cùng secret. Recompute signature từ header.payload + secret. Nếu match → token chưa bị sửa, claims là thật.

Algorithms

  • HS256 (HMAC + SHA-256): symmetric — cùng secret cho ký và verify. Nhanh, đơn giản.
  • RS256 (RSA + SHA-256): asymmetric — private key ký, public key verify. Tốt cho microservice (chỉ auth service có private key).
  • ES256 (ECDSA): tương tự RS256 nhưng key nhỏ hơn, nhanh hơn.

Lưu ý

  • Không bỏ secret nhỏ vào JWT (vì payload đọc được)
  • Phải set exp — JWT không tự revoke được, nếu không expire sẽ "sống mãi"
  • Verify alg — đừng accept alg: none (lỗ hổng kinh điển)

Code Node.js

const jwt = require('jsonwebtoken');

// Sign
const token = jwt.sign(
  { sub: 42, name: 'Alice', role: 'admin' },
  process.env.JWT_SECRET,
  { expiresIn: '1h', algorithm: 'HS256' }
);

// Verify
try {
  const payload = jwt.verify(token, process.env.JWT_SECRET);
  console.log(payload.sub);
} catch (err) {
  if (err.name === 'TokenExpiredError') { /* refresh */ }
  if (err.name === 'JsonWebTokenError') { /* invalid */ }
}

9.5 Session vs JWT — câu phỏng vấn kinh điển

Tiêu chíSessionJWT
StateStateful (server lưu)Stateless (server không cần lưu)
Lookup mỗi requestCó (DB/Redis)Không (chỉ verify signature)
LogoutDễ (xoá session)Khó (token vẫn valid đến exp)
Revoke 1 userDễCần blocklist hoặc đổi secret
ScaleCần shared storeKhông cần — verify cục bộ
MicroservicePhức tạp (chia sẻ session)Tốt (verify với public key)
Mobile/native clientCookie không tự gửiHeader Authorization dễ
Token sizeNhỏ (chỉ session ID)Lớn hơn (chứa claims)

Khi nào Session?

  • Web app đơn giản, monolith
  • Cần revoke ngay lập tức (banking, admin tool)
  • Có sẵn Redis/DB

Khi nào JWT?

  • Microservice — auth service ký, các service khác verify với public key
  • Mobile app, SPA — không cookie
  • Cross-domain auth (SSO partial)
  • Stateless API — không muốn DB lookup mỗi request
⚠️ JWT KHÔNG phải session thay thế tự động

JWT phổ biến nhưng có nhiều pitfall: revoke khó, lưu trữ nguy hiểm (XSS lấy được), size lớn, cần manage secret. Nhiều project dùng JWT chỉ vì "trendy" — thực ra session đủ tốt.

JWT lưu ở đâu?

  • Cookie HttpOnly + Secure + SameSite: an toàn nhất, JS không lấy được, browser tự gửi. Nhưng vẫn có CSRF risk (giảm với SameSite).
  • localStorage: tiện nhưng XSS lấy được → nguy hiểm. Tránh.
  • In-memory: an toàn nhất nhưng mất khi reload trang.

9.6 Access Token + Refresh Token

JWT exp ngắn (15 phút) thì user phải login lại liên tục. Exp dài (30 ngày) thì nguy hiểm khi lộ. Giải pháp: 2 token.

  • Access token (JWT): exp ngắn (15-60 phút), gửi mỗi API request
  • Refresh token: exp dài (7-30 ngày), chỉ dùng để lấy access token mới. Lưu DB (có thể revoke).

Flow

  1. Login → server trả access (15m) + refresh (7d)
  2. App gọi API với access token
  3. Access expired → app dùng refresh token gọi /refresh → nhận access token mới
  4. Refresh expired hoặc revoked → user phải login lại

Refresh token rotation

Tăng security: mỗi lần dùng refresh token, server cấp refresh token MỚI và invalidate cái cũ. Nếu attacker steal refresh + dùng → user dùng → server thấy 2 lần dùng cùng refresh → biết bị compromise → revoke tất cả.

9.7 OAuth 2.0 — login bằng Google/Facebook

OAuth 2.0 (RFC 6749) — protocol cho user của 1 service (Google) cấp quyền cho 1 service khác (your app) mà không cần share password.

4 actor

  • Resource Owner: user (bạn)
  • Client: app muốn truy cập data (vd Spotify)
  • Authorization Server: nơi user login (Google)
  • Resource Server: API có data (Google Drive API)

Authorization Code Flow (recommended)

User → Click "Login with Google" trên Spotify │ ▼ Spotify → Redirect user đến Google login page ?client_id=spotify &redirect_uri=https://spotify.com/callback &scope=email profile &response_type=code │ ▼ User → Login Google + click "Allow Spotify" │ ▼ Google → Redirect về spotify.com/callback?code=AUTH_CODE │ ▼ Spotify backend → POST /token đến Google { client_id: "spotify", client_secret: "xyz", code: "AUTH_CODE", redirect_uri: "..." } │ ▼ Google → trả access_token + refresh_token + (id_token nếu OIDC) │ ▼ Spotify → dùng access_token gọi Google API GET /userinfo Authorization: Bearer ACCESS_TOKEN

Tại sao 2 bước (auth code + token)?

Authorization code đi qua URL của user (qua browser) — không an toàn để gửi trực tiếp access_token. Code chỉ dùng 1 lần, đổi lấy token qua server-to-server (có client_secret) → an toàn hơn.

PKCE — cho mobile/SPA

Mobile/SPA không thể giữ client_secret bí mật (decompile được). PKCE (Proof Key for Code Exchange) thêm code_verifier:

  1. Client tạo random code_verifier, hash thành code_challenge
  2. Gửi code_challenge trong auth request
  3. Khi đổi code → token, gửi code_verifier gốc → server verify hash khớp

Đảm bảo: chỉ client thật sự khởi tạo flow mới đổi được code → token.

Scope

Định danh quyền cụ thể: email, profile, https://www.googleapis.com/auth/calendar.readonly. User see và approve scope.

Other grant types (less used)

  • Client Credentials: server-to-server, không có user
  • Implicit: cho SPA (deprecated, dùng Auth Code + PKCE)
  • Resource Owner Password: app trực tiếp lấy password (nguy hiểm, deprecated)
  • Device Code: cho TV, console (vd login Netflix trên TV bằng cách quét QR trên phone)

9.8 OpenID Connect (OIDC) — auth trên OAuth

OAuth 2.0 là authorization (cấp quyền truy cập API) — không phải authentication. Nhưng nhiều người dùng OAuth làm "Login with Google" — về mặt kỹ thuật là sai.

OpenID Connect = layer authentication trên OAuth. Thêm id_token (JWT chứa user info) vào response.

// id_token là JWT decode được:
{
  "iss": "https://accounts.google.com",
  "sub": "12345",
  "email": "alice@gmail.com",
  "name": "Alice",
  "picture": "https://...",
  "iat": ...,
  "exp": ...
}

"Login with Google" trên app web = OIDC. Nhận id_token → biết user là ai → tạo session/JWT của riêng app.

9.9 Common Auth Attacks

1. Token Theft — XSS

Nếu JWT lưu localStorage, attacker tiêm script đọc localStorage.token → gửi server attacker. Fix: cookie HttpOnly + CSP.

2. CSRF (Cross-Site Request Forgery)

Attacker tạo trang ác chứa <form action="https://bank.com/transfer" method="POST">. User đã login bank, click form → browser tự gửi cookie session bank → tiền bị chuyển. Fix: SameSite cookie, CSRF token. Sẽ học sâu chương 10.

3. Replay Attack

Attacker bắt được token, gửi lại sau. Fix: exp ngắn, nonce, IP binding.

4. JWT alg=none

Bug kinh điển: lib parse JWT mà chấp nhận alg: none (không signature). Attacker chỉnh payload → server tin. Fix: verify alg whitelist.

5. JWT key confusion

Bug: lib dùng RS256 (asymmetric) nhưng config secret string → attacker dùng public key làm HS256 secret để forge. Fix: hardcode alg expected.

6. Session fixation

Attacker dụ user login với session ID attacker biết trước → sau khi login attacker dùng cùng session ID. Fix: rotate session ID sau login.

7. Brute force / credential stuffing

Thử password phổ biến, hoặc dùng password leak từ site khác. Fix: rate limit, MFA, captcha, hash bcrypt/argon2.

Bài tập

Bài 1 — Cookie inspection

Login vào 1 site bạn dùng. Mở DevTools → Application → Cookies. Liệt kê: domain, expires, HttpOnly, Secure, SameSite của các cookie session.

Bài 2 — Decode JWT

Nếu site dùng JWT, copy token, paste vào jwt.io decoder. Đọc payload. Có nhạy cảm không?

Bài 3 — Session vs JWT scenarios

Mỗi case sau, dùng Session hay JWT? Tại sao? (a) Banking app — admin có thể disable user ngay; (b) Mobile game multiplayer; (c) Internal tool monolith Rails; (d) Microservice authn shared giữa 10 services; (e) Public API ai cũng dùng được key.

Bài 4 — OAuth flow tay

Đăng ký 1 OAuth app trên GitHub Developer Settings. Implement Authorization Code flow đầu cuối: redirect → callback → trao đổi code → gọi user API.

Bài 5 — Refresh token rotation

Thiết kế DB schema cho refresh token với rotation. Mỗi refresh token có: id, user_id, expires_at, replaced_by.

Bài 6 — Logout của JWT

JWT stateless → khó logout. Đề xuất 3 cách giải quyết: (a) blacklist; (b) ngắn hạn + refresh; (c) đổi secret.

🧪 Quiz cuối chương

Câu 1. Cookie attribute HttpOnly để?

  • Chỉ gửi qua HTTP, không HTTPS
  • Cookie chỉ valid 1 lần
  • JavaScript KHÔNG đọc được cookie (qua document.cookie) — chống XSS đánh cắp
  • Cookie không dùng được trên mobile

Đáp án: chống JS đọc. XSS không thể lấy session cookie nếu HttpOnly. Best practice cho session.

Câu 2. SameSite cookie attribute để?

  • Cookie chỉ gửi cùng IP
  • Chống CSRF — Strict không gửi cross-site, Lax chỉ gửi với GET top-nav, None gửi mọi nơi (cần Secure)
  • Cookie giữa nhiều subdomain
  • Tăng tốc

Đáp án: chống CSRF. Default Lax từ Chrome 80+ → fix nhiều CSRF tự động.

Câu 3. Khác biệt chính Session vs JWT?

  • Session stateful (server lưu); JWT stateless (claims trong token, verify signature)
  • Session dùng cookie, JWT không
  • JWT nhanh hơn
  • JWT chỉ cho mobile

Đáp án: stateful vs stateless. Session cần DB lookup, JWT verify cục bộ.

Câu 4. JWT signature dùng để?

  • Mã hoá payload
  • Compress token
  • Đảm bảo token không bị tamper — verify signature → tin payload là server gốc đã ký
  • Định danh user

Đáp án: chống tamper. Payload đọc được (base64), signature đảm bảo không sửa.

Câu 5. JWT lưu ở đâu là an toàn nhất?

  • Cookie HttpOnly + Secure + SameSite — JS không đọc được, browser tự gửi
  • localStorage
  • sessionStorage
  • URL query param

Đáp án: HttpOnly cookie. localStorage có XSS risk. URL leak vào log/referer.

Câu 6. Refresh token để làm gì?

  • Refresh trang web
  • Lấy access token mới khi access expired — không phải login lại; có thể revoke server-side
  • Mã hoá access token
  • Compress data

Đáp án: lấy access mới. Access ngắn hạn (15m) → security; refresh dài hạn (7d) → UX. Refresh lưu DB → revoke được.

Câu 7. OAuth 2.0 Authorization Code flow tại sao 2 bước?

  • Để chậm hơn
  • Spec yêu cầu
  • Auth code đi qua browser (URL); code đổi token qua server-to-server với client_secret → an toàn hơn gửi token trực tiếp
  • Vì backwards compat

Đáp án: bảo mật. Code chỉ dùng 1 lần, đổi qua HTTPS server-to-server.

Câu 8. "Login with Google" thực ra là OAuth hay OIDC?

  • OIDC (OpenID Connect) — layer authentication trên OAuth, trả id_token chứa user info
  • OAuth thuần
  • SAML
  • LDAP

Đáp án: OIDC. OAuth là authorization, OIDC mới là authentication. Nhiều người gọi nhầm.

Tổng kết chương 9

  • AuthN = ai bạn? AuthZ = bạn được làm gì? Status 401 vs 403
  • Cookie attributes: HttpOnly (chống XSS), Secure (HTTPS), SameSite (chống CSRF)
  • Session: stateful, server lưu session ID → user; logout dễ; cần shared store
  • JWT: stateless, header.payload.signature; verify signature đủ; logout/revoke khó
  • ✅ Session vs JWT: chọn theo nhu cầu (revoke, microservice, mobile)
  • Access + Refresh token + rotation = balance security/UX
  • OAuth 2.0: authorization (cấp quyền API). Authorization Code flow chuẩn; PKCE cho mobile/SPA
  • OIDC = OAuth + id_token cho authentication ("Login with Google")
  • ✅ Common attacks: XSS theft, CSRF, replay, JWT alg=none, key confusion, session fixation
  • ✅ Best practice: cookie HttpOnly + Secure + SameSite + bcrypt/argon2 password + rate limit + MFA
← Chương trước Chương 08: API Styles Chương kế tiếp Chương 10: Security & Infra →