Home > Blog > tech

OAuth 2.0 และ OpenID Connect เจาะลึก Authorization Server, Token Flow สำหรับ Developer 2026

oauth2 openid connect deep dive
OAuth 2.0 OpenID Connect Deep Dive 2026
2026-04-08 | tech | 3600 words

OAuth 2.0 เป็นมาตรฐาน Authorization ที่ใช้กันอย่างแพร่หลายที่สุดในโลก ทุกครั้งที่คุณกดปุ่ม "Login with Google" หรือ "Sign in with GitHub" คุณกำลังใช้ OAuth 2.0 อยู่ แต่สำหรับ Developer หลายคน OAuth ยังเป็นเรื่องที่สับสนมาก เพราะมี Grant Types หลายแบบ มี Token หลายประเภท และมี Security Concerns ที่ต้องระวัง

บทความนี้จะเจาะลึก OAuth 2.0 และ OpenID Connect อย่างครบถ้วน ตั้งแต่สถาปัตยกรรมของ Authorization Server, Grant Types ทุกแบบพร้อมตัวอย่าง Code, Token Formats, JWKS, Token Rotation ไปจนถึงมาตรฐานใหม่อย่าง OAuth 2.1, DPoP และ Rich Authorization Requests พร้อมแนวทาง Security Best Practices สำหรับปี 2026

ทบทวน OAuth 2.0 — เข้าใจพื้นฐานให้แน่น

OAuth 2.0 (RFC 6749) เป็น Authorization Framework ไม่ใช่ Authentication Protocol สิ่งที่ OAuth ทำคือ "ให้สิทธิ์เข้าถึงทรัพยากร" (Authorization) ไม่ใช่ "ยืนยันตัวตน" (Authentication) ตัวอย่างเช่น เมื่อ App ขออนุญาตเข้าถึงรูปภาพใน Google Photos ของคุณ OAuth จะจัดการเรื่องการ "ให้สิทธิ์" แต่การ "ยืนยันตัวตน" ว่าคุณคือคุณจริงๆ เป็นหน้าที่ของ OpenID Connect ที่สร้างทับ OAuth อีกชั้น

ตัวละครหลักใน OAuth 2.0 มี 4 บทบาท ได้แก่ Resource Owner คือ User ที่เป็นเจ้าของทรัพยากร เช่น เจ้าของ Google Account Client คือ Application ที่ต้องการเข้าถึงทรัพยากร เช่น App ที่ต้องการดูรูปภาพ Authorization Server คือ Server ที่ออก Token เช่น Google Authorization Server และ Resource Server คือ Server ที่เก็บทรัพยากร เช่น Google Photos API

Authorization Server Components — ส่วนประกอบภายใน

Authorization Server เป็นหัวใจของระบบ OAuth ภายในประกอบด้วยส่วนสำคัญหลายส่วน

Authorization Endpoint คือ URL ที่ Client ส่ง User ไปเพื่อ Login และให้ Consent เช่น /oauth/authorize ทำงานผ่าน Browser ด้วย HTTP Redirect เป็นจุดเริ่มต้นของ Authorization Code Flow

Token Endpoint คือ URL ที่ Client ส่ง Request ไปเพื่อแลก Authorization Code กับ Access Token เช่น /oauth/token ทำงานแบบ Backend-to-Backend ด้วย HTTP POST ไม่ผ่าน Browser

Client Registry คือระบบจัดเก็บข้อมูล Client ที่ลงทะเบียนไว้ รวมถึง Client ID, Client Secret, Redirect URIs, Scopes ที่อนุญาต และ Grant Types ที่รองรับ

Token Store คือระบบจัดเก็บ Token ที่ออกไปแล้ว สำหรับ Opaque Tokens ต้องเก็บไว้เพื่อตรวจสอบ (Introspection) สำหรับ JWT อาจไม่ต้องเก็บถ้าใช้ Signature Verification แทน

Key Management คือระบบจัดการ Cryptographic Keys สำหรับ Sign และ Verify JWT รวมถึง Key Rotation ที่ต้องทำเป็นประจำ เผยแพร่ Public Keys ผ่าน JWKS Endpoint

Grant Types — วิธีการขอ Token แต่ละแบบ

1. Authorization Code + PKCE (แนะนำ)

นี่คือ Grant Type ที่แนะนำสำหรับเกือบทุก Use Case ในปี 2026 ทั้ง Web App, Mobile App และ SPA โดย PKCE (Proof Key for Code Exchange) เป็น Extension ที่ป้องกัน Authorization Code Interception Attack

