โหมดมืด
บทที่ 2 — Authentication ลึก
หลังจบบท คุณจะ:
- เลือก password hashing algorithm ถูก
- ทำ MFA / passkey ที่ใช้งานได้จริง
- เข้าใจ session vs JWT — เลือกใช้ตอนไหน
- ทำ OAuth2 / OIDC flow ที่ถูกต้อง
- เข้าใจ SAML / SSO สำหรับ enterprise
📌 ศัพท์ที่ต้องรู้ก่อน (อ่านผ่าน ๆ ไว้ก่อน เดี๋ยวเจอละเอียดในบท)
คำ/คำย่อ คำอ่าน กางเต็ม ความหมายสั้น ๆ AuthN ออธ-เอ็น Authentication (จำว่า autheNticatioN — N ท้าย) "คุณเป็นใคร?" — การยืนยันตัวตน AuthZ ออธ-แซด Authorization (จำว่า authoriZation — Z) "คุณทำอะไรได้?" — การให้สิทธิ์ (อยู่บท 3) MFA / 2FA เอ็ม-เอฟ-เอ / ทู-เอฟ-เอ Multi-Factor / Two-Factor Authentication ยืนยันตัวด้วยหลายปัจจัย (รหัส + OTP) — MFA และ 2FA ความหมายเหมือนกัน เพียงแต่ 2FA บ่งบอกว่าใช้แค่ 2 ปัจจัยเท่านั้น OTP โอ-ที-พี One-Time Password รหัสใช้ครั้งเดียว TOTP โท-ทป หรือ ที-โอ-ที-พี Time-based OTP รหัสใช้ครั้งเดียวที่เปลี่ยนตามเวลา (เช่น Google Authenticator) JWT เจ-ดับเบิลยู-ที (หรือ "จ็อต") JSON Web Token token ที่เซ็นลายเซ็นไว้ เก็บข้อมูล login ฝั่ง client OAuth2 โอ-ออธ-ทู Open Authorization 2.0 (ปี 2026 = OAuth 2.1 — BCP RFC 9700) กรอบมาตรฐานให้แอปขอเข้าถึงข้อมูล user โดยไม่ต้องรู้ password OIDC โอ-ไอ-ดี-ซี OpenID Connect ชั้นยืนยันตัวตนที่ต่อยอดบน OAuth (ใช้ทำ "Login with Google") PKCE พิก-ซี่ Proof Key for Code Exchange กันการขโมย authorization code ใน OAuth flow (OAuth 2.1 บังคับใช้ทุก client) DPoP ดี-ป็อป Demonstrating Proof of Possession (RFC 9449) bind token เข้ากับ key ฝั่ง client — กัน token ถูกขโมยไปใช้ SAML แซม-เอ็ล หรือ เอส-เอ-เอ็ม-แอล Security Assertion Markup Language มาตรฐาน SSO ฝั่ง enterprise (เก่า, ใช้ XML) SSO เอส-เอส-โอ Single Sign-On login ครั้งเดียว ใช้ได้หลายระบบ passkey / WebAuthn / FIDO2 พาส-คีย์ / เว็บ-ออธ-เอ็น / ฟิ-โด-ทู — login แบบไม่ใช้ password ใช้ลายนิ้วมือ/หน้า แทน (มาตรฐานของ FIDO Alliance) Argon2id อาร์-กอน-ทู-ไอ-ดี Argon2 (id variant) password hashing ที่ OWASP 2024 แนะนำ 👉 ไม่ต้องจำให้หมด — เปิดตารางนี้กลับมาดูได้ตลอดเวลาที่เจอคำย่อแปลก ๆ
1. คำศัพท์เบื้องต้น
ก่อนลงรายละเอียด มาทำความรู้จักศัพท์ที่จะเจอตลอดทั้งบท — โดยเฉพาะความต่างของ authentication (คุณเป็นใคร) กับ authorization (คุณทำอะไรได้) ที่สับสนกันบ่อย และคำว่า session vs token ที่จะกลับมาเจอเรื่อย ๆ:
Authentication (AuthN) "คุณเป็นใคร?"
Authorization (AuthZ) "คุณทำอะไรได้?"
Identity สิ่งระบุตัวคุณ (email, name)
Credential หลักฐานยืนยันตัวตน (password, key)
Session สถานะ login ที่ server เก็บไว้
Token สถานะ login ที่อยู่ฝั่ง client (stateless)→ บทนี้ = AuthN (บทถัดไปจะเป็น AuthZ)
2. Authentication Factors — ปัจจัยในการยืนยันตัว
การยืนยันตัวตนทำได้หลาย "ปัจจัย" (factor) แบ่งตามว่าใช้สิ่งที่คุณ รู้/มี/เป็น/อยู่ที่ไหน — กุญแจสำคัญคือ MFA (multi-factor) ต้องใช้ factor จากคนละหมวด (password + OTP) ถึงจะปลอดภัยจริง การใช้ 2 password ไม่นับเป็น MFA:
1. Something you know — รู้: password, PIN, security question
2. Something you have — มี: phone (SMS), token (TOTP, hardware key)
3. Something you are — เป็น: ลายนิ้วมือ, ใบหน้า
4. Somewhere you are — อยู่ที่ไหน: IP, GPS location
5. Something you do — ทำยังไง: typing pattern (behavioral)MFA = ใช้ 2+ factors จาก category ที่ต่างกัน
2 passwords ≠ MFA (factor เดียวกัน)
Part 1: Password Authentication
3. ทำไมต้อง Hash Password
กฎเหล็กข้อแรกของ auth — ห้ามเก็บ password เป็น plain text เด็ดขาด เพราะถ้า DB ถูก leak password ทุกคนหลุดทันที การ hash ทำให้แม้ DB หลุดไป hash ก็ย้อนกลับเป็น password เดิมไม่ได้ ลองเทียบสองวิธีเก็บ:
เก็บแบบ plain: เก็บแบบ hash:
───────── ─────────
email | password email | hash
───────────── ──────────
alice | secret123 alice | $2a$10$abc...
❌ DB ถูก leak → password ทุกคนหลุดทันที
✅ DB ถูก leak → hash ไร้ประโยชน์ (reverse ไม่ได้)4. Hash ≠ Encrypt
Encrypt: text → cipher → text (reverse ได้ ถ้ามี key)
Hash: text → digest (reverse ไม่ได้)Password = ต้อง hash เสมอ — ไม่ใช่ encrypt
(เพราะ encrypt ถ้า key หลุด → password ทุกคนหลุด)
5. วิธีที่ผิด — อย่าทำ
Plain text — เก็บตรง ๆ
java
user.password = "secret123"; // ❌ หายนะรอเกิดBase64 / encode — ไม่ใช่ security
java
user.password = Base64.encode("secret123"); // ❌ decode 1 บรรทัดMD5 / SHA-1 / SHA-256 / SHA-512 — เร็วเกินไป
java
user.password = DigestUtils.sha256Hex("secret123");
// ❌ Fast hash → brute force ได้ง่าย
// 1 GPU สมัยใหม่ = SHA-256 หลาย billion ครั้ง / วินาที"password123" หลัง SHA-256:
ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f
Attacker:
1. ขโมย hash จาก DB
2. lookup ใน rainbow table (precomputed)
3. → "password123" ในเสี้ยววินาทีHash + Salt (ดี — แต่ยังใช้ SHA-256 ไม่พอ)
java
String salt = randomBytes();
String hash = SHA256(salt + password);→ Salt ช่วยกัน rainbow table — แต่ยัง brute force ได้ millions/sec บน GPU
6. วิธีที่ถูก — ช้าและกิน RAM โดยตั้งใจ
BCrypt (ปี 1999) — Workhorse
java
// Maven dep: org.springframework.security:spring-security-crypto
// (มากับ spring-boot-starter-security อยู่แล้ว — ไม่ต้องเพิ่มต่างหาก)
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12); // cost factor
String hash = encoder.encode("secret123");
// "$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
// ↑ ↑ ↑
// algo cost (2^12 รอบ) salt + hash รวมกัน
//
// หมายเหตุ: prefix "$2a$" เป็น legacy — modern แนะนำ "$2b$" (แก้ sign-extension bug ของ $2a$)
// Spring Security default ออกเป็น $2a$ ซึ่งเทียบเท่าและใช้ได้ปลอดภัย
boolean ok = encoder.matches("secret123", hash); // true🧠 Mental model — cost factor คืออะไร: cost factor (หรือ "work factor") = ตัวเลขที่บอกว่า hash 1 ครั้งต้องวนกี่รอบ ยิ่ง cost สูง → hash 1 ครั้งใช้เวลานานขึ้น → attacker ที่ brute force ต้องช้าลงทวีคูณ เราเสียเวลา login นิดเดียว (~250ms) แต่ attacker เสียเวลาเป็นล้านเท่า
Cost 12 — ตีค่าจริง ๆ บน server ปี 2026:
- 2^12 = 4096 iterations ต่อ 1 hash
- ใช้เวลา ~250–500ms บน server CPU สมัยใหม่ (Intel Xeon m6i, Apple M-series) — ตัวเลขเก่าที่บอก "30-100ms" ล้าสมัยแล้ว
- OWASP Password Storage Cheat Sheet (2024) แนะนำ target work factor ~250ms–1s สำหรับ interactive login (ไม่ใช่ 100ms)
- ควร benchmark บนเครื่อง production จริง เสมอ — ตัวเลขต่างกันตาม CPU
- → brute force ช้าลงเป็นล้านเท่า
ข้อดี:
- Salt ในตัว ไม่ต้องจัดการเอง
- ผ่านการทดสอบมา 25+ ปี
- มีในทุกภาษา / framework
ข้อจำกัด:
- รับ password ได้แค่ 72 bytes — truncate เงียบ ๆ ไม่ error (บาง impl ตัดที่ null byte 0x00 ด้วย) → ถ้า password อาจยาวกว่านี้ (เช่น passphrase ยาว) ให้ pre-hash ด้วย SHA-256 + base64 ก่อนส่งเข้า BCrypt
- ไม่ memory-hard (GPU ขนานกันได้)
scrypt (ปี 2009) — Memory-hard
java
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(
16384, // CPU cost
8, // memory cost
1, // parallelism
32, // key length
16 // salt length
);Memory-hard = ต้องใช้ RAM เยอะ → GPU เสีย advantage
Argon2 (ปี 2015) ⭐ ตัวที่ recommended ปี 2026
ผู้ชนะ Password Hashing Competition
java
// Maven deps:
// org.springframework.security:spring-security-crypto (มี Argon2PasswordEncoder)
// org.bouncycastle:bcprov-jdk18on (runtime — Argon2 impl)
// signature: Argon2PasswordEncoder(saltLength, hashLength, parallelism, memoryInKB, iterations)
// (อ้างอิง Spring Security 6.x source — ตรวจสอบ version ของคุณก่อนใช้)
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder(
16, // saltLength (bytes)
32, // hashLength (bytes)
1, // parallelism
19456, // memoryInKB — 19 MiB OWASP baseline
2 // iterations (OWASP baseline)
);
String hash = encoder.encode("secret123");📎 กางตัวเลข Argon2 parameters:
16(saltLength) = ขนาด salt 16 bytes (random ที่ใส่หน้า password ก่อน hash — กันใช้ rainbow table)32(hashLength) = ขนาด hash output 32 bytes1(parallelism, p) = ใช้ 1 thread ตอน hash (ตั้ง > 1 ถ้า CPU มีหลาย core เหลือ)19456(memory, m) = ใช้ memory 19456 KiB ≈ 19 MiB (19 × 1024 KiB) ตอน hash 1 ครั้ง — ยิ่งเยอะ ยิ่งทน GPU brute force
- 💡 KiB ≠ KB: KiB = kibibyte = 1024 bytes (binary); KB = kilobyte = 1000 bytes (decimal) — Argon2 ใช้ binary
2(iterations, t) = วน 2 รอบ — ยิ่งเยอะ ยิ่งช้าต่อ 1 hash📎 self-DoS คืออะไร: ถ้าตั้ง m + t สูงเกิน (เช่น m=64 MiB, t=10) → hash 1 ครั้งใช้เวลา > 1 วินาที · เมื่อ traffic peak (login พร้อมกัน 1000 คน) server เอง CPU 100% — เหมือนถูก DoS แต่จากตัวเอง · ตัวเลข OWASP baseline (19 MiB, t=2) สมดุลที่สุด
⚠️ เลือก parameters จาก OWASP Password Storage Cheat Sheet (2024):
- Baseline: m=19 MiB (19456), t=2, p=1 — เร็วประมาณ 50–100ms ต่อ hash บน server CPU
- Alternative profile (trade-off ต่าง ไม่ใช่ "ปลอดภัยกว่า"): m=46 MiB, t=1, p=1 — กิน RAM เยอะกว่าแต่วนน้อยรอบ
- อย่าตั้ง t สูงเกิน 3 + m สูงเกิน 64 MiB — แต่ละ login จะใช้ >1 วินาที กลายเป็น self-DoS เมื่อ traffic สูง
- target work factor: ~250ms–1s — benchmark บนเครื่อง production จริงเสมอ
- ref: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
มี 3 variant (ออกเสียง: Argon2d = อาร์-กอน-ทู-ดี, Argon2i = อาร์-กอน-ทู-ไอ, Argon2id = อาร์-กอน-ทู-ไอ-ดี):
- Argon2d — เร็ว แต่เสี่ยง side-channel attack (= การโจมตีโดยวัดเวลา/ไฟ/เสียงของ CPU แทนที่จะ break crypto โดยตรง — เช่นวัดเวลาว่า memory access pattern ต่างกันยังไงเมื่อ password ต่างกัน)
- Argon2i — ช้าหน่อย ปลอดภัยกว่าจาก side-channel (memory access ไม่ขึ้นกับ password)
- Argon2id — hybrid ⭐ ตัวที่แนะนำ (รวมข้อดี 2 ตัวบน — OWASP 2024 default)
สรุป — เลือกอะไร (ปี 2026)
Best: Argon2id
ดี: scrypt
OK: BCrypt (cost ≥ 12)
ห้าม: MD5, SHA-* แบบเดี่ยว, plain, Base647. Password Policy — สมัยใหม่ไม่เหมือนเก่า
Guideline ของ NIST SP 800-63B Rev.4 (ปี 2024)
✅ ขั้นต่ำ 8 ตัว (15 สำหรับ high-assurance account เช่น admin/banking)
✅ อนุญาต ทุก character (รวม space, emoji, Unicode)
✅ อนุญาต paste (ห้าม disable — ขัดขวาง password manager)
✅ check ว่าโดน breach ไหม (haveibeenpwned)
❌ บังคับ complexity (ต้องมีตัวใหญ่ + ตัวเลข + special char) — Rev.4 เลิกแล้ว
❌ บังคับเปลี่ยน periodic (ทุก 90 วัน) — Rev.4 ห้าม rotate โดยไม่มีเหตุ
❌ Security question ("สีโปรด?")
❌ Hintทำไม "ห้าม" สิ่งที่เคยทำกัน?
- บังคับ complexity → user ใช้ "Password1!" (เดาได้)
- บังคับ rotation → user เปลี่ยนเป็น "Password2!" → "Password3!"
- Security question → คำตอบหาเจอใน Facebook
- Hint → ก็คือบอก attacker
Check Password ที่ Breach แล้ว — K-anonymity (อ่าน "เค-แอ-นอ-นิ-มิ-ตี้")
🧠 K-anonymity คืออะไร (ภาษาคน): = "ซ่อนตัวในฝูง" — ส่งข้อมูลที่ทำให้ server หาตัวจริงไม่ได้ในกลุ่มอย่างน้อย K ตัว เราอยากรู้ว่า password ของเรา เคย leak หรือเปล่า แต่ ไม่อยากบอก server ว่าเรากำลังเช็ค password อะไร ทริค: ส่งแค่ 5 ตัวแรกของ hash ไป → server ตอบกลับ hash ทุกตัวที่ขึ้นต้นเหมือนกัน (หลายร้อย) → เราเทียบเองที่เครื่อง
⚠️ หมายเหตุสำคัญ: SHA-1 ที่ใช้ตรงนี้คือ โปรโตคอลของ HaveIBeenPwned API เท่านั้น — ไม่ใช่ SHA-1 สำหรับเก็บ password (ห้ามใช้ SHA-1 เก็บ password เด็ดขาด ใช้ Argon2id/BCrypt)
Walk-through step-by-step:
สมมุติ password = "password123"
ขั้น 1: hash ฝั่งเรา (browser/server เรา)
SHA-1("password123") = CBFDAC6008F9CAB4083784CBD1874F76618D2A97
ขั้น 2: ตัดเป็น prefix (5 ตัว) + suffix
prefix = "CBFDA"
suffix = "C6008F9CAB4083784CBD1874F76618D2A97"
ขั้น 3: ส่งแค่ prefix ไป HIBP
GET https://api.pwnedpasswords.com/range/CBFDA
→ server ตอบ: hash หลายร้อยตัวที่ขึ้นต้น "CBFDA"
C6008F9CAB4083784CBD1874F76618D2A97:23597311 ← นี่!
C601021F2543C9F95EA6F00F33E5A8D70BD:5
... (อีก ~500 บรรทัด)
ขั้น 4: เทียบ suffix ที่เครื่องเรา
ถ้าเจอ → password นี้เคย leak → reject
→ server ไม่รู้ว่า "เราเช็ค password อะไร" เพราะ prefix เดียวกันมี hash เป็นไปได้นับล้านjava
public boolean isPasswordBreached(String password) {
// ⚠️ SHA-1 ตรงนี้คือ "โปรโตคอลของ HIBP API" ไม่ใช่ password storage
// password storage ใช้ Argon2id/BCrypt ตามที่อธิบายข้างบน
String hash = DigestUtils.sha1Hex(password).toUpperCase();
String prefix = hash.substring(0, 5); // ส่งไป server
String suffix = hash.substring(5); // เก็บไว้เทียบที่เครื่องเรา
String response = restClient.get()
.uri("https://api.pwnedpasswords.com/range/" + prefix)
.retrieve()
.body(String.class);
// response = หลายร้อยบรรทัดของ "SUFFIX:count" — เทียบ suffix ของเรา
return response.lines().anyMatch(line -> line.startsWith(suffix));
}→ ถ้า password นี้เคยอยู่ใน data breach → reject
8. Login Flow ที่ทำให้สมบูรณ์
java
@PostMapping("/login")
public LoginResponse login(@RequestBody @Valid LoginRequest req) {
// 1. Rate limit per IP **ก่อน** ทุกอย่าง — กัน DoS ที่ตั้งใจ lock account คนอื่น
// (ถ้าทุก attempt บังคับ DB lookup → attacker สามารถ flood ได้)
rateLimiter.acquireByIp(req.clientIp()); // จำกัด ต่อ IP ก่อน
rateLimiter.acquireByAccount(req.email()); // แล้วค่อย ต่อ account
// 2. หา user
User user = userRepo.findByEmail(req.email())
.orElseThrow(() -> new InvalidCredentialsException());
// 3. Check lockout
if (user.isLocked()) {
throw new AccountLockedException(user.getLockedUntil());
}
// 4. Verify password
if (!passwordEncoder.matches(req.password(), user.getPassword())) {
user.recordFailedAttempt();
if (user.getFailedAttempts() >= 5) {
// ⚠️ DoS risk: attacker เจตนา fail หลายครั้งเพื่อ lock account เหยื่อ
// → IP rate limit ข้อ 1 ต้องทำงานก่อน mitigation เพิ่ม:
// - increase delay แทนการ lock (exponential backoff)
// - หรือ require CAPTCHA แทน lock
user.lockUntil(Instant.now().plus(15, ChronoUnit.MINUTES));
}
userRepo.save(user);
throw new InvalidCredentialsException();
}
// 5. Reset failed attempts ตอน login สำเร็จ
user.resetFailedAttempts();
user.setLastLoginAt(Instant.now());
userRepo.save(user);
// 6. Check ว่าต้อง MFA ไหม
if (user.isMfaEnabled()) {
return LoginResponse.mfaRequired(createMfaChallenge(user));
}
// 7. ออก session / token
String token = jwtService.generate(user);
auditLog.record("LOGIN_SUCCESS", user.getId());
return LoginResponse.success(token, user);
}ข้อควรระวัง
- Error message เดียวกัน สำหรับ "password ผิด" + "ไม่มี user" → กัน enumeration
- Constant-time comparison (BCrypt ทำให้แล้ว)
- Audit log ทุกครั้ง — สำเร็จ + ล้มเหลว
- Lockout + delay (ระวัง DoS — attacker เจตนา lock account ของคนอื่น)
- Rate limit ทั้ง IP + account
9. MFA — Multi-Factor Authentication
ทำไมต้องมี
Password เพียงอย่างเดียว = single point of failure
MFA = block 99.9% ของ commodity automated attack (ข้อมูล Microsoft ปี 2019)
⚠️ Caveat สำคัญ (อัปเดต 2024–2026): ตัวเลข 99.9% ใช้กับ automated attack ทั่วไป เท่านั้น
- AiTM (Adversary-in-the-Middle) phishing kit เช่น EvilProxy, Tycoon2FA (2023-2025) bypass TOTP/SMS/push MFA ได้แบบ scale — ขโมย session token หลังจาก user ผ่าน MFA แล้ว
- เฉพาะ phishing-resistant MFA (passkey, hardware key เช่น YubiKey) ที่หยุด AiTM ได้
- → high-value account ต้องบังคับ passkey, ไม่พึ่ง TOTP/SMS อย่างเดียว
วิธีต่าง ๆ
TOTP (Time-based One-Time Password — อ่าน "โท-ทป" หรือ "ที-โอ-ที-พี") ⭐ Default
มาตรฐาน RFC 6238 — รหัสที่เปลี่ยนทุก 30 วินาทีตามเวลา
user scan QR code → app เก็บ secret
ทุก 30 วินาที → generate code 6 หลัก
server verify code (ยอมรับ ±1 window สำหรับ clock skew)java
// Maven dep: dev.samstevens.totp:totp-spring-boot-starter
// (หรือ dev.samstevens.totp:totp ถ้าไม่ใช้ Spring Boot)
// - SecretGenerator = สุ่ม secret base32 สำหรับ user ใหม่
// - QrData / QrGenerator = สร้าง QR code ที่ user สแกนเข้า authenticator app
// - CodeVerifier = ตรวจสอบรหัส 6 หลักที่ user กรอกตอน login
// 1. Setup — generate secret
SecretGenerator secretGenerator = new DefaultSecretGenerator();
String secret = secretGenerator.generate(); // "JBSWY3DPEHPK3PXP"
// 2. แสดง QR
QrData data = new QrData.Builder()
.label(user.getEmail())
.secret(secret)
.issuer("MyApp")
.digits(6)
.period(30)
// SHA1 ใช้เพื่อ compat กับ Google Authenticator (default ของ RFC 6238)
// — Authy, 1Password รองรับ SHA256/SHA512 ปลอดภัยกว่า แต่ Google Authenticator ยังไม่รองรับ (ณ ปี 2026)
.algorithm(HashingAlgorithm.SHA1)
.build();
String qrCode = qrGenerator.generate(data);
// Save secret (encrypted) ใน user record
// 3. Verify code
CodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), new SystemTimeProvider());
boolean valid = verifier.isValidCode(secret, userInputCode);App ที่ user ใช้: Google Authenticator, Authy, 1Password, Microsoft Authenticator
SMS OTP
user login → SMS รหัส 6 หลัก → กรอกรหัส⚠️ NIST SP 800-63B Rev.4 (2024) ไม่แนะนำเด็ดขาด — SMS OTP ไม่ phishing-resistant และมีความเสี่ยง:
- SIM swap attack = attacker หลอกค่ายมือถือ (call center) ให้ย้ายเบอร์ของเหยื่อมาซิมของตัวเอง → รับ SMS OTP แทนเหยื่อ
- SS7 vulnerability (อ่าน "เอส-เอส-เซเว่น") = ช่องโหว่ในโปรโตคอลเครือข่ายโทรศัพท์ Signaling System 7 ที่ใช้ดักอ่าน SMS ได้จากระยะไกล
→ last-resort fallback กับ low-risk account เท่านั้น — high-value account (banking, admin, email หลัก) ห้ามใช้ SMS เลย → App ใหม่ปี 2026: เริ่มต้นด้วย TOTP/Passkey ตั้งแต่แรก ไม่ต้องมี SMS
Email OTP
คล้าย SMS แต่ส่งทาง email⚠️ ถ้า email โดน compromise → MFA bypass
→ OK สำหรับ app ไม่ critical, ไม่ OK กับ banking
Push Notification
user login → push notification ที่มือถือ → กด approveUser-friendly แต่ระวัง "MFA fatigue" — attacker spam push หลายครั้ง หวังให้ user เผลอ approve
🛡️ Mitigation (ปี 2026 standard):
- Number matching = แสดงเลข 2 หลักบน login screen, user ต้องพิมพ์เข้า authenticator app ให้ตรง (Microsoft + Okta บังคับ default ตั้งแต่ 2023)
- Context display = แสดง location / app / IP ใน push เพื่อให้ user เช็คได้
- Push แบบ tap-only (กดอนุมัติเฉย ๆ) deprecated — ต้องมี number matching เสมอ
Hardware Token (YubiKey — อ่าน "ยู-บิ-คีย์" แบรนด์ hardware key ยอดนิยมจาก Yubico)
user login → แตะ hardware key → verify ผ่าน WebAuthnปลอดภัยที่สุด — กัน phishing ได้ (key ผูกกับ domain)
Backup Codes
หลัง setup MFA → แสดง 10 รหัสใช้ครั้งเดียว
user save ไว้ — ใช้ได้ถ้ามือถือหาย→ ขาดไม่ได้ สำหรับ account recovery
10. Passkeys (WebAuthn 2.0 / FIDO2) ⭐ Standard ปี 2026
Passwordless authentication ผ่าน biometric ของอุปกรณ์มาตรฐานจาก FIDO Alliance (Fast IDentity Online) + W3C — CISA (หน่วยงานความมั่นคงไซเบอร์ของรัฐบาลสหรัฐ) บังคับ phishing-resistant MFA (passkey/hardware key) สำหรับระบบรัฐบาลกลาง ปี 2024+
Adoption Status ปี 2026 — Mainstream แล้ว
✅ ใช้แล้วในทุกแพลตฟอร์มหลัก:
- Apple (iCloud Keychain — iOS 16+, macOS Ventura+)
- Google Accounts (default ปี 2023)
- Microsoft (รวมถึง Windows Hello)
- GitHub (ปี 2023)
- Amazon, eBay, PayPal, Shopify
- 1Password, Dashlane, Bitwarden sync ข้ามอุปกรณ์
✅ Browser support: 100% modern browsers
✅ Mobile: iOS, Android รองรับ nativeFlow
User register:
1. browser ขอ biometric (face/fingerprint)
2. อุปกรณ์ generate key pair (Ed25519, ECDSA)
3. ส่ง public key ไป server
4. private key อยู่ใน อุปกรณ์ — **2 แบบ:**
- **Synced passkey** (default ของ Apple, Google) → encrypted sync ผ่าน iCloud Keychain / Google Password Manager / 1Password
- **Device-bound passkey** (เช่น hardware key YubiKey) → ไม่ sync, อยู่ในอุปกรณ์ตัวเดียวเท่านั้น
Login:
1. server ส่ง challenge
2. user authenticate ด้วย biometric (Face ID, Touch ID, Windows Hello)
3. อุปกรณ์ sign challenge ด้วย private key
4. server verify ด้วย public keyข้อดี
✅ ไม่มี password ให้ขโมย (server เก็บแค่ public key)
✅ Phishing-resistant — key ผูกกับ domain (origin)
→ Fake site ใช้ key ไม่ได้แม้ user เผลอเข้า
✅ UX ดีมาก (แตะ → login ภายใน 1 วินาที)
✅ Sync ข้ามอุปกรณ์ (iCloud Keychain, Google Password Manager, 1Password)
✅ MFA ในตัว (biometric = "have device" + "are who")ข้อเสีย / Limitations
⚠️ Recovery — ถ้า device หาย + ไม่มี sync → ลำบาก
→ Best practice: บังคับให้ user มี multiple passkeys (phone + laptop)
→ หรือมี backup auth (email magic link, security key)
⚠️ Enterprise — บาง corporation block iCloud/Google sync
→ ใช้ hardware key (YubiKey) แทน
⚠️ Migration — app เก่ามักให้ผสม password + passkeySpring Security 6.4+ (Nov 2024 — first stable passkey/WebAuthn DSL)
⚠️ Verify API ก่อนใช้: WebAuthn DSL เพิ่งเปิดตัวใน Spring Security 6.4 (Nov 2024) — signature อาจเปลี่ยนใน minor version ถัด ๆ ไป เช็ค release notes ก่อน copy ใช้
java
@Configuration
@EnableWebSecurity
public class WebAuthnConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.webAuthn(webauthn -> webauthn
// rp = Relying Party = แอปของเรา (ฝั่งที่ "พึ่ง" passkey เพื่อยืนยันตัว user)
.rpName("My App") // ชื่อแอป (แสดงตอน user สร้าง passkey)
.rpId("example.com") // domain ที่ผูกกับ key — passkey จะใช้ได้แค่กับ domain นี้
.allowedOrigins("https://example.com") // origin ที่อนุญาตให้ initiate WebAuthn ceremony
)
.formLogin(form -> form
.loginPage("/login") // fallback to password if needed
);
return http.build();
}
}Recommendation ปี 2026
✅ App ใหม่ → support passkey ตั้งแต่ day 1
✅ App เก่า → เพิ่ม passkey เป็น option (parallel to password)
✅ บังคับให้ user มี ≥ 2 passkeys (recovery)
✅ Offer hardware key (YubiKey) สำหรับ admin / high-value account
❌ อย่ายกเลิก password ทันที — เผื่อ recovery
❌ อย่า force ทุก user เปลี่ยน — UX shockTrend: 2026 = "password+passkey" era. ปี 2030 = "passkey only" สำหรับหลายๆ app
11. Session vs Token — ใช้แบบไหน
Session-based
Login:
server สร้าง session → save ใน store (DB / Redis)
ส่ง sessionId cookie กลับ
Request:
browser ส่ง cookie มาด้วย
server lookup session
return response
Logout:
server ลบ sessionข้อดี:
- ✅ Revoke ทันทีได้ (ลบจาก store)
- ✅ Token เล็ก
- ✅ เก็บ data ที่ server-side
ข้อเสีย:
- ❌ Server stateful — scale ลำบาก
- ❌ DB / cache lookup ทุก request
Token-based (JWT)
Login:
server generate JWT (signed)
ส่งคืน client
Request:
client ส่ง "Authorization: Bearer <jwt>"
server verify signature (ไม่ต้อง lookup DB)
decode payload → user info
Logout:
client ลบ token
(server revoke ลำบาก — ต้องมี state)ข้อดี:
- ✅ Stateless — scale ง่าย
- ✅ ไม่ต้อง DB lookup
- ✅ Cross-service (microservices)
ข้อเสีย:
- ❌ Revoke ไม่ได้จนกว่าจะหมดอายุ
- ❌ ใหญ่กว่า session ID (1KB+)
- ❌ เก็บฝั่ง client ไม่ปลอดภัย → XSS risk
เลือกใช้แบบไหน?
Session:
- Traditional web app (server-rendered)
- ต้อง revoke ทันที
- Single domain
- Sensitive (banking)
JWT:
- SPA + API
- Microservices
- Cross-domain
- Mobile app
Hybrid (แนะนำ — ใช้กันมาก):
- Access token อายุสั้น (JWT, 15 นาที)
- Refresh token อายุยาว (random, ใน HttpOnly cookie + DB)
- Refresh → new access token + rotate refreshSession Fixation — attack ที่ต้องป้องกัน
Attack (4 ขั้น):
- Attacker ขอ session ID จาก server (ก่อน login)
- ส่ง link พร้อม session ID นั้นให้ victim (เช่น
/login?sid=ABC123) - Victim เปิด link แล้ว login — server ใช้ session ID เดิม (ABC123) ผูกกับ identity ของ victim
- Attacker ที่รู้ session ID มาตั้งแต่ขั้น 1 ก็เข้า account victim ได้ทันที
ป้องกัน: ต้อง regenerate session ID หลัง login สำเร็จเสมอ ห้ามใช้ ID เดิมต่อ
Spring Security default ทำให้แล้ว แต่ถ้าตั้งเอง:
java
http.sessionManagement(session -> session
.sessionFixation().migrateSession() // ✅ default ของ Spring Security
// หรือ .newSession() = สร้าง session ใหม่ทิ้ง attribute เก่า
);12. JWT — เจาะลึก
โครงสร้าง
header.payload.signature
Header: {"alg":"HS256","typ":"JWT"}
Payload: {"sub":"42","exp":1893456000,"role":"user"} // exp = 2030-01-01 (ตัวอย่าง)
Signature: HMACSHA256(base64(header)+"."+base64(payload), secret)
Final: eyJhbGc...eyJzdWIi...abcdef💡
eyJhbGc...คืออะไร: = base64url encoding ของ JSON ข้างบน (header + payload) base64 ≠ การเข้ารหัส (encryption) — ใครก็ decode กลับเป็น JSON ได้ ใน 1 บรรทัด สิ่งที่ป้องกันการแก้ไขคือ signature — ถ้าใครแก้ payload, signature จะไม่ตรง → server reject
→ Decode ก็อ่านได้! (ไม่ได้ encrypt — แค่ signed)
→ Verify → ถ้าโดนแก้จะรู้ทันที (signature ไม่ตรง)
Algorithm — เลือกอันไหน
EdDSA — Ed25519 ⭐ ตัวที่แนะนำสำหรับ system ใหม่ (เร็ว, key เล็ก, ปลอดภัย)
ES256 — ECDSA P-256 — สมัยใหม่ asymmetric, key เล็ก, support เยอะ
RS256 — RSA SHA-256 — asymmetric legacy แต่ compat สูงสุด
HS256 — HMAC SHA-256 — symmetric (secret เดียว) สำหรับ single service เท่านั้นภาษาคน — เลือกตามสถานการณ์:
- HS256 (symmetric) = ใช้ secret เดียว ทั้ง sign และ verify → service เดียวที่ทั้ง issue และ verify เอง
- RS256/ES256/EdDSA (asymmetric) = sign ด้วย private key ฝั่ง auth server, verify ด้วย public key ที่กระจายให้ทุก service → microservices ที่ issuer กับ verifier คนละ service
→ App ใหม่ปี 2026: EdDSA (Ed25519) ก่อน; fallback ES256 → RS256 ถ้า library ไม่รองรับ; HS256 เฉพาะ monolith เท่านั้น → Auth0, Keycloak, Authelia ปี 2026 รองรับ Ed25519 แล้ว
Claims (ข้อมูลใน Payload)
json
{
"iss": "https://api.example.com", // issuer
"sub": "42", // subject (user ID)
"aud": "https://app.example.com", // audience
"exp": 1893459600, // expires at (2030-01-01 01:00 UTC ตัวอย่าง)
"nbf": 1893456000, // not before (2030-01-01 00:00 UTC ตัวอย่าง)
"iat": 1893456000, // issued at
"jti": "abc-123", // JWT ID (สำหรับ revocation)
// Custom
"role": "admin",
"email": "alice@example.com"
}ปัญหาความปลอดภัยของ JWT
1. เก็บที่ฝั่ง Frontend ผิดที่
❌ localStorage → XSS อ่านได้
❌ sessionStorage → เหมือนกัน
⚠️ Cookie (non-HttpOnly) → XSS อ่านได้
✅ HttpOnly + Secure + SameSite cookie ← วิธีที่ปลอดภัยที่สุด
⚠️ Memory (Zustand / React state) → marginally safer เท่านั้น
- หาย ตอน reload (window สั้นกว่า) แต่ **XSS ยังอ่านได้** เพราะ run ใน same-origin JS context
- ไม่ใช่ "XSS-resistant" — แค่ลด attack window🔑 กฎทอง: ถ้า XSS เข้าถึง JS context ของเราได้ → ทุก storage ฝั่ง JS โดน (localStorage, sessionStorage, memory) · เฉพาะ HttpOnly cookie ที่ JS อ่านไม่ได้เลย
2. Algorithm Confusion — ช่องโหว่คลาสสิก (สับสนชนิด algorithm)
💡 อ่านบทที่ 4 (Cryptography) ก่อน ถ้ายังไม่คุ้นคำว่า symmetric / asymmetric / HMAC / public key — เนื้อหาด้านล่างใช้ศัพท์เหล่านี้ครบ
Algorithm confusion สั้น ๆ:
- เป้าหมาย: หลอกให้ server ตีความ algorithm ของลายเซ็นผิดชนิด
- กลไก: เปลี่ยนจากแบบ asymmetric (RS256 — sign ด้วย private, verify ด้วย public) เป็น symmetric (HS256 — secret เดียวทั้ง sign + verify)
- ผลลัพธ์: attacker เอา public key ของ server (ซึ่งเปิดเผยอยู่แล้ว) มาใช้แทน HMAC secret → ปลอมลายเซ็นได้
- เกิดเพราะ: server ใช้ตัวแปร
keyเดียวกันสำหรับทั้ง 2 algorithm — RS256 path ใช้เป็น public key, HS256 path กลายเป็น HMAC secret
🔍 Attack flow: เมื่อ attacker เปลี่ยน
algจาก RS256 → HS256กฎทอง: server ต้อง ระบุ algorithm ที่ยอม เอง (whitelist) ไม่ใช่อ่านจาก header ของ token
javascript
// ❌ Vulnerable — ตัวแปร `key` เดียวกันใช้ได้ทั้ง 2 paths
const publicKey = fs.readFileSync('rsa-public.pem');
jwt.verify(token, publicKey, { algorithms: ['HS256', 'RS256'] });
// ^^^^^^^^^
// - ถ้า token มี alg=RS256 → ใช้เป็น public key (ถูก)
// - ถ้า token มี alg=HS256 → กลายเป็น HMAC secret (ช่องโหว่!)
// attacker sign token ด้วย publicKey เดียวกันได้
// ✅ Fix: pin algorithm — ไม่ให้ verifier เลือก alg จาก header
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// ^^^^^^^ whitelist เดียว🔑 กฎทอง: server ต้อง ระบุ algorithm ที่ยอม เอง (whitelist) ไม่ใช่อ่านจาก header ของ token
3. "none" Algorithm
{"alg":"none"} ← ช่องโหว่เก่า
server skip signature check → token อะไรก็ผ่าน
Fix: reject "none" explicitly — pin algorithm whitelist เสมอ ไม่อนุญาต "none"4. Token หมดอายุแต่ยังใช้ได้
java
// ❌
jwtParser.parse(token); // ไม่ check exp
// ✅ (jjwt 0.12+ ใช้ parseSignedClaims; 0.11.x เก่ากว่าใช้ parseClaimsJws)
jwtParser.parseSignedClaims(token); // check exp อัตโนมัติ5. Token Leak ใน URL
❌ ?token=xxx ใน URL (อยู่ใน log, history, referrer)
✅ Authorization header เท่านั้น6. Token อายุยาวเกิน
❌ exp = 1 ปี
✅ exp = 15 นาที + refresh token7. JWKS + Key Rotation (asymmetric JWT)
JWKS (JSON Web Key Set) = endpoint ที่ auth server publish public key สำหรับ verify JWT — ใช้กับ RS256/ES256/EdDSA
GET https://auth.example.com/.well-known/jwks.json
→ ส่งคืน array ของ public key พร้อม "kid" (key ID)Pitfall ที่พบบ่อย:
- ❌ Cache JWKS ตลอดไป — เมื่อ auth server rotate key (เช่นทุก 90 วัน) เราจะ verify token ใหม่ไม่ได้
- ❌ ไม่ validate
kid(key ID) ใน header — attacker อาจ inject key ของตัวเอง - ✅ Cache JWKS แบบ short TTL (5-15 นาที) + refetch ถ้าเจอ
kidใหม่ - ✅ ตรวจสอบว่า
kidใน header ตรงกับ key ใน JWKS — reject ถ้าไม่เจอ
8. Audience (aud) Claim ไม่ validate
Token ที่ออกให้ Service A (aud: "service-a") ถูกส่งไปใช้ที่ Service B
→ ถ้า Service B ไม่ check aud → ยอมรับ → ช่องโหว่ "token confusion"✅ verify aud ใน token ตรงกับชื่อ service ของเราเสมอ
9. ไม่ check nbf (not before)
nbf = "not before" — token ใช้ได้หลังเวลานี้
- ใช้กับ pre-issued token (ออกล่วงหน้า) เช่นออก token ตอน register, ใช้ได้หลัง verify email
- library ส่วนใหญ่ check default แต่ บาง impl ข้าม — verify เอง ถ้า security-critical
10. DPoP (อ่าน "ดี-ป็อป") — Token Binding (RFC 9449)
ปัญหา: ถ้า attacker ขโมย access token (ผ่าน XSS, log leak) → ใช้ token นั้นได้ทันทีเพราะ JWT ไม่ผูกกับใคร
DPoP = client สร้าง key pair, ใส่ public key ใน token, ทุก request ต้องแนบ proof ที่ sign ด้วย private key → attacker ที่มีแค่ token ใช้ไม่ได้ (ต้องมี private key ด้วย)
Authorization: DPoP eyJ... (access token)
DPoP: eyJ... (proof JWT — sign สด ๆ ต่อ request)→ ใช้กับ high-value API; Spring Authorization Server 1.3+ รองรับแล้ว
กลยุทธ์ Revoke
Stateless = revoke ไม่ได้โดยตรง มี option:
- Token อายุสั้น + refresh token (วิธีที่ใช้กันมาก)
- Token blacklist (Redis) — เพิ่ม state แต่ทำได้
- Token version per user — เพิ่ม version เพื่อ revoke ทุก token
java
// Option 3 — version
class User {
int tokenVersion; // เพิ่มขึ้นตอน logout-all
}
// JWT มี user's tokenVersion
// ตอน verify — check JWT version == user.tokenVersion13. Refresh Token Pattern
JWT แบบ stateless มีปัญหา — ถ้าตั้งอายุยาวก็ revoke ยาก ถ้าสั้นก็ต้อง login บ่อย refresh token pattern แก้ด้วยการมี 2 token: access token อายุสั้น (15 นาที) ใช้งานจริง และ refresh token อายุยาว (เก็บใน DB, revoke ได้) ไว้ขอ access token ใหม่ พร้อมเทคนิค rotation ที่จับการขโมย token ได้:
1. Login:
→ Access token (JWT, 15 นาที)
→ Refresh token (random 256-bit, 7 วัน, HttpOnly cookie)
server save refresh hash ใน DB
2. ใช้ access token จนกว่าจะหมดอายุ
3. Access หมดอายุ → 401
client → POST /refresh (พร้อม refresh cookie)
server:
- verify refresh token จาก DB
- ออก access token ใหม่
- rotate refresh token (สร้างใหม่, ลบเก่า)
- return ทั้ง 2 ตัว
4. Logout:
server ลบ refresh จาก DB
client ลบ token ทั้งคู่Token Rotation (สำคัญ)
ถ้า refresh token ถูกใช้ซ้ำ 2 ครั้ง → แสดงว่าโดนขโมย → revoke ทั้งหมดของ userjava
public AuthResponse refresh(String refreshToken) {
RefreshToken stored = refreshRepo.findByTokenHash(hash(refreshToken))
.orElseThrow();
if (stored.isUsed()) {
// ตรวจพบ token reuse → suspect theft!
refreshRepo.deleteByUserId(stored.getUserId()); // revoke ทุก session
throw new SecurityException("Token reused - all sessions revoked");
}
stored.markUsed();
refreshRepo.save(stored);
return new AuthResponse(
jwtService.generate(stored.getUser()),
createRefreshToken(stored.getUser()) // ใหม่
);
}⚠️ Edge case: 2 tab เปิดพร้อมกัน → race condition User เปิด 2 tab ใช้ refresh token เดียวกันพร้อมกัน → ตัวที่ส่งทีหลังจะถูกตี false positive ว่า "reuse → token ถูกขโมย" → revoke ทั้ง user → user โดนเตะออกหมด
Mitigation 1 — Grace period (10-30 วินาที):
java
// schema: refresh_tokens(id, user_id, token_hash, previous_token_hash, rotated_at, used)
public AuthResponse refresh(String refreshToken) {
String hash = hash(refreshToken);
// หา token: current หรือ previous (ภายใน grace window)
RefreshToken stored = refreshRepo
.findByTokenHash(hash)
.or(() -> refreshRepo.findByPreviousTokenHashAndRotatedAtAfter(
hash, Instant.now().minusSeconds(30))) // 30s grace
.orElseThrow();
// ถ้าเจอที่ previous_token_hash → อยู่ใน grace, return token เดิมที่ rotate ไปแล้ว
if (stored.matchesPrevious(hash)) {
return new AuthResponse(stored.getCurrentAccessToken(), stored.getCurrentRefreshToken());
}
// ถ้าเจอที่ current แต่ used → reuse จริง → revoke ทั้ง family
if (stored.isUsed()) {
refreshRepo.deleteByFamilyId(stored.getFamilyId());
throw new SecurityException("Token reused - family revoked");
}
// rotate ปกติ
stored.setPreviousTokenHash(hash);
stored.setRotatedAt(Instant.now());
stored.markUsed();
// ... ออก token ใหม่
}Mitigation 2 — Family tracking: ทุก refresh token มี familyId (UUID) ที่ผูกกับ login session เดียว — revoke ทั้ง family เมื่อ detect reuse จริง ๆ (ไม่ revoke ทุก device ของ user)
Part 2: OAuth2 + OIDC
14. OAuth 2.1 — Authorization Framework
(ดู Spring Boot บทที่ 07 เพื่อรายละเอียดเต็ม)
📌 ปี 2026 = ยุค OAuth 2.1 (BCP RFC 9700, 2024):
- PKCE บังคับสำหรับทุก client — ทั้ง public (SPA/mobile) และ confidential (web app ที่มี backend)
- Implicit grant (
response_type=token) deprecated- Resource Owner Password Credentials grant (ส่ง username/password ตรง ๆ) deprecated
- Refresh token rotation เป็นมาตรฐาน
- เอกสารเก่าที่เขียนว่า "PKCE สำหรับ SPA/mobile เท่านั้น" = ล้าสมัยแล้ว
OAuth = "User อนุญาตให้ app ของเราเข้าถึงข้อมูลของเขาบน Service X — โดยไม่ต้องให้ password"
ตัวอย่าง: "Spotify ขอ access เพลย์ลิสต์ของคุณบน Spotify"
- user authenticate กับ Spotify (ไม่ใช่ app ของเรา)
- Spotify ถาม "อนุญาตให้ App นี้?"
- user กด "Allow"
- app เราได้ access token (scope จำกัด)
- app เราเรียก Spotify API ในนามของ user4 Role
Resource Owner — User
Client — App ของเรา
Auth Server — service ที่ authenticate (Google, Facebook)
Resource Server — service ที่มี data (Google Drive API)Flow ที่ใช้กัน (OAuth 2.1)
Authorization Code + PKCE ⭐ ใช้ทุก client — ทั้ง SPA/mobile และ web app ที่มี backend Client Credentials (server-to-server, ไม่มี user) Authorization Code (ไม่มี PKCE) — legacy, OAuth 2.1 ไม่แนะนำ Implicit — deprecated Resource Owner Password — deprecated
Authorization Code + PKCE (default ของ OAuth 2.1)
ทำไม PKCE (อ่าน "พิก-ซี่" — Proof Key for Code Exchange) — แยกเป็นขั้น:
- ก่อนเริ่ม OAuth — client สร้าง ค่าลับ (
code_verifier) เก็บไว้ที่ตัวเอง - ส่งแค่ hash (
code_challenge= SHA-256 ของ verifier) ไป auth server ตอน /authorize - ตอนแลก token — client ส่ง ค่าลับจริง (verifier) ให้ auth server
- Auth server เช็คว่า SHA-256(verifier) ตรงกับ challenge ที่เก็บไว้ → ถ้าตรงค่อยออก token
ป้องกันอะไร? ถ้า attacker ดัก authorization code ระหว่างทาง (เช่น log, browser history, network sniff) → attacker ไม่มี code_verifier → แลก token ไม่ได้
state + nonce + PKCE = trio ที่ต้องมาคู่กัน
| Parameter | ป้องกัน | ส่งใน |
|---|---|---|
| state | CSRF ของ OAuth flow (attacker ส่ง code มาให้ victim) | authorize request + callback |
| nonce (ค่าใช้ครั้งเดียว — number used once) | Replay attack ของ OIDC id_token | authorize request + verify ใน id_token claim |
| PKCE (code_challenge) | Code interception (attacker ดัก authorization code) | authorize request + token request |
⚠️ Production ใช้ library — อย่าเขียน OAuth เอง:
- JS/TS:
oauth4webapi(panva — author เดียวกับjose)- Java/Spring Boot: Spring Security OAuth2 Client (
spring-boot-starter-oauth2-client) ตั้งstate+nonce+ PKCE ให้อัตโนมัติ- Python:
authlib- Go:
golang.org/x/oauth2basic ที่ใช้ Spring Security OAuth2 Client: ทั้ง 3 ตัว (state/nonce/PKCE) เปิด default แล้ว — ไม่ต้อง implement เอง · บทนี้แค่ให้รู้ว่า "เบื้องหลังมีอะไร" จะได้ debug ได้เมื่อพัง
🔐 Storage warning: ตัวอย่างใช้
sessionStorageเพื่อให้อ่านง่าย — แต่ XSS เข้าถึงได้ เช่นเดียวกับ localStorage Production ที่ปลอดภัยกว่า: ส่ง state/nonce/verifier ไป backend (server-side session หรือ short-lived encrypted HttpOnly cookie) แล้วให้ backend handle การแลก token
ตัวอย่างด้านล่างใช้ Web Crypto API จริง (รันได้ทั้ง browser และ Node 19+):
typescript
// helper: base64url ของ ArrayBuffer
function base64url(buf: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// helper: random string (43-128 chars สำหรับ PKCE)
function randomString(byteLength: number): string {
const arr = new Uint8Array(byteLength);
crypto.getRandomValues(arr);
return base64url(arr.buffer);
}
// helper: SHA-256 → base64url (PKCE S256 method)
async function sha256Base64url(input: string): Promise<string> {
const data = new TextEncoder().encode(input);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64url(hash);
}
// ตัวอย่างครบทั้ง 3 (state + nonce + PKCE)
const state = randomString(32);
const nonce = randomString(32);
const codeVerifier = randomString(64); // ขั้นต่ำ 43, สูงสุด 128 chars
const codeChallenge = await sha256Base64url(codeVerifier);
// ⚠️ sessionStorage อ่านได้ด้วย XSS — production ควรใช้ HttpOnly cookie ผ่าน backend
sessionStorage.setItem('oauth_state', state);
sessionStorage.setItem('oauth_nonce', nonce);
sessionStorage.setItem('oauth_verifier', codeVerifier);
// ส่ง authorize request
window.location.href = `${authUrl}?` + new URLSearchParams({
client_id, redirect_uri, response_type: 'code',
scope: 'openid profile',
state, nonce,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
// ตอน callback: verify state + nonce ใน id_token
if (callbackState !== sessionStorage.getItem('oauth_state')) throw new Error('CSRF');
// ... POST /token พร้อม code + code_verifier (ค่าลับจริง)
// ... verify id_token.nonce === sessionStorage.getItem('oauth_nonce')15. OIDC 1.0 — OpenID Connect
OAuth เดิมออกแบบมาเพื่อ "authorization" (ให้แอปเข้าถึงข้อมูล) ไม่ใช่ "authentication" (บอกว่า user คือใคร) — OIDC เติม identity layer บน OAuth ผ่าน ID token (JWT ที่บอกตัวตน user) ทำให้ทำ "Login with Google" ได้อย่างถูกต้อง ส่วนนี้อธิบาย ID token และ flow:
OIDC = "OAuth 2.1 + Identity Layer"
OAuth2: "App มี access ข้อมูล user" (authorization)
OIDC: "User คนนี้คือ X" (authentication + authorization)ID Token
json
{
"iss": "https://accounts.google.com",
"sub": "12345",
"aud": "client_id_of_my_app",
"email": "alice@gmail.com",
"name": "Alice",
"picture": "https://...",
"iat": 1893456000,
"exp": 1893459600
}→ JWT ที่บอก identity ของ user
"Login with Google" Flow
1. user คลิก "Login with Google"
2. redirect ไป Google
3. user authenticate + consent
4. Google redirect กลับมาพร้อม code
5. server แลก code → access token + ID token
6. server verify ID token (issuer, audience, signature)
7. server สร้าง session / JWT ของ app เรา→ ใช้ OIDC สำหรับ sign-in, OAuth2 สำหรับ API access
16. SAML 2.0 (อ่าน "แซม-เอ็ล" หรือ "เอส-เอ-เอ็ม-แอล") — Enterprise SSO
เก่า (ตั้งแต่ปี 2005) แต่ enterprise ใช้กันเยอะ:
ศัพท์ในรูป: SP = Service Provider (ผู้ให้บริการ — คือแอปของเราที่ user อยากเข้า), IdP = Identity Provider (ผู้ให้ตัวตน — ระบบกลางที่ยืนยันว่า user คือใคร เช่น Okta), Assertion = ใบรับรอง (XML ที่ IdP เซ็นว่า "คนนี้ login ผ่านแล้ว")
User → App → "Login" → SAML Service Provider (SP = แอปเรา)
↓
SAML Identity Provider (IdP = ระบบยืนยันตัวตนกลาง) — Okta, AD FS, Auth0
↓
user authenticate (ยืนยันตัวตนที่ IdP)
↓
SAML Assertion (ใบรับรอง — XML, signed = เซ็นลายเซ็นแล้ว)
↓
app รับ assertion → สร้าง sessionเทียบกับ OIDC:
- ✅ เก่ากว่า + enterprise tool รองรับเยอะ
- ❌ XML (ซับซ้อน)
- ❌ ใช้กับ mobile ลำบาก
- ❌ browser-based เท่านั้น
ปี 2026 enterprise = ผสม SAML (legacy) + OIDC (app ใหม่)
📌 เทรนด์ 2024-2026: Microsoft Entra ID และ Google Workspace ผลักดัน OIDC แทน SAML — Entra ID ยังรองรับ SAML แต่ feature ใหม่ออก OIDC ก่อนทุกครั้ง · app ใหม่แนะนำ OIDC; ใช้ SAML เฉพาะตอนต้อง integrate กับ legacy IdP ที่ยังไม่รองรับ OIDC
Part 3: Account Lifecycle
17. Signup
การสมัครสมาชิกเป็นจุดแรกของ account lifecycle และมีจุดที่พลาดง่าย — ต้อง validate email, เช็ค password กับ breach list, และที่สำคัญคือต้อง verify email ก่อนให้สิทธิ์ใช้งานเต็ม flow ด้านล่างคือขั้นตอนที่ปลอดภัย:
1. validate email (format)
2. check breach list (password)
3. generate verification token (random, expire 24 ชม.)
4. save user, status = UNVERIFIED
5. ส่ง verification email
6. user คลิกลิงค์ → status = VERIFIED⚠️ อย่าให้ access จนกว่าจะ verify (หรือจำกัด feature)
18. Password Reset
1. user กรอก email
2. แสดง "ถ้ามี account จริง เราส่ง email ไปแล้ว" (ห้าม leak ว่ามีหรือไม่มี)
3. generate token (random 256-bit, expire 1 ชม., ใช้ครั้งเดียว)
4. save token hash ใน DB
5. ส่ง email พร้อมลิงค์: /reset?token=xxx
6. user คลิก → form กรอก password ใหม่
7. POST พร้อม token → verify + reset password
8. invalidate session / refresh token ทั้งหมดของ user คนนี้
9. ส่ง email แจ้งเตือน "Password ถูกเปลี่ยน"⚠️ Token requirements:
- Random 256-bit (ไม่ใช่ "1234")
- Hash ใน DB (ไม่เก็บ plain)
- Expire 1 ชั่วโมง
- ใช้ครั้งเดียว
- ผูกกับ user
19. Email Change
การเปลี่ยน email อันตรายกว่าที่คิด เพราะถ้า attacker ยึด account ได้ชั่วคราว เขาจะเปลี่ยน email เพื่อ lock เจ้าของจริงออก — กุญแจคือต้อง verify email ใหม่ และ "แจ้งเตือน email เก่า" ด้วยเสมอ เพื่อให้เจ้าของจริงรู้ตัวและยกเลิกได้:
1. user ขอเปลี่ยน email
2. ส่ง verification link ไป email "ใหม่"
3. ส่ง notification ไป email "เก่า" ("ถ้าไม่ใช่คุณ click ที่นี่")
4. หลัง verify email ใหม่ → update⚠️ ต้อง notify ทั้ง 2 email — attacker อาจมี email ใหม่
20. Account Deletion
1. require password / MFA (re-authenticate)
2. soft delete ก่อน (30 วัน)
3. หลัง 30 วัน → hard delete (GDPR compliance)
4. anonymize related data (order, comment) — เก็บไว้แต่ unlink user
5. notify user (ทาง secondary email)GDPR: user มีสิทธิ์ลบข้อมูล (มีข้อยกเว้นบางอย่าง เช่น legal record)
21. Account Recovery — ส่วนที่ยากที่สุด
account recovery คือจุดอ่อนที่สุดของระบบ auth — เพราะมันเป็น "ทางลัด bypass password" โดยตั้งใจ ถ้าออกแบบหละหลวม attacker จะใช้มันยึด account แทนที่จะเดา password recovery จึงต้องปลอดภัยกว่า password เอง (เช่นเลี่ยง SMS เพราะ SIM swap, เลี่ยง security question เพราะหาคำตอบได้ใน social media):
Recovery = วิธี bypass auth — ต้องปลอดภัย กว่า password เอง:
Common flows:
1. Email reset (ใช้บ่อยสุด)
2. SMS reset (ไม่ปลอดภัย — SIM swap)
3. Recovery codes (set ตอน signup)
4. Security questions ❌ (อย่าใช้ — คำตอบหาเจอใน social media)
5. Identity verification (upload ID, video call) — สำหรับ critical app
6. Account recovery contacts (เพื่อนช่วย verify — แบบ Apple)Part 4: Production Considerations
22. Audit Log
ระบบ auth ต้องเก็บ audit log ของทุกเหตุการณ์สำคัญ (login, logout, เปลี่ยน password, ตั้ง MFA) — ทั้งเพื่อ debug, สืบสวนเมื่อถูกโจมตี และแสดง "activity history" ให้ user ตรวจสอบเอง ส่วนนี้แสดง schema และรายการที่ควร log:
java
@Entity
class AuthAuditLog {
Long userId; // ใคร
String action; // LOGIN, LOGOUT, MFA_SETUP, PASSWORD_CHANGE
String result; // SUCCESS, FAILURE
String ipAddress;
String userAgent;
Instant timestamp;
String reason; // เหตุผลกรณี fail
}ควร log:
- Login (success + failure)
- Logout
- MFA enable / disable
- Password change
- Email change
- Permission grant
- Token issue / refresh
→ แสดงให้ user ดูได้: "Activity history"
23. Rate Limiting
endpoint ของ auth (login, reset, signup) เป็นเป้าหมายของ brute-force และ abuse — rate limiting จำกัดจำนวนครั้งต่อ IP/account เพื่อกันเดา password และสร้าง account ปลอม ค่าด้านล่างเป็นจุดเริ่มที่เหมาะสม ควรจำกัดทั้งต่อ IP และต่อ account:
Login:
- 5 ครั้ง / นาที / account ← เน้นต่อ account เป็นหลัก
- 100 ครั้ง / นาที / IP (เริ่มต้น) ← per-IP เป็น safety net
- 100 ครั้ง / วัน / IP
Password reset:
- 3 ครั้ง / ชั่วโมง / email
Sign-up:
- 5 ครั้ง / ชั่วโมง / IP
- CAPTCHA หลัง fail 2 ครั้ง⚠️ ระวัง shared IP (corporate NAT, mobile carrier, university Wi-Fi): NAT/CGNAT ใหญ่อาจรวม user หลายพันคนภายใต้ IP เดียว — ถ้าตั้ง per-IP limit ที่ 5/นาทีจะ lock user ที่บริสุทธิ์ → ใช้ per-account limit เป็นหลัก (5/นาที) + per-IP เป็นเพดานหลวม (100+/นาที) → high-risk endpoint (admin login) อาจตั้งเข้มกว่านี้
Library implement:
- Redis token bucket — distributed rate limit ข้าม instance
- Bucket4j — Java library implement token bucket algorithm (in-memory หรือ Redis-backed)
24. Suspicious Activity Detection
นอกจาก rate limit ระบบที่ดีควรตรวจจับ "พฤติกรรมผิดปกติ" เชิงรุก — login จากประเทศใหม่, ตำแหน่งที่เป็นไปไม่ได้ (impossible travel), หรือ fail หลายครั้งแล้วสำเร็จ เมื่อเจอก็แจ้งเตือน user หรือบังคับ MFA ซ้ำ เพื่อจับการยึด account ก่อนเกิดความเสียหาย:
ตรวจจับ + alert / block:
- Login จากประเทศใหม่
- Login จากตำแหน่งที่เป็นไปไม่ได้ (NYC → Tokyo ใน 1 ชม.)
- Fail หลายครั้งแล้วสำเร็จ
- Login เวลาผิดปกติ
- หลายอุปกรณ์พร้อมกัน
- จาก Tor / VPNAction:
- Notify user ทาง email
- บังคับ MFA re-challenge
- Lock account
- Force password reset
25. CAPTCHA
Option สมัยใหม่:
- Cloudflare Turnstile ⭐ — ฟรี + privacy-friendly แนะนำสุดสำหรับ general use ปี 2026
- hCaptcha — privacy-friendly, free tier
- reCAPTCHA Enterprise — Google ผลักดันแทน v3 (ที่อยู่ใน maintenance mode แล้ว)
- reCAPTCHA v3 — invisible, ใช้ score (maintenance mode — ไม่แนะนำสำหรับ project ใหม่)
- PoW captcha (proof-of-work, ไม่ tracking) — เช่น mCaptcha, Friendly Captcha
ใส่ที่:
- Signup
- Password reset
- หลัง fail login 3 ครั้ง
26. ข้อผิดพลาดที่พบบ่อย
| ❌ | ✅ |
|---|---|
| เก็บ password เป็น plain | BCrypt / Argon2 |
| SHA256 password | hash ที่ slow + memory-hard |
| "No user" vs "Wrong password" ต่าง message | message เดียวกันเสมอ |
| JWT ใน localStorage | HttpOnly cookie หรือ memory |
| JWT อายุยาว | สั้น + refresh |
| Trust algorithm ใน JWT header | pin algorithm ฝั่ง server |
| ไม่มี MFA option | offer TOTP + backup codes |
| Security question | ใช้ email reset |
| ไม่มี audit log | log ทุก auth event |
| Password policy เดียวกันทุกแอป | ใช้ NIST guideline สมัยใหม่ |
27. Checkpoint
🛠️ Checkpoint 2.1 — ทำ Auth ครบ flow
Build:
- Signup + email verification
- Login + rate limit + lockout
- BCrypt password
- JWT (15 นาที) + refresh (7 วัน)
- Logout (invalidate refresh)
🛠️ Checkpoint 2.2 — เพิ่ม MFA
- Enable / disable TOTP
- Generate QR code
- Verify ตอน login
- Backup codes
- Recovery flow
🛠️ Checkpoint 2.3 — Password Reset
- Request → email + token
- Token expire 1 ชม., ใช้ครั้งเดียว
- Reset → invalidate ทุก session
- Notification ไป user
🛠️ Checkpoint 2.4 — OAuth Login
- "Login with Google" ผ่าน OIDC
- Link / unlink กับ email account
- Handle case user มี email ซ้ำ
🛠️ Checkpoint 2.5 — Test การโจมตี
- Brute force → lockout ทำงาน
- Token reuse → revoke
- Expired token → reject
- Wrong algorithm → reject
28. สรุปบท
✅ Hash password ด้วย Argon2id (OWASP 2024 default) / scrypt / BCrypt (cost ≥ 12, target ~250ms-1s) — ห้ามเก็บ plain หรือ SHA ✅ NIST SP 800-63B Rev.4 (2024): ขั้นต่ำ 8 (15 สำหรับ high-assurance), ไม่บังคับ complexity/rotation, check breach (HIBP K-anonymity) ✅ MFA: TOTP (RFC 6238) + backup codes — SMS deprecated (SS7/SIM-swap, NIST 800-63B Rev.4 ไม่แนะนำ) ✅ Passkeys / WebAuthn 2.0 = phishing-resistant, CISA บังคับสำหรับ federal — preferred ปี 2026 ✅ Session vs JWT — JWT สำหรับ SPA/microservices, session สำหรับ traditional ✅ JWT pitfalls: alg:none, algorithm confusion (HS256 + RSA pub key), JWKS rotation, kid validation, aud claim, exp/nbf, key rotation, HttpOnly cookie storage ✅ DPoP (RFC 9449) สำหรับ token binding ใน high-value API ✅ Refresh token rotation + grace period + family tracking — ตรวจจับการขโมยไม่เผลอ revoke ผู้บริสุทธิ์ ✅ OAuth 2.1 (RFC 9700, 2024): PKCE บังคับสำหรับ ทุก client (public + confidential), implicit/password grant deprecated ✅ OIDC 1.0 สำหรับ sign-in ("Login with Google") ✅ SAML 2.0 สำหรับ enterprise SSO (legacy — เทรนด์ย้ายไป OIDC) ✅ Account lifecycle: signup → verify → use → reset → delete ✅ Audit log + rate limit (per-account หลัก + per-IP เพดาน) + suspicious detection — จำเป็น ✅ Error message เดียวกันสำหรับ auth fail (กัน enumeration)
📖 Glossary — คำย่อ + คำอ่าน
| คำ | คำอ่าน | ความหมายสั้น |
|---|---|---|
| AuthN | ออธ-เอ็น | authentication (autheNticatioN) — "คุณเป็นใคร?" |
| AuthZ | ออธ-แซด | authorization (authoriZation) — "คุณทำอะไรได้?" |
| OAuth (2.1) | โอ-ออธ-ทู-พอย-วัน | กรอบ authorization ระหว่างแอป — ปี 2026 ใช้ 2.1 (RFC 9700 BCP) |
| OIDC | โอ-ไอ-ดี-ซี | OpenID Connect — identity layer บน OAuth |
| PKCE | พิก-ซี่ | Proof Key for Code Exchange — กัน auth code ถูกขโมย |
| JWT | เจ-ดับเบิลยู-ที (หรือ "จ็อต") | JSON Web Token — token แบบมีลายเซ็น |
| JWKS | เจ-ดับเบิลยู-เค-เอส | JSON Web Key Set — endpoint publish public key สำหรับ verify JWT |
| DPoP | ดี-ป็อป | Demonstrating Proof of Possession (RFC 9449) — bind token เข้ากับ key |
| WebAuthn | เว็บ-ออธ-เอ็น | W3C API สำหรับ passkey / hardware key — passwordless |
| passkey | พาส-คีย์ | credential ที่ใช้ WebAuthn — sync ผ่าน iCloud/Google/1Password |
| FIDO2 | ฟิ-โด-ทู | มาตรฐานจาก FIDO Alliance ที่รวม WebAuthn + CTAP |
| TOTP | โท-ทป หรือ ที-โอ-ที-พี | Time-based One-Time Password (RFC 6238) |
| HOTP | เอช-โอ-ที-พี | HMAC-based OTP — รุ่นพี่ของ TOTP, ไม่ใช้เวลา |
| Argon2id | อาร์-กอน-ทู-ไอ-ดี | password hashing — OWASP 2024 default |
| BCrypt | บี-คริปต์ | password hashing — RFC อยู่นาน, 72-byte limit |
| scrypt | เอส-คริปต์ | password hashing — memory-hard |
| SAML | แซม-เอ็ล / เอส-เอ-เอ็ม-แอล | enterprise SSO ใช้ XML |
| SSO | เอส-เอส-โอ | Single Sign-On |
| SP / IdP | เอส-พี / ไอ-ดี-พี | Service Provider / Identity Provider |
| MFA / 2FA | เอ็ม-เอฟ-เอ / ทู-เอฟ-เอ | Multi-Factor / Two-Factor Authentication |
| AiTM | เอ-ไอ-ที-เอ็ม | Adversary-in-the-Middle phishing (เช่น EvilProxy, Tycoon2FA) |
| SS7 | เอส-เอส-เซเว่น | Signaling System 7 — โปรโตคอลเครือข่ายโทรศัพท์ที่มีช่องโหว่ดัก SMS |
| K-anonymity | เค-แอ-นอ-นิ-มิ-ตี้ | ซ่อนตัวในฝูง — เทคนิคของ HIBP ส่งแค่ prefix 5 ตัว |
| HIBP | เอช-ไอ-บี-พี | Have I Been Pwned — ฐานข้อมูล password leak |
| YubiKey | ยู-บิ-คีย์ | แบรนด์ hardware security key จาก Yubico |
| CISA | ซี-ซ่า | Cybersecurity and Infrastructure Security Agency (สหรัฐ) |
| NIST | นิสต์ | National Institute of Standards and Technology |