05 · การตรวจสอบโทเค็นและความปลอดภัย

05 · การตรวจสอบโทเค็นและความปลอดภัย

ไม่ว่าจะแบบ Push หรือ Pull สุดท้ายนักพัฒนาจะได้ wsa(HS256 JWT)หนึ่งใบ บทความนี้เป็นสิ่งที่ผู้เชื่อมต่อทุกฝ่ายต้องอ่าน — ตรวจสอบไม่รัดกุม = ตัวตนถูกปลอมแปลงได้

รายการตรวจสอบบังคับ

หลังได้ wsa แล้ว ในบริการแบ็กเอนด์ของแอปส่วนขยายให้ตรวจสอบตามลำดับ(verifyWsa มีครบในตัวแล้ว):

  1. ลายเซ็น: ใช้คีย์ลับที่ได้จากการแจกจ่าย/ลงทะเบียนของแพลตฟอร์มตรวจสอบด้วย HS256 ล้มเหลว → ปฏิเสธ
  2. exp: เวลาปัจจุบัน ≤ exp(รวม leeway เล็กน้อย)exp ต้องมีอยู่ — โทเค็นที่ไม่มี exp ควรปฏิเสธทันที(ไม่เช่นนั้นเท่ากับไม่มีวันหมดอายุ)
  3. iss: ต้องเท่ากับ gptbots-workspace
  4. aud: ต้องเท่ากับ host ของแอปส่วนขยายเป้าหมาย นี่คือแนวป้องกันสำคัญในการกัน「โทเค็นถูกขโมยไปใช้กับอีกแอปหนึ่ง」ห้ามละเว้น
  5. iat / nbf(หากมี): ต้องไม่อยู่ในอนาคต(เกิน leeway)เพื่อป้องกันโทเค็นที่「ตั้งเวลาออกไว้ในอนาคตอันไกล」ใช้งานได้ยาวนาน
  6. แยกผู้เช่าตาม workspace_id: หากแอปส่วนขยายทำการแยกหลายพื้นที่ทำงาน ให้จัดคำขออยู่ภายใต้ workspace_id นั้น ห้ามเข้าถึงข้ามผู้เช่า
  7. หลัง Landing ลบ wsa / code ออกจาก URL ทันที: history.replaceState, เลี่ยงกรณีผู้ใช้คัดลอก URL แล้วแชร์โทเค็นออกไปด้วย
  8. สร้างเซสชันของตัวเอง: แลกเป็น session/cookie ของแอปส่วนขยายเอง คำขอถัดไปไม่ต้องพึ่ง wsa อีก — มันหมดอายุใน 5 นาที

verifyWsa ของ SDK ทางการบังคับให้ exp ต้องมีอยู่, ตรวจสอบการเลื่อนไปอนาคตของ iat/nbf, และทำการเปรียบเทียบลายเซ็นแบบเวลาคงที่พร้อม whitelist อัลกอริทึม(ค่าเริ่มต้นเฉพาะ HS256 เพื่อกันการโจมตีสับสน alg)ใช้มันก็จะครอบคลุมข้อ 1–5 โดยอัตโนมัติ


แนะนำให้ใช้ SDK ตรวจสอบ

import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify'; try { const id = verifyWsa(wsa, { secret: process.env.EXTENSION_APP_SECRET, audience: 'app.example.com', }); // id: { accountId, role, workspaceId, username?, email?, avatar?, appName?, issuedAt?, expiresAt? } } catch (e) { if (e instanceof WsaVerificationError) { // e.code: InvalidToken | InvalidSignature | Expired | NotYetValid // | WrongIssuer | WrongAudience | MissingClaim | UnsupportedAlgorithm } }
                      
                      import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';

try {
  const id = verifyWsa(wsa, {
    secret: process.env.EXTENSION_APP_SECRET,
    audience: 'app.example.com',
  });
  // id: { accountId, role, workspaceId, username?, email?, avatar?, appName?, issuedAt?, expiresAt? }
} catch (e) {
  if (e instanceof WsaVerificationError) {
    // e.code: InvalidToken | InvalidSignature | Expired | NotYetValid
    //       | WrongIssuer | WrongAudience | MissingClaim | UnsupportedAlgorithm
  }
}

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