# ขั้นตอนที่ 1: Client สร้าง Code Verifier และ Code Challenge
import hashlib, base64, secrets

# สร้าง Code Verifier (random string 43-128 ตัวอักษร)
code_verifier = secrets.token_urlsafe(64)

# สร้าง Code Challenge จาก Verifier
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()

# ขั้นตอนที่ 2: Redirect User ไป Authorization Server
auth_url = (
    "https://auth.example.com/oauth/authorize?"
    "response_type=code"
    "&client_id=my-app"
    "&redirect_uri=https://myapp.com/callback"
    "&scope=openid profile email"
    "&state=random-state-value"
    f"&code_challenge={code_challenge}"
    "&code_challenge_method=S256"
)
# → User Login + Consent → Redirect กลับมาพร้อม Authorization Code

# ขั้นตอนที่ 3: แลก Code กับ Token
import requests

token_response = requests.post("https://auth.example.com/oauth/token", data={
    "grant_type": "authorization_code",
    "code": "received-auth-code",
    "redirect_uri": "https://myapp.com/callback",
    "client_id": "my-app",
    "code_verifier": code_verifier,  # ส่ง Verifier เพื่อพิสูจน์
})

tokens = token_response.json()
# {
#   "access_token": "eyJhbGci...",
#   "token_type": "Bearer",
#   "expires_in": 3600,
#   "refresh_token": "dGhpcyBpcyBh...",
#   "id_token": "eyJhbGci..."    ← OpenID Connect
# }

PKCE ทำงานโดย Client สร้าง code_verifier (ค่าลับ) และ code_challenge (Hash ของ Verifier) ส่ง Challenge ไปกับ Authorization Request และส่ง Verifier ไปกับ Token Request Authorization Server จะ Hash Verifier และเปรียบเทียบกับ Challenge ที่ได้รับก่อนหน้า ถ้าตรงกันแสดงว่าเป็น Client เดียวกัน ทำให้แม้จะมีคนดัก Authorization Code ไปก็ไม่สามารถแลกเป็น Token ได้ เพราะไม่มี code_verifier

2. Client Credentials

ใช้สำหรับ Machine-to-Machine Communication ที่ไม่มี User เกี่ยวข้อง เช่น Microservice เรียก Microservice อื่น, Cron Job เรียก API, Backend Service เข้าถึง Shared Resource

# Client Credentials Flow — ง่ายที่สุด
import requests, base64

# Client ส่ง Client ID + Secret เพื่อขอ Token โดยตรง
token_response = requests.post(
    "https://auth.example.com/oauth/token",
    data={
        "grant_type": "client_credentials",
        "scope": "read:data write:data",
    },
    auth=("my-service-id", "my-service-secret"),  # Basic Auth
)

tokens = token_response.json()
# {
#   "access_token": "eyJhbGci...",
#   "token_type": "Bearer",
#   "expires_in": 3600
#   ← ไม่มี refresh_token (ไม่จำเป็น เพราะ Client มี Secret อยู่แล้ว)
# }

# ใช้ Token เรียก API
headers = {"Authorization": f"Bearer {tokens['access_token']}"}
response = requests.get("https://api.example.com/data", headers=headers)

3. Device Code Grant

ใช้สำหรับ Device ที่ไม่มี Browser หรือพิมพ์ลำบาก เช่น Smart TV, IoT Device, CLI Tool ตัวอย่างที่เห็นบ่อยคือ Login YouTube บน Smart TV โดยให้ไปเปิด URL บนมือถือแล้วกรอก Code

# Device Code Flow
import requests, time

# ขั้นตอนที่ 1: ขอ Device Code
device_response = requests.post(
    "https://auth.example.com/oauth/device/code",
    data={
        "client_id": "my-tv-app",
        "scope": "openid profile",
    },
)
device_data = device_response.json()
# {
#   "device_code": "GmRhmhcxhZA...",
#   "user_code": "WDJB-MJHT",          ← แสดงให้ User เห็น
#   "verification_uri": "https://auth.example.com/device",
#   "expires_in": 1800,
#   "interval": 5
# }

# ขั้นตอนที่ 2: แสดง Code ให้ User
print(f"ไปที่: {device_data['verification_uri']}")
print(f"กรอกรหัส: {device_data['user_code']}")

