ข้ามไปยังเนื้อหา

Backend API

เมื่อ client ส่ง request พร้อม header Authorization: Bearer <token> backend ของคุณต้อง:

  1. ดึง JWKS ของ realm — ชุด public key ที่ Keycloak ใช้เซ็น JWT URL คือ <keycloak-url>/realms/<realm>/protocol/openid-connect/certs
  2. ตรวจสอบ signature — ยืนยันว่า JWT ถูกเซ็นโดย Keycloak ไม่ใช่ปลอมโดยผู้โจมตี
  3. ตรวจสอบ issuer (iss) — claim iss ใน token ต้องเท่ากับ <keycloak-url>/realms/<realm> token จาก realm อื่นหรือ server อื่นต้องถูกปฏิเสธ
  4. ตรวจสอบ audience (aud) — claim aud ควรมี client ID ของคุณ เพื่อยืนยันว่า token ถูก issue สำหรับ API ของคุณ
  5. ตรวจสอบ expiry (exp) — ปฏิเสธ token ที่หมดอายุแล้ว

หากตรวจสอบล้มเหลวในข้อใดข้อหนึ่ง token นั้นไม่ถูกต้องและต้องปฏิเสธ request ด้วย 401 Unauthorized

ไลบรารี jose เป็น JWT library ที่ทันสมัย ไม่มี dependency เสริม รองรับการดึง JWKS และการตรวจสอบมาตรฐานทั้งหมด ติดตั้ง:

Terminal window
npm install jose
import { createRemoteJWKSet, jwtVerify } from 'jose';
import express from 'express';

const KEYCLOAK_URL = 'http://localhost:8080';
const REALM = 'my-app';
const CLIENT_ID = 'my-app-backend';

// Cache the JWKS remote keyset — jose handles key rotation automatically.
const JWKS = createRemoteJWKSet(
  new URL(`${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/certs`)
);

async function requireAuth(req, res, next) {
  const authHeader = req.headers['authorization'] ?? '';
  const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;

  if (!token) {
    return res.status(401).json({ error: 'Missing bearer token' });
  }

  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: `${KEYCLOAK_URL}/realms/${REALM}`,
      audience: CLIENT_ID,
    });
    req.user = payload; // attach claims to the request
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

const app = express();

app.get('/profile', requireAuth, (req, res) => {
  res.json({
    username: req.user.preferred_username,
    email: req.user.email,
    roles: req.user.realm_access?.roles ?? [],
  });
});

app.listen(3001, () => console.log('API listening on :3001'));
  • createRemoteJWKSet สร้าง key set ที่ cache ไว้จาก JWKS URL ของ Keycloak เมื่อ Keycloak หมุนเวียน key jose จะดึง key ใหม่โดยอัตโนมัติ
  • jwtVerify ตรวจสอบ signature, iss, aud และปฏิเสธ token ที่หมดอายุ หากตรวจสอบล้มเหลวจะ throw error — block catch จะส่ง 401 กลับ
  • หลังการตรวจสอบสำเร็จ payload จะมี claims ทั้งหมดที่ถอดรหัสแล้ว (sub, preferred_username, realm_access เป็นต้น) และสามารถนำไปใช้ใน authorisation logic ได้

Keycloak เข้ารหัส realm roles ใน realm_access.roles และ client-specific roles ใน resource_access.<clientId>.roles helper ตรวจสอบ role:

function hasRole(user: Record<string, unknown>, role: string): boolean {
const roles = (user.realm_access as { roles?: string[] })?.roles ?? [];
return roles.includes(role);
}
ตัวเลือกBenefitCost
ตรวจสอบ JWT ทุก request (stateless validation)ไม่ต้อง query session store ทุกครั้ง scale ได้ง่ายเพราะ backend ไม่ต้องเก็บ stateต้อง verify signature ด้วย cryptography ทุก request ซึ่งมี CPU cost เล็กน้อยต่อ request
Cache JWKS ไว้ในหน่วยความจำ (เช่น createRemoteJWKSet ของ jose)ลดจำนวนครั้งที่ต้องเรียก JWKS endpoint ของ Keycloak ทำให้ latency ต่ำลงถ้า Keycloak หมุนเวียน key กะทันหันโดยไม่รอ cache หมดอายุ อาจมี window สั้น ๆ ที่ verify ล้มเหลวจนกว่า library จะดึง key ใหม่
ใช้ library ตรวจสอบ JWT มาตรฐาน (เช่น jose)เป็น library ที่ community ตรวจสอบแล้ว รองรับ JWKS, key rotation และมาตรฐาน OIDC ครบต้องเขียน middleware เชื่อมกับ framework เอง ไม่ได้ผูกกับ Keycloak โดยตรงเหมือน adapter เฉพาะทาง
  • backend ไม่ตรวจสอบ claim aud/iss ทุก request — ตรวจแค่ signature อย่างเดียวไม่พอ เพราะ token ที่เซ็นถูกต้องจาก realm อื่นหรือ client อื่นก็ผ่านการตรวจ signature ได้เช่นกัน ต้องเช็ค iss ให้ตรงกับ realm ของคุณ และ aud ให้ตรงกับ client ID ของ backend ทุกครั้ง
  • เชื่อ token ที่แนบมาโดยไม่ verify signature กับ JWKS — การ decode JWT แล้วอ่าน claims ตรง ๆ โดยไม่ verify signature เท่ากับเปิดให้ผู้โจมตีปลอม payload อะไรก็ได้ ต้องใช้ library อย่าง jose หรือ jjwt เพื่อ verify กับ public key จาก JWKS เสมอ
  • ไม่จัดการ key rotation — hardcode public key ไว้ในโค้ดแทนที่จะดึงจาก JWKS endpoint แบบ dynamic ทำให้เมื่อ Keycloak หมุนเวียน key แล้ว backend verify token ใหม่ไม่ผ่านจนกว่าจะ deploy โค้ดใหม่

💡 ตัวอย่างจากของจริง

Spring Security oauth2ResourceServer — เป็นตัวอย่างมาตรฐานของ backend-side JWT validation middleware กำหนดแค่ issuer-uri ก็ดึง JWKS, ตรวจ signature, iss, aud และ exp ให้อัตโนมัติโดยไม่ต้องเขียน validation logic เอง

jose + createRemoteJWKSet — ใช้ในตัวอย่าง Node/Express ของบทเรียนนี้ เป็น pattern เดียวกับที่ทีม backend จำนวนมากใช้จริงใน production เพราะ cache JWKS อัตโนมัติและรองรับ key rotation โดยไม่ต้อง restart service

claim iss (issuer) ใน JWT ของ Keycloak บรรจุข้อมูลอะไร?
จะเกิดอะไรขึ้นหาก claim aud (audience) ไม่ตรงกับ client ID ของ backend?
เหตุใด createRemoteJWKSet จาก jose จึง cache JWKS ไว้?
Keycloak เผยแพร่ public signing key (JWKS) ไว้ที่ใด?