ประเภทของ Token
ประเภทของ Token ใน Keycloak
หัวข้อที่มีชื่อว่า “ประเภทของ Token ใน Keycloak”Keycloak ออก token สามประเภทในขั้นตอน OIDC flow แต่ละประเภทมีวัตถุประสงค์ต่างกัน
Access Token
หัวข้อที่มีชื่อว่า “Access Token”ใช้อนุญาต API call — เป็น JWT อายุสั้นที่ API ของคุณตรวจสอบในทุก request Bearer token นี้คือสิ่งที่คุณส่งไปใน Authorization header เมื่อเรียก backend service:
Authorization: Bearer <access_token>คุณสมบัติสำคัญ:
- อายุสั้นโดยค่าเริ่มต้น (5 นาทีใน Keycloak)
- บรรจุ claim เกี่ยวกับผู้ใช้และสิทธิ์ที่ได้รับ (role, scope)
- มีไว้สำหรับ API ของคุณ — ไม่ใช่สำหรับตัว client application เอง
- ควร validate ทุก request ด้วย public key ของ realm (ผ่าน JWKS)
ID Token
หัวข้อที่มีชื่อว่า “ID Token”บอก client ว่าผู้ใช้คือใคร — ใช้ได้เฉพาะใน client เท่านั้น เช่น เพื่อแสดงชื่อผู้ใช้ใน UI ห้ามส่ง ID token ไปยัง backend API เด็ดขาด
คุณสมบัติสำคัญ:
- บรรจุ identity claim:
sub,name,email,preferred_usernameและอื่นๆ - มีไว้สำหรับ client application เท่านั้น — ห้ามส่งไปยัง API ของคุณ
- ไม่ควรใช้เพื่อตัดสินใจเรื่อง authorization
Refresh Token
หัวข้อที่มีชื่อว่า “Refresh Token”อายุยาวกว่า access token — ใช้ขอ access token ใหม่โดยไม่ต้อง login ซ้ำ ส่งได้เฉพาะไปยัง Keycloak token endpoint เท่านั้น
คุณสมบัติสำคัญ:
- ส่งได้เฉพาะไปยัง Keycloak token endpoint (
/protocol/openid-connect/token) - ไม่ส่งไปยัง API ของคุณเด็ดขาด
- ถูกยกเลิกเมื่อผู้ใช้ logout หรือเมื่อ SSO session หมดอายุ
- ถ้าขอ scope
offline_accessKeycloak จะออก offline refresh token ที่มีอายุอยู่ได้นานกว่า session ปกติ
ตัวอย่าง Decoded JWT
หัวข้อที่มีชื่อว่า “ตัวอย่าง Decoded JWT”ด้านล่างนี้คือ access token ที่ถูก decode แล้ว แสดง header, payload และ claim ต่างๆ ที่ Keycloak ใส่ไว้:
{ "header": { "alg": "RS256", "typ": "JWT", "kid": "abc123" }, "payload": { "exp": 1719878400, "iat": 1719874800, "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "iss": "https://auth.example.com/realms/my-app", "aud": "account", "sub": "user-uuid-here", "typ": "Bearer", "azp": "my-app-frontend", "scope": "openid profile email roles", "realm_access": { "roles": ["offline_access", "uma_authorization", "user"] }, "resource_access": { "my-app-frontend": { "roles": ["app-user"] } }, "email_verified": true, "name": "Alice Example", "preferred_username": "alice", }}{
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "abc123"
},
"payload": {
"exp": 1719878400,
"iat": 1719874800,
"jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"iss": "https://auth.example.com/realms/my-app",
"aud": "account",
"sub": "user-uuid-here",
"typ": "Bearer",
"azp": "my-app-frontend",
"scope": "openid profile email roles",
"realm_access": {
"roles": ["offline_access", "uma_authorization", "user"]
},
"resource_access": {
"my-app-frontend": {
"roles": ["app-user"]
}
},
"email_verified": true,
"name": "Alice Example",
"preferred_username": "alice",
"email": "[email protected]"
}
}Claim สำคัญใน Access Token
หัวข้อที่มีชื่อว่า “Claim สำคัญใน Access Token”| Claim | ความหมาย |
|---|---|
sub | Unique ID ของผู้ใช้ |
iss | URL ของ realm ที่ออก token |
aud | ผู้รับ token ที่ตั้งใจ |
exp | เวลาที่ token หมดอายุ (Unix timestamp) |
azp | Client ที่ขอ token |
scope | Scope ที่ถูก grant |
realm_access.roles | Realm roles ของผู้ใช้ |
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
ขอ ID token พร้อม access token (scope=openid) | Client รู้ตัวตนผู้ใช้ทันทีโดยไม่ต้องเรียก userinfo endpoint เพิ่ม | Response ใหญ่ขึ้น และมี token อีกใบที่ต้อง handle ให้ถูก — ห้ามส่งไป backend เด็ดขาด |
| ใช้ refresh token เพื่อขอ access token ใหม่ | ผู้ใช้ไม่ต้อง login ซ้ำ แม้ access token จะหมดอายุเร็ว | ต้องเก็บ refresh token อย่างปลอดภัย เพราะมีอายุยาวกว่าและมีสิทธิ์สูงกว่า access token |
| Public client (SPA) ใช้ PKCE แทน client secret | ไม่ต้อง embed secret ไว้ในโค้ด frontend ที่ใครก็ inspect ได้ | ต้องพึ่ง redirect URI ที่ตั้งไว้อย่างเข้มงวด เพื่อป้องกัน authorization code ถูกขโมยระหว่าง redirect |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ส่ง ID token ไปยัง backend API — ID token มีไว้ให้ client อ่านเพื่อแสดงข้อมูลผู้ใช้ใน UI เท่านั้น ไม่ได้ออกแบบมาให้ API ตรวจสอบ ให้ส่ง access token เป็น Bearer token เสมอ
- เก็บ token (โดยเฉพาะ refresh token) ไว้ใน localStorage — JavaScript ใดๆ บนหน้าเว็บอ่าน localStorage ได้ ถ้าแอปมีช่องโหว่ XSS แม้เล็กน้อย token ก็หลุดไปกับผู้โจมตีทันที ควรเก็บใน memory หรือ httpOnly cookie แทน
- ใช้ access token เป็นตัวระบุ session ระยะยาว — access token หมดอายุและเปลี่ยนค่าไปเรื่อยๆ ตาม lifespan อย่าผูก logic ที่ต้องคงอยู่นานกับค่า token ตรงๆ ให้ผูกกับ
subclaim หรือ session ฝั่ง server แทน
💡 ตัวอย่างจากของจริง
Auth0 SPA SDK / Google Identity Services — เก็บ access token และ refresh token ไว้ใน memory ของ JavaScript runtime เท่านั้น (ไม่แตะ localStorage) แล้วให้ session กลับมาผ่าน silent refresh หรือ refresh token rotation เมื่อ reload หน้า
แอปธนาคารบนมือถือ — แยกการใช้งาน ID token ชัดเจนว่าใช้แสดงชื่อ/รูปโปรไฟล์ในแอปเท่านั้น ส่วนทุก call ไปยัง core banking API จะแนบเฉพาะ access token ผ่าน
Authorization: Bearerheader