Skip to content

บทที่ 2 — Authentication ลึก

← บทที่ 1 | สารบัญ | บทที่ 3 →

หลังจบบท คุณจะ:

  • เลือก 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 Tokentoken ที่เซ็นลายเซ็นไว้ เก็บข้อมูล 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-Onlogin ครั้งเดียว ใช้ได้หลายระบบ
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

ผู้ชนะ 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 bytes
  • 1 (parallelism, p) = ใช้ 1 thread ตอน hash (ตั้ง > 1 ถ้า CPU มีหลาย core เหลือ)
  • 19456 (memory, m) = ใช้ memory 19456 KiB19 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, Base64

7. 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 ที่มือถือ → กด approve

User-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 รองรับ native

Flow

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 + passkey

Spring 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 shock

Trend: 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 refresh

Session Fixation — attack ที่ต้องป้องกัน

Attack (4 ขั้น):

  1. Attacker ขอ session ID จาก server (ก่อน login)
  2. ส่ง link พร้อม session ID นั้นให้ victim (เช่น /login?sid=ABC123)
  3. Victim เปิด link แล้ว login — server ใช้ session ID เดิม (ABC123) ผูกกับ identity ของ victim
  4. 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 token

7. 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:

  1. Token อายุสั้น + refresh token (วิธีที่ใช้กันมาก)
  2. Token blacklist (Redis) — เพิ่ม state แต่ทำได้
  3. 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.tokenVersion

13. 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 ทั้งหมดของ user
java
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 ในนามของ user

4 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) — แยกเป็นขั้น:

  1. ก่อนเริ่ม OAuth — client สร้าง ค่าลับ (code_verifier) เก็บไว้ที่ตัวเอง
  2. ส่งแค่ hash (code_challenge = SHA-256 ของ verifier) ไป auth server ตอน /authorize
  3. ตอนแลก token — client ส่ง ค่าลับจริง (verifier) ให้ auth server
  4. Auth server เช็คว่า SHA-256(verifier) ตรงกับ challenge ที่เก็บไว้ → ถ้าตรงค่อยออก token

ป้องกันอะไร? ถ้า attacker ดัก authorization code ระหว่างทาง (เช่น log, browser history, network sniff) → attacker ไม่มี code_verifier → แลก token ไม่ได้

state + nonce + PKCE = trio ที่ต้องมาคู่กัน

Parameterป้องกันส่งใน
stateCSRF ของ OAuth flow (attacker ส่ง code มาให้ victim)authorize request + callback
nonce (ค่าใช้ครั้งเดียว — number used once)Replay attack ของ OIDC id_tokenauthorize 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/oauth2

basic ที่ใช้ 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 / VPN

Action:

  • 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 เป็น plainBCrypt / Argon2
SHA256 passwordhash ที่ slow + memory-hard
"No user" vs "Wrong password" ต่าง messagemessage เดียวกันเสมอ
JWT ใน localStorageHttpOnly cookie หรือ memory
JWT อายุยาวสั้น + refresh
Trust algorithm ใน JWT headerpin algorithm ฝั่ง server
ไม่มี MFA optionoffer TOTP + backup codes
Security questionใช้ email reset
ไม่มี audit loglog ทุก 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 JWTJWT สำหรับ 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 APIRefresh 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

← บทที่ 1 | บทที่ 3 → Authorization