# ขั้นตอนที่ 3: Poll Token Endpoint
while True:
    token_response = requests.post(
        "https://auth.example.com/oauth/token",
        data={
            "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
            "device_code": device_data["device_code"],
            "client_id": "my-tv-app",
        },
    )
    if token_response.status_code == 200:
        tokens = token_response.json()
        print("Login สำเร็จ!")
        break
    elif token_response.json().get("error") == "authorization_pending":
        time.sleep(device_data["interval"])
    else:
        print("Error:", token_response.json())
        break

4. Token Exchange (RFC 8693)

Token Exchange ใช้สำหรับแปลง Token จากรูปแบบหนึ่งเป็นอีกรูปแบบหนึ่ง เหมาะกับสถาปัตยกรรม Microservices ที่ต้องส่ง Token ต่อไปยัง Service อื่นแต่ต้องการจำกัดสิทธิ์ให้เหมาะสม (Downscoping) หรือแปลง Token จากระบบภายนอกเป็น Token ภายในองค์กร

# Token Exchange — แลก Token ข้ามระบบ
import requests

exchange_response = requests.post(
    "https://auth.example.com/oauth/token",
    data={
        "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
        "subject_token": "original-access-token",
        "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
        "audience": "downstream-service",
        "scope": "read:limited",  # ขอ Scope ที่น้อยกว่า
    },
    auth=("gateway-service", "gateway-secret"),
)

# ได้ Token ใหม่ที่มีสิทธิ์จำกัดกว่าเดิม
new_token = exchange_response.json()

Access Token Formats — Opaque vs JWT

Access Token มี 2 รูปแบบหลัก แต่ละแบบมีข้อดีข้อเสียต่างกัน

Opaque Token

Opaque Token เป็น Random String ที่ไม่มีข้อมูลใดๆ ในตัว Resource Server ต้องส่ง Token ไปถาม Authorization Server ทุกครั้ง (Token Introspection) เพื่อตรวจสอบว่า Token ยังใช้งานได้อยู่หรือไม่ ข้อดีคือสามารถ Revoke ได้ทันที ข้อเสียคือต้อง Network Call ทุกครั้ง ทำให้ช้าลงและเป็น Single Point of Failure

# Token Introspection (RFC 7662)
# Resource Server ส่ง Token ไปตรวจสอบ
import requests

introspection = requests.post(
    "https://auth.example.com/oauth/introspect",
    data={"token": "opaque-token-value"},
    auth=("resource-server-id", "resource-server-secret"),
)

result = introspection.json()
# {
#   "active": true,
#   "sub": "user123",
#   "client_id": "my-app",
#   "scope": "read write",
#   "exp": 1712534400,
#   "iat": 1712530800
# }

JWT (JSON Web Token)

JWT เป็น Self-contained Token ที่มีข้อมูลอยู่ในตัว Resource Server สามารถ Verify Token ได้เองโดย Verify Signature ด้วย Public Key ไม่ต้อง Call กลับไปยัง Authorization Server ข้อดีคือเร็วและไม่ต้องพึ่ง Authorization Server ตอน Validate ข้อเสียคือไม่สามารถ Revoke ได้ทันที ต้องรอ Token หมดอายุ (แก้ได้ด้วย Short-lived Token + Refresh Token)

# โครงสร้าง JWT
# Header.Payload.Signature

# Header (Base64URL encoded)
{
  "alg": "RS256",        # Algorithm (RSA SHA-256)
  "typ": "JWT",
  "kid": "key-2026-01"   # Key ID (สำหรับ Key Rotation)
}

# Payload (Base64URL encoded)
{
  "iss": "https://auth.example.com",   # Issuer
  "sub": "user123",                     # Subject (User ID)
  "aud": "https://api.example.com",     # Audience
  "exp": 1712534400,                    # Expiration
  "iat": 1712530800,                    # Issued At
  "scope": "read write",                # Scopes
  "client_id": "my-app"                 # Client ID
}

# Signature
RSASHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  privateKey
)
# Verify JWT ใน Python
import jwt
import requests

# ดึง Public Keys จาก JWKS Endpoint
jwks_response = requests.get("https://auth.example.com/.well-known/jwks.json")
jwks = jwks_response.json()

# Verify Token
try:
    payload = jwt.decode(
        token,
        key=jwks,                              # ใช้ JWKS
        algorithms=["RS256"],                   # อนุญาตเฉพาะ RS256
        audience="https://api.example.com",     # ตรวจ Audience
        issuer="https://auth.example.com",      # ตรวจ Issuer
    )
    print(f"User: {payload['sub']}, Scopes: {payload['scope']}")
except jwt.ExpiredSignatureError:
    print("Token หมดอายุ")
except jwt.InvalidTokenError as e:
    print(f"Token ไม่ถูกต้อง: {e}")

Refresh Token Rotation — ความปลอดภัยของ Token ระยะยาว

Refresh Token ใช้สำหรับขอ Access Token ใหม่เมื่อ Access Token เดิมหมดอายุ โดยไม่ต้องให้ User Login ใหม่ แต่ Refresh Token มีอายุยาวกว่า Access Token มาก (อาจเป็นวัน สัปดาห์ หรือเดือน) จึงต้องระวังเรื่องความปลอดภัยเป็นพิเศษ

Refresh Token Rotation เป็นเทคนิคที่ Authorization Server จะออก Refresh Token ใหม่ทุกครั้งที่มีการใช้ Refresh Token เดิม และ Invalidate Token เดิมทิ้ง ถ้ามีการใช้ Refresh Token ที่ถูก Invalidate ไปแล้ว (อาจถูกขโมย) Authorization Server จะ Revoke Token Family ทั้งหมดเพื่อความปลอดภัย

# Refresh Token Rotation Flow
import requests

def refresh_access_token(refresh_token):
    response = requests.post(
        "https://auth.example.com/oauth/token",
        data={
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
            "client_id": "my-app",
        },
    )
    tokens = response.json()
    # {
    #   "access_token": "new-access-token",
    #   "refresh_token": "NEW-refresh-token",  ← Token ใหม่!
    #   "expires_in": 3600
    # }

    # สำคัญ: เก็บ Refresh Token ใหม่ทันที
    # ถ้าใช้ Token เดิมอีกจะ FAIL และ Token ทั้งหมดถูก Revoke
    save_refresh_token(tokens["refresh_token"])
    return tokens["access_token"]
Best Practice: ตั้ง Access Token ให้หมดอายุเร็ว (5-15 นาที) และใช้ Refresh Token Rotation สำหรับขอ Token ใหม่ ทำให้ถ้า Access Token ถูกขโมย ก็จะใช้ได้แค่ช่วงสั้นๆ เท่านั้น

Scopes และ Consent

Scopes เป็นกลไกที่ใช้จำกัดสิทธิ์การเข้าถึงของ Token แต่ละ Scope แทนสิทธิ์เฉพาะอย่าง เช่น read:profile อ่านข้อมูลโปรไฟล์ write:posts สร้างและแก้ไขโพสต์ delete:account ลบบัญชี เมื่อ Client ขอ Access Token จะระบุ Scopes ที่ต้องการ Authorization Server จะแสดง Consent Screen ให้ User อนุมัติ และ Token ที่ออกมาจะมีเฉพาะ Scopes ที่ User อนุมัติเท่านั้น

# ตรวจสอบ Scope ใน API
from functools import wraps

def require_scope(required_scope):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            token_scopes = get_token_scopes(request)  # ดึง Scopes จาก Token
            if required_scope not in token_scopes:
                return {"error": "insufficient_scope"}, 403
            return f(*args, **kwargs)
        return wrapper
    return decorator

@app.route("/api/profile")
@require_scope("read:profile")
def get_profile():
    return {"name": "User Name", "email": "user@example.com"}

@app.route("/api/posts", methods=["POST"])
@require_scope("write:posts")
def create_post():
    # สร้างโพสต์ใหม่
    pass

OAuth 2.1 — อะไรเปลี่ยนบ้าง?

OAuth 2.1 เป็น Draft ที่รวบรวม Best Practices จาก OAuth 2.0 เข้าด้วยกัน โดยมีการเปลี่ยนแปลงสำคัญดังนี้

ยกเลิก Implicit Grant เนื่องจากไม่ปลอดภัย เพราะส่ง Access Token ผ่าน URL Fragment ที่อาจถูกดักจับได้ ให้ใช้ Authorization Code + PKCE แทน แม้กับ SPA

ยกเลิก Resource Owner Password Credentials (ROPC) เนื่องจากต้องส่ง Username/Password ของ User ให้ Client ซึ่งขัดกับหลักการของ OAuth ที่ไม่ควรให้ Client รู้รหัสผ่านของ User

บังคับใช้ PKCE สำหรับทุก Authorization Code Flow ไม่ใช่เฉพาะ Public Clients อีกต่อไป แม้แต่ Confidential Clients ก็ต้องใช้ PKCE

บังคับ Exact String Matching สำหรับ Redirect URIs ไม่อนุญาตให้ใช้ Wildcard หรือ Subdirectory Matching เพื่อป้องกัน Open Redirect Attack

บังคับ Refresh Token Rotation หรือ Sender-Constraining สำหรับ Public Clients

OpenID Connect — ชั้น Authentication ทับ OAuth

OpenID Connect (OIDC) เป็น Identity Layer ที่สร้างทับ OAuth 2.0 เพิ่มความสามารถด้าน Authentication ที่ OAuth ไม่มี โดยเพิ่ม Component สำคัญหลายอย่าง

ID Token

ID Token เป็น JWT ที่มีข้อมูลตัวตนของ User ต่างจาก Access Token ที่ใช้เข้าถึงทรัพยากร ID Token ใช้เพื่อยืนยันว่า User คือใคร

# ID Token Payload
{
  "iss": "https://auth.example.com",
  "sub": "user123",                    # User ID
  "aud": "my-app-client-id",           # Client ID
  "exp": 1712534400,
  "iat": 1712530800,
  "auth_time": 1712530780,             # เวลาที่ Login จริง
  "nonce": "random-nonce-value",       # ป้องกัน Replay Attack
  "name": "สมชาย ใจดี",                # User's name
  "email": "somchai@example.com",
  "email_verified": true,
  "picture": "https://example.com/photo.jpg",
  "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q"  # Access Token Hash
}

UserInfo Endpoint

UserInfo Endpoint ให้ข้อมูล User เพิ่มเติมที่อาจไม่ได้อยู่ใน ID Token เรียกด้วย Access Token

# ดึงข้อมูล User จาก UserInfo Endpoint
import requests

userinfo = requests.get(
    "https://auth.example.com/oauth/userinfo",
    headers={"Authorization": f"Bearer {access_token}"},
)

user = userinfo.json()
# {
#   "sub": "user123",
#   "name": "สมชาย ใจดี",
#   "email": "somchai@example.com",
#   "email_verified": true,
#   "picture": "https://example.com/photo.jpg",
#   "locale": "th",
#   "updated_at": 1712530800
# }

Discovery Document

OIDC Discovery ทำให้ Client สามารถค้นหา Endpoints ทั้งหมดของ Authorization Server ได้โดยอัตโนมัติจาก URL เดียว

# OpenID Connect Discovery
# GET https://auth.example.com/.well-known/openid-configuration

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/oauth/authorize",
  "token_endpoint": "https://auth.example.com/oauth/token",
  "userinfo_endpoint": "https://auth.example.com/oauth/userinfo",
  "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
  "registration_endpoint": "https://auth.example.com/oauth/register",
  "scopes_supported": ["openid", "profile", "email", "offline_access"],
  "response_types_supported": ["code", "token", "id_token"],
  "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256", "ES256"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "private_key_jwt"],
  "code_challenge_methods_supported": ["S256"]
}

JWKS และ Key Rotation

JWKS (JSON Web Key Set) เป็น Endpoint ที่เผยแพร่ Public Keys ที่ใช้ Verify JWT Token Resource Server จะดึง Keys จาก JWKS Endpoint เพื่อ Verify Signature ของ Token โดยไม่ต้องมี Shared Secret กับ Authorization Server

# JWKS Endpoint Response
# GET https://auth.example.com/.well-known/jwks.json

{
  "keys": [
    {
      "kty": "RSA",                    # Key Type
      "kid": "key-2026-04",            # Key ID
      "use": "sig",                     # Usage: signature
      "alg": "RS256",                   # Algorithm
      "n": "0vx7agoebGcQSuuP...",      # RSA Modulus (Public Key)
      "e": "AQAB"                       # RSA Exponent
    },
    {
      "kty": "RSA",
      "kid": "key-2026-01",            # Key เก่า (ยังใช้ Verify Token เก่าได้)
      "use": "sig",
      "alg": "RS256",
      "n": "1b2M6aINqu7...",
      "e": "AQAB"
    }
  ]
}

Key Rotation คือกระบวนการเปลี่ยน Key ที่ใช้ Sign Token เป็นประจำ (เช่น ทุก 3 เดือน) เพื่อลดความเสี่ยงถ้า Key รั่วไหล ขั้นตอนคือสร้าง Key Pair ใหม่ เพิ่ม Public Key ใหม่ลง JWKS (ยังคง Key เก่าไว้) เริ่ม Sign Token ใหม่ด้วย Key ใหม่ รอจน Token ที่ Sign ด้วย Key เก่าหมดอายุทั้งหมด จากนั้นจึงลบ Key เก่าออกจาก JWKS

สำคัญ: Resource Server ควร Cache JWKS ไว้และ Refresh เป็นระยะ ไม่ควรดึง JWKS ทุก Request เพราะจะทำให้ช้า แต่ถ้าเจอ Token ที่มี kid ไม่ตรงกับ Key ใน Cache ให้ Refresh JWKS ใหม่ทันที

Token Revocation

Token Revocation (RFC 7009) ให้ Client สามารถแจ้ง Authorization Server ให้ Invalidate Token ที่ออกไปแล้ว ใช้ในกรณีเช่น User Logout, เปลี่ยนรหัสผ่าน หรือตรวจพบว่า Token ถูกขโมย

# Revoke Token
import requests

# Revoke Access Token
requests.post(
    "https://auth.example.com/oauth/revoke",
    data={
        "token": "access-token-to-revoke",
        "token_type_hint": "access_token",
    },
    auth=("my-app-id", "my-app-secret"),
)

# Revoke Refresh Token (จะ Revoke Access Token ที่เกี่ยวข้องด้วย)
requests.post(
    "https://auth.example.com/oauth/revoke",
    data={
        "token": "refresh-token-to-revoke",
        "token_type_hint": "refresh_token",
    },
    auth=("my-app-id", "my-app-secret"),
)

Authorization Server Implementation — Keycloak

Keycloak เป็น Open Source Identity and Access Management ที่นิยมที่สุด พัฒนาโดย Red Hat รองรับ OAuth 2.0, OpenID Connect, SAML 2.0 และมี Admin Console สำหรับจัดการ User, Client, Roles, Groups ได้สะดวก

# ติดตั้ง Keycloak ด้วย Docker
docker run -d --name keycloak \
  -p 8080:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:latest \
  start-dev

# เข้า Admin Console: http://localhost:8080/admin

# สร้าง Realm
# 1. Login ด้วย admin/admin
# 2. Create Realm → ตั้งชื่อ "my-app"
# 3. สร้าง Client → client_id: "frontend", "backend"
# 4. สร้าง Users → กำหนด Roles
# ใช้ Keycloak กับ FastAPI
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2AuthorizationCodeBearer
import jwt
import requests

app = FastAPI()

KEYCLOAK_URL = "http://localhost:8080/realms/my-app"
JWKS_URL = f"{KEYCLOAK_URL}/protocol/openid-connect/certs"

oauth2_scheme = OAuth2AuthorizationCodeBearer(
    authorizationUrl=f"{KEYCLOAK_URL}/protocol/openid-connect/auth",
    tokenUrl=f"{KEYCLOAK_URL}/protocol/openid-connect/token",
)

# Cache JWKS
jwks_client = jwt.PyJWKClient(JWKS_URL)

async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        signing_key = jwks_client.get_signing_key_from_jwt(token)
        payload = jwt.decode(
            token,
            signing_key.key,
            algorithms=["RS256"],
            audience="frontend",
        )
        return payload
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="Invalid token")

@app.get("/api/profile")
async def profile(user=Depends(get_current_user)):
    return {
        "sub": user["sub"],
        "name": user.get("name"),
        "email": user.get("email"),
        "roles": user.get("realm_access", {}).get("roles", []),
    }

Auth0 — Managed Solution

Auth0 เป็น Authorization Server แบบ Managed Service ไม่ต้องติดตั้งหรือดูแล Server เอง เหมาะกับทีมที่ต้องการเริ่มต้นเร็วและไม่อยากจัดการ Infrastructure สนับสนุน Social Login (Google, Facebook, GitHub) และ Enterprise SSO (SAML, Active Directory) พร้อมใช้งานทันที มี Free Tier รองรับ 7,500 MAU

Ory Hydra — Headless OAuth Server

Ory Hydra เป็น Open Source OAuth 2.0 และ OpenID Connect Server ที่เป็น Headless คือไม่มี Login UI ในตัว ให้คุณสร้าง Login/Consent UI เองได้อย่างอิสระ เหมาะกับทีมที่ต้องการ Customization สูงสุดและ Self-hosted ด้วย Cloud Native Architecture ที่ Scale ได้ง่าย

OAuth สำหรับ Microservices

ในสถาปัตยกรรม Microservices การจัดการ Authentication และ Authorization มีความซับซ้อนมากขึ้น เนื่องจากมี Service หลายตัวที่ต้องสื่อสารกัน

API Gateway Pattern: Gateway ทำหน้าที่ Validate Token แล้วส่ง Request พร้อม User Info ไปยัง Internal Services ทำให้ Service ภายในไม่ต้อง Validate Token เอง แต่ต้องเชื่อถือ Gateway

Service-to-Service Auth: ใช้ Client Credentials Grant สำหรับ Service ที่ต้องเรียก Service อื่นโดยตรง แต่ละ Service มี Client ID/Secret ของตัวเอง และได้ Token ที่มี Scope จำกัดเฉพาะสิ่งที่ต้องการ

Token Relay: เมื่อ Service A ต้องเรียก Service B ในนามของ User สามารถส่ง Token ของ User ต่อไปได้ แต่ควรใช้ Token Exchange เพื่อ Downscope Token ก่อนส่งต่อ เพื่อหลักการ Least Privilege

DPoP — Demonstration of Proof-of-Possession

DPoP (RFC 9449) เป็นมาตรฐานใหม่ที่ผูก Access Token กับ Client ที่ขอ Token ทำให้แม้ Token จะถูกขโมยไป ก็ไม่สามารถใช้ได้ เพราะต้องพิสูจน์ว่ามี Private Key ที่ใช้ขอ Token

# DPoP Flow
import jwt
import time
import uuid
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization

# 1. Client สร้าง Key Pair (เก็บ Private Key ไว้ใน Client)
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()

# 2. สร้าง DPoP Proof JWT
dpop_proof = jwt.encode(
    {
        "jti": str(uuid.uuid4()),           # Unique ID
        "htm": "POST",                       # HTTP Method
        "htu": "https://auth.example.com/oauth/token",  # URL
        "iat": int(time.time()),
    },
    private_key,
    algorithm="ES256",
    headers={
        "typ": "dpop+jwt",
        "jwk": public_key_as_jwk,           # Public Key ใน Header
    },
)

# 3. ส่ง DPoP Proof พร้อม Token Request
response = requests.post(
    "https://auth.example.com/oauth/token",
    data={
        "grant_type": "authorization_code",
        "code": "auth-code",
        "code_verifier": code_verifier,
    },
    headers={
        "DPoP": dpop_proof,                 # DPoP Proof Header
    },
)

# 4. ได้ DPoP-bound Token
# token_type จะเป็น "DPoP" แทน "Bearer"
# {
#   "access_token": "eyJhbGci...",
#   "token_type": "DPoP",        ← ไม่ใช่ Bearer
#   "expires_in": 3600
# }

# 5. ใช้ Token ต้องสร้าง DPoP Proof ใหม่ทุก Request
dpop_proof_api = jwt.encode(
    {
        "jti": str(uuid.uuid4()),
        "htm": "GET",
        "htu": "https://api.example.com/data",
        "iat": int(time.time()),
        "ath": access_token_hash,            # Hash ของ Access Token
    },
    private_key,
    algorithm="ES256",
    headers={"typ": "dpop+jwt", "jwk": public_key_as_jwk},
)

# Resource Server จะ Verify ทั้ง Token + DPoP Proof
api_response = requests.get(
    "https://api.example.com/data",
    headers={
        "Authorization": f"DPoP {access_token}",
        "DPoP": dpop_proof_api,
    },
)

Rich Authorization Requests (RAR)

RAR (RFC 9396) เป็น Extension ของ OAuth 2.0 ที่ให้ Client สามารถขอสิทธิ์ที่ละเอียดกว่า Scopes ธรรมดา เหมาะกับ Use Case ที่ซับซ้อน เช่น Banking API ที่ต้องระบุว่าต้องการโอนเงินจากบัญชีไหน เท่าไหร่ ไปที่ไหน

# RAR — Rich Authorization Requests
# แทนที่จะขอ scope=transfer:money (กว้างเกินไป)
# สามารถระบุรายละเอียดได้

authorization_details = [
    {
        "type": "payment_initiation",
        "actions": ["initiate", "status"],
        "locations": ["https://bank.example.com/payments"],
        "instructedAmount": {
            "currency": "THB",
            "amount": "5000.00"
        },
        "creditorAccount": {
            "iban": "TH1234567890"
        }
    }
]