ตัวเลือกครบถ้วนและรหัสข้อผิดพลาดดูที่ SDK-API-อ้างอิง

การตรวจสอบเมื่อไม่ใช้ SDK(หลายภาษา)

wsa เป็น HS256 JWT มาตรฐาน ไลบรารี JWT ใดก็ตรวจสอบได้ โปรดเปิดการตรวจสอบ iss / aud อย่างชัดเจน และล็อก algorithms=['HS256']

Java(auth0 java-jwt)

JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SHARED_SECRET)) .withIssuer("gptbots-workspace") .withAudience("app.example.com") .acceptLeeway(30) // ผ่อนผันการคลาดเคลื่อนของนาฬิกา 30s .build(); DecodedJWT jwt = verifier.verify(wsaParam); String userId = jwt.getSubject(); String workspaceId = jwt.getClaim("workspace_id").asString(); String role = jwt.getClaim("role").asString(); String username = jwt.getClaim("username").asString(); // อาจเป็น null String email = jwt.getClaim("email").asString(); // อาจเป็น null
                      
                      JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SHARED_SECRET))
    .withIssuer("gptbots-workspace")
    .withAudience("app.example.com")
    .acceptLeeway(30) // ผ่อนผันการคลาดเคลื่อนของนาฬิกา 30s
    .build();

DecodedJWT jwt = verifier.verify(wsaParam);
String userId      = jwt.getSubject();
String workspaceId = jwt.getClaim("workspace_id").asString();
String role        = jwt.getClaim("role").asString();
String username    = jwt.getClaim("username").asString();  // อาจเป็น null
String email       = jwt.getClaim("email").asString();     // อาจเป็น null

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

Node.js(jsonwebtoken)

const jwt = require('jsonwebtoken'); const payload = jwt.verify(wsaParam, SHARED_SECRET, { algorithms: ['HS256'], issuer: 'gptbots-workspace', audience: 'app.example.com', clockTolerance: 30, }); const { sub: userId, workspace_id, role, username, email, avatar } = payload;
                      
                      const jwt = require('jsonwebtoken');

const payload = jwt.verify(wsaParam, SHARED_SECRET, {
  algorithms: ['HS256'],
  issuer: 'gptbots-workspace',
  audience: 'app.example.com',
  clockTolerance: 30,
});
const { sub: userId, workspace_id, role, username, email, avatar } = payload;

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

Python(PyJWT)

import jwt payload = jwt.decode( wsa_param, SHARED_SECRET, algorithms=["HS256"], issuer="gptbots-workspace", audience="app.example.com", leeway=30, ) user_id = payload["sub"] workspace_id = payload["workspace_id"] role = payload["role"]
                      
                      import jwt

payload = jwt.decode(
    wsa_param,
    SHARED_SECRET,
    algorithms=["HS256"],
    issuer="gptbots-workspace",
    audience="app.example.com",
    leeway=30,
)
user_id      = payload["sub"]
workspace_id = payload["workspace_id"]
role         = payload["role"]

                    
บล็อกโค้ดนี้ในหน้าต่างลอย

เตือนเรื่องการเข้ารหัสคีย์ลับ: HS256 ใช้สตริงคีย์ลับเป็นไบต์ UTF-8 โดยตรงเป็น HMAC key คีย์ลับ per-app ของ Tier 2 มีรูปแบบ wext_+64 hex โปรดส่งเข้าไปเป็นสตริงตามเดิมเป็นคีย์ลับ(อย่า hex decode อีก)ให้สอดคล้องกับฝั่งลงลายเซ็นของแพลตฟอร์ม

ข้อกำหนดด้านความปลอดภัย(ปฏิบัติทีละข้อ)

  1. คีย์ลับคือโมเดลความปลอดภัยทั้งหมด ปัจจุบัน HS256 เป็นคีย์ลับแบบสมมาตร: เมื่อรั่วไหล ใครก็สามารถปลอมแปลงตัวตนของผู้ใช้พื้นที่ทำงานใดก็ได้เพื่อโจมตีแอปส่วนขยายที่เชื่อมต่อ เก็บไว้ที่แบ็กเอนด์เท่านั้น(ตัวแปรสภาพแวดล้อม/KMS)ห้ามเขียนลงในโค้ดฟรอนต์เอนด์, repo git, log, ค่าคอนฟิกฝั่งไคลเอนต์เด็ดขาด
  2. พื้นที่เปิดเผยของ JWT ใน URL(เฉพาะแบบ Push): query string จะถูกบันทึกในประวัติเบราว์เซอร์, access log ของเว็บเซิร์ฟเวอร์, log แคช CDN, บันทึก Referer ถึงแม้จะถูกบันทึกไว้ หากถูกดักได้ภายใน 5 นาทีก็ยังใช้ประโยชน์ได้ หลัง Landing ให้ history.replaceState ลบทันทีconsumeHandoff ทำให้เป็นค่าเริ่มต้นแล้ว)แบบ Pull(M-Auth)โดยธรรมชาติไม่ใส่ wsa ลงใน URL จึงปลอดภัยกว่า
  3. อย่าส่งผ่าน wsa ไปยังทรัพยากรย่อย หลังแลกเป็นเซสชันของตัวเองแล้ว คำขอ XHR/fetch/img ถัดไปทั้งหมดต้องไม่พก wsa ดั้งเดิมอีก ไม่เช่นนั้นมันจะปรากฏใน Referer ของทุกทรัพยากรย่อย
  4. ซิงค์นาฬิกา HS256 ตัดสิน exp อย่างเข้มงวด นาฬิกาเซิร์ฟเวอร์ต้องซิงค์ NTP; ในตัวอย่าง leeway 30s ผ่อนผันการเลื่อนเล็กน้อย อย่าขยายไปถึงระดับนาที
  5. role ใช้เป็นการแมปสิทธิ์เริ่มต้นเท่านั้น มันสะท้อนเพียงบทบาทของผู้ใช้ในพื้นที่ทำงานนั้น ไม่ได้แทนสิทธิ์ภายในแอปของคุณ; แอปของคุณดูแลโมเดลสิทธิ์ของตัวเอง
  6. แยกผู้เช่าหลายราย ใช้ workspace_id เป็นคีย์แยกข้อมูลเสมอ เพื่อกันไม่ให้ผู้ใช้ของพื้นที่ทำงาน A อ่านข้อมูลของ B ได้
  7. การรับมือฉุกเฉินเรื่องหมุนเวียน/เพิกถอนคีย์ลับ: ในการจัดการพื้นที่ ให้「หมุนเวียนคีย์ลับ」ของแอป จะสร้างคีย์ลับใหม่และแสดงเพียงครั้งเดียว จากนั้นใช้คีย์ลับใหม่ตั้งค่าแบ็กเอนด์ของคุณอีกครั้ง

5. ขอบเขตความน่าเชื่อถือของฟรอนต์เอนด์(สำคัญ)

  • ฟรอนต์เอนด์ทำเพียง placeholder แสดงผล ไม่ตัดสินใจเรื่องสิทธิ์ ฟรอนต์เอนด์สามารถ base64 ถอด payload ของ JWT ได้ แต่ไม่มีคีย์ลับก็ตรวจลายเซ็นไม่ได้ — ใครก็สร้าง payload ที่「ดูเหมือนถูก」ได้
  • การตัดสินทุกอย่างว่า「คนนี้เป็นใคร ทำสิ่งนี้ได้ไหม」ต้องยึดผลของ verifyWsa ที่แบ็กเอนด์ของแอปส่วนขยายเป็นหลัก
  • ดังนั้นกลยุทธ์มาตรฐานคือ: ฟรอนต์เอนด์ส่ง wsa/code ให้แบ็กเอนด์ของแอปส่วนขยาย → แบ็กเอนด์ตรวจสอบ → แบ็กเอนด์สร้างเซสชัน → ฟรอนต์เอนด์เชื่อเฉพาะเซสชันนี้

ขั้นถัดไป: SDK-API-อ้างอิง เพื่อดู API ครบถ้วน; เมื่อพบปัญหาใด ๆ โปรดดูที่ คำถามที่พบบ่อยและการแก้ปัญหา