SPA Frontend
SPA รันอยู่ทั้งหมดใน browser ของผู้ใช้ โค้ดฝั่ง client (รวมถึงค่าคงที่หรือ config ใด ๆ ที่ฝังไว้) จึงเปิดให้ทุกคนตรวจสอบได้เสมอ ดังนั้น SPA จึงไม่สามารถเก็บ client secret ไว้อย่างปลอดภัยเหมือนที่ backend ทำได้ Keycloak แก้ปัญหานี้โดยให้ SPA ลงทะเบียนเป็น public client (ไม่มี secret) แล้วใช้ PKCE (Proof Key for Code Exchange) แทน เพื่อป้องกันไม่ให้ authorization code ที่ถูกดักจับระหว่างทางถูกนำไปแลก token ได้
การติดตั้ง keycloak-js
หัวข้อที่มีชื่อว่า “การติดตั้ง keycloak-js”keycloak-js คือ adapter อย่างเป็นทางการฝั่ง browser สำหรับ Keycloak จัดการ Authorization Code + PKCE flow, การจัดเก็บ token, silent refresh และ logout โดยที่คุณไม่ต้องจัดการ redirect URL หรือการแลก token เอง
ติดตั้งผ่าน npm:
npm install keycloak-jspackage มี TypeScript types มาในตัว ไม่จำเป็นต้องติดตั้ง @types แยกต่างหาก
การสร้าง Keycloak instance
หัวข้อที่มีชื่อว่า “การสร้าง Keycloak instance”ส่ง URL ของ realm, ชื่อ realm และ client ID เข้าไป ค่าสามตัวนี้คือขั้นต่ำที่จำเป็นสำหรับการระบุตัวตนแอปพลิเคชันให้กับ Keycloak:
import Keycloak from 'keycloak-js';
const keycloak = new Keycloak({
url: 'http://localhost:8080',
realm: 'my-app',
clientId: 'my-app-frontend',
});
keycloak
.init({ onLoad: 'login-required', pkceMethod: 'S256' })
.then((authenticated) => {
if (authenticated) {
console.log('Logged in as', keycloak.tokenParsed?.preferred_username);
startApp();
}
});
function startApp() {
// Your app bootstrap goes here.
// keycloak.token is available from this point on.
}ประเด็นสำคัญ:
onLoad: 'login-required'จะ redirect ผู้ใช้ไปยัง Keycloak ทันทีหากยังไม่ได้ authenticate ใช้'check-sso'หากต้องการให้หน้าเว็บเข้าถึงได้โดยไม่ต้อง login และแสดงปุ่ม login แทนpkceMethod: 'S256'เปิดใช้งาน PKCE (Proof Key for Code Exchange) ซึ่งจำเป็นสำหรับ public client (SPA) เพราะไม่สามารถเก็บ client secret ได้อย่างปลอดภัย PKCE แทนที่ secret ด้วย code challenge ที่สร้างขึ้นต่อ request
การเรียก API ที่ได้รับการป้องกัน
หัวข้อที่มีชื่อว่า “การเรียก API ที่ได้รับการป้องกัน”หลังจาก init resolve กลับมาด้วย authenticated: true แล้ว keycloak.token จะเก็บ JWT string ดิบไว้ แนบ token นี้เป็น Bearer token ใน API request ทุกครั้ง:
async function fetchProfile() {
await keycloak.updateToken(30); // refresh if expiring in < 30 seconds
const response = await fetch('https://api.example.com/profile', {
headers: {
Authorization: `Bearer ${keycloak.token}`,
},
});
return response.json();
}updateToken(minValidity) ตรวจสอบว่า token ปัจจุบันจะหมดอายุภายใน minValidity วินาทีหรือไม่ ถ้าใช่จะใช้ refresh token เพื่อรับ token ใหม่แบบ silent เรียกใช้ก่อนแนบ keycloak.token เสมอ เพื่อหลีกเลี่ยงการส่ง JWT ที่หมดอายุ
การอ่าน claims จาก token
หัวข้อที่มีชื่อว่า “การอ่าน claims จาก token”payload ที่ถอดรหัสแล้วอยู่ใน keycloak.tokenParsed claims ที่ใช้บ่อย:
keycloak.tokenParsed?.preferred_username // usernamekeycloak.tokenParsed?.email // email addresskeycloak.tokenParsed?.realm_access?.roles // array of realm roleskeycloak.tokenParsed?.resource_access?.['my-app-frontend']?.roles // client rolesข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| เก็บ token ไว้ในหน่วยความจำ (in-memory) | ปลอดภัยกว่า เพราะ JavaScript ฝั่งอื่นหรือ script ที่ถูกแทรกผ่าน XSS เข้าถึงได้ยากกว่า และ token จะหายไปเองเมื่อปิด tab | token หายทุกครั้งที่ refresh หน้า ต้อง trigger silent SSO check ใหม่ผ่าน iframe หรือ redirect |
| เก็บ token ไว้ใน localStorage | ผู้ใช้ยังคง login อยู่แม้ refresh หน้าเว็บ ใช้งานง่ายเพราะไม่ต้องจัดการ silent refresh เอง | เปิดช่องให้ XSS attack ขโมย token ได้ทันที เพราะ script ใดก็ตามที่รันบนหน้าเว็บอ่าน localStorage ได้ |
| ใช้ keycloak-js (official adapter) | จัดการ PKCE flow, silent refresh และการจัดเก็บ token ให้อัตโนมัติ ลดโอกาสเขียนโค้ด security ผิดเอง | ผูกกับ Keycloak โดยตรง หาก migrate ไป identity provider อื่นต้องเขียน integration ใหม่ |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- เก็บ token ไว้ใน localStorage — เป็นความผิดพลาดที่พบบ่อยที่สุดของ SPA เพราะ localStorage เข้าถึงได้จาก JavaScript ทุกบรรทัดที่รันบนหน้า หาก third-party script หรือ dependency ที่ถูกแฮ็กมี XSS vulnerability ผู้โจมตีจะดึง token ออกไปได้ทันที ให้ปล่อยให้ keycloak-js เก็บ token ไว้ในหน่วยความจำตามค่าเริ่มต้นแทน
- เชื่อ client-side “logged in” state โดยไม่ตรวจสอบที่ backend — การเช็คว่า keycloak.authenticated เป็น true บน frontend บอกได้แค่ว่า UI ควรแสดงอะไร ไม่ใช่หลักฐานว่า request นั้นถูกต้อง backend ต้อง verify JWT signature ทุกครั้งที่ endpoint
- ไม่เรียก updateToken ก่อนยิง API ทุกครั้ง — การเก็บ token ไว้ในตัวแปรแล้วใช้ซ้ำโดยไม่เช็คว่าใกล้หมดอายุหรือยัง ทำให้ request ล้มเหลวกลางทางด้วย 401 ทั้งที่สามารถ refresh แบบ silent ล่วงหน้าได้
💡 ตัวอย่างจากของจริง
keycloak-js official pattern — Keycloak เองแนะนำให้เก็บ token ไว้ในหน่วยความจำ (JavaScript variable) เท่านั้น ไม่ใช่ localStorage หรือ sessionStorage เมื่อ refresh หน้า adapter จะทำ silent SSO check ผ่าน hidden iframe เพื่อขอ token ใหม่โดยผู้ใช้ไม่รู้ตัว
Auth0 SPA SDK — ใช้แนวทางเดียวกันคือ in-memory storage + refresh token rotation แทนการเก็บลง localStorage ซึ่งกลายเป็น best practice มาตรฐานของ OIDC client library สำหรับ browser ทั่วทั้งอุตสาหกรรม