# ส่งใน Authorization Request
auth_url = (
    "https://auth.bank.com/oauth/authorize?"
    "response_type=code"
    "&client_id=my-banking-app"
    f"&authorization_details={json.dumps(authorization_details)}"
    "&redirect_uri=https://myapp.com/callback"
)

OAuth Security Best Practices

ความปลอดภัยของระบบ OAuth มีความสำคัญมากเนื่องจากเป็นประตูเข้าสู่ทรัพยากรทั้งหมด ต่อไปนี้คือ Best Practices ที่ต้องปฏิบัติตาม

ใช้ PKCE เสมอ: ทุก Authorization Code Flow ต้องใช้ PKCE ไม่ว่าจะเป็น Public หรือ Confidential Client

ตรวจสอบ Redirect URI อย่างเข้มงวด: ใช้ Exact String Matching ไม่อนุญาต Wildcard หรือ Pattern Matching เพราะอาจนำไปสู่ Open Redirect Attack ที่ส่ง Authorization Code ไปยัง Attacker

Access Token อายุสั้น: ตั้ง Access Token ให้หมดอายุภายใน 5-15 นาที ใช้ Refresh Token สำหรับขอ Token ใหม่ และใช้ Refresh Token Rotation

ตรวจสอบ Token อย่างครบถ้วน: Verify Signature ด้วย Algorithm ที่กำหนดเท่านั้น (อย่า Accept "none") ตรวจ Issuer (iss), Audience (aud), Expiration (exp) ทุกครั้ง

เก็บ Token อย่างปลอดภัย: ฝั่ง Browser ใช้ HttpOnly, Secure, SameSite Cookie ห้ามเก็บใน localStorage เพราะ XSS สามารถเข้าถึงได้ ฝั่ง Mobile ใช้ Secure Storage ของ OS (Keychain/Keystore)

ใช้ State Parameter: ป้องกัน CSRF Attack ด้วยการส่ง Random State ไปกับ Authorization Request และตรวจสอบว่า State ที่กลับมาตรงกัน

จำกัด Scope: ขอเฉพาะ Scope ที่จำเป็นเท่านั้น (Principle of Least Privilege) ไม่ขอ Scope กว้างๆ แล้วค่อยกรองทีหลัง

Common Vulnerabilities และการป้องกัน

ช่องโหว่สาเหตุการป้องกัน
Authorization Code InterceptionCode ถูกดักระหว่างทางใช้ PKCE
Open RedirectRedirect URI ไม่เข้มงวดExact URI matching
Token Theft (XSS)Token เก็บใน localStorageใช้ HttpOnly Cookie
CSRFไม่ใช้ State parameterใช้ State + PKCE
Token ReplayToken ถูกใช้ซ้ำใช้ DPoP
Insufficient Scope ValidationAPI ไม่ตรวจ Scopeตรวจ Scope ทุก Endpoint
JWT Algorithm ConfusionAccept alg: noneWhitelist Algorithm
Refresh Token TheftToken ไม่ถูก Rotateใช้ Refresh Token Rotation

สรุป

OAuth 2.0 และ OpenID Connect เป็นมาตรฐานที่ซับซ้อนแต่จำเป็นสำหรับ Application ยุคใหม่ทุกตัว ในปี 2026 Developer ควรยึดหลักดังนี้ ใช้ Authorization Code + PKCE เป็น Default สำหรับทุก Client ใช้ Client Credentials สำหรับ Service-to-Service ใช้ JWT เป็น Access Token Format พร้อม JWKS สำหรับ Key Distribution ตั้ง Token Lifetime ให้สั้น พร้อม Refresh Token Rotation พิจารณา DPoP สำหรับ Application ที่ต้องการ Security สูง เลือก Authorization Server ที่เหมาะสม ไม่ว่าจะเป็น Keycloak (Self-hosted), Auth0 (Managed), หรือ Ory Hydra (Headless) และปฏิบัติตาม Security Best Practices อย่างเคร่งครัด

สิ่งสำคัญที่สุดคืออย่าสร้าง Authorization Server เองตั้งแต่ศูนย์ เว้นแต่คุณจะเป็นผู้เชี่ยวชาญด้าน Security โดยเฉพาะ ให้ใช้ Solution ที่ผ่านการทดสอบแล้วและเป็นที่ยอมรับในอุตสาหกรรม จะปลอดภัยกว่ามาก


Back to Blog | iCafe Forex | SiamLanCard | Siam2R