การเชื่อมต่อแบบ Push(การจับมือยืนยันตัวตน / Push Handoff)
สถานการณ์: ผู้ใช้คลิกแอปส่วนขยายของนักพัฒนาในหน้า พื้นที่ทำงาน → ส่วนขยาย แพลตฟอร์มจะผลักตัวตนของผู้ใช้ในรูปแบบ ?wsa=<JWT> ไปยังหน้า Landing ของแอปส่วนขยายเป้าหมาย นักพัฒนาบริโภคที่หน้า Landing และตรวจสอบที่แบ็กเอนด์ แล้วแลกเป็นเซสชันของตัวเอง
ในโหมดนี้ นักพัฒนาไม่จำเป็นต้องเรียกใช้ endpoint ลงลายเซ็นของแพลตฟอร์ม — นั่นเป็นสิ่งที่ฟรอนต์เอนด์ของพื้นที่ทำงานเรียกใช้เองเมื่อผู้ใช้คลิก นักพัฒนารับผิดชอบเพียง「รับมาและตรวจสอบ」
ลำดับเหตุการณ์แบบครบวงจร
sequenceDiagram
autonumber
actor User as ผู้ใช้เข้าถึงแอปส่วนขยาย
participant GB as แพลตฟอร์ม GPTBots
participant FE as แอปส่วนขยาย-หน้า Landing
participant BE as แอปส่วนขยาย-แบ็กเอนด์
User->>GB: คลิกไอคอนแอป(POST sign-token)
GB->>GB: ตรวจสอบว่าผู้คลิกเป็นสมาชิกของ workspace นี้
GB->>GB: ใช้คีย์ลับลงลายเซ็น wsa<br/>(JWT อายุ 5 นาที, aud=host ของแอปส่วนขยาย)
GB->>FE: เปิด app_home_url?wsa=<JWT>
Note over FE: consumeHandoff()<br/>อ่าน ?wsa=, POST ไปยังแบ็กเอนด์ของแอปส่วนขยาย
FE->>BE: POST /session/exchange { wsa }
BE->>BE: verifyWsa() → identity
BE->>BE: สร้าง session ของตัวเอง
BE-->>FE: คืนค่า session(Set-Cookie)
Note over FE: history.replaceState ลบ ?wsa=
กฎการสร้าง URL เปลี่ยนเส้นทางของแพลตฟอร์ม(นักพัฒนาไม่ต้องนำไปสร้างเอง):
- จับคู่แอปส่วนขยายเป้าหมายจากข้อมูลลงทะเบียนโดยจับคู่
app_home_urlแบบแม่นยำ(URL ที่ไม่ได้ลงทะเบียนจะถูกปฏิเสธการลงลายเซ็นทั้งหมด เพื่อป้องกันการรั่วไหลของตัวตน) - ตรวจสอบว่าผู้คลิกเป็นสมาชิกของพื้นที่ทำงานนั้น(
workspace_id)จริง - หาก
app_home_urlมี query อยู่แล้ว จะต่อwsaด้วย&; ค่าwsaถูก URL-encode แล้ว คุณอ่านมาแล้วไม่ต้อง decode เอง
ฟรอนต์เอนด์: บริโภค wsa ที่หน้า Landing
ใช้ SDK(แนะนำ)
import { consumeHandoff } from '@gptbots/workspace-extension-sdk';
try {
const identity = await consumeHandoff({
exchangeUrl: '/session/exchange', // อินเทอร์เฟซตรวจสอบของแบ็กเอนด์คุณเอง
// search: location.search, // ค่าเริ่มต้นอ่าน location.search
// paramName: 'wsa', // ชื่อพารามิเตอร์เริ่มต้น wsa
// strip: true, // ค่าเริ่มต้นลบ wsa ออกจาก URL เมื่อสำเร็จ
});
bootYourApp(identity);
} catch (e) {
// ไม่มี wsa(ผู้ใช้เข้าถึงโดยตรง)หรือแบ็กเอนด์ตรวจสอบไม่ผ่าน
showLoginOrError(e);
}
consumeHandoff ทำสี่สิ่ง: ① อ่าน ?wsa= ② POST { wsa } ไปยัง exchangeUrl ③ เมื่อสำเร็จ history.replaceState ลบ ?wsa= ④ คืนค่า identity ที่แบ็กเอนด์ส่งกลับ
จังหวะการลบ: โทเค็นจะถูกลบออกจาก URL หลังแลกสำเร็จเท่านั้น เพื่อให้ความล้มเหลวชั่วคราวครั้งเดียวสามารถรีเฟรชลองใหม่ได้ นี่คือโทเค็นใช้ครั้งเดียวอายุ 5 นาที; หากนักพัฒนาต้องการให้ลบทันทีแม้ล้มเหลว สามารถ
catchแล้วเรียกstripHandoffToken()เอง
ไม่สร้างเซสชัน อ่านตัวตนอย่างเดียว(receive-only)
import { readHandoffToken, stripHandoffToken } from '@gptbots/workspace-extension-sdk';
const token = readHandoffToken(); // ฟังก์ชันบริสุทธิ์ คืนค่าสตริง JWT ดั้งเดิมหรือ null
if (token) {
// ยังแนะนำให้ส่ง token ไปให้แบ็กเอนด์ verifyWsa ก่อนจึงเชื่อเนื้อหา (อย่า parse JWT ที่ฟรอนต์เอนด์แล้วถือเป็นตัวตนที่เชื่อถือได้)
await fetch('/telemetry/visit', { method: 'POST', body: JSON.stringify({ wsa: token }) });
}
stripHandoffToken(); // อย่างไรก็ตามให้ลบ wsa ออกจาก URL
⚠️ อย่าถอด JWT ที่ฟรอนต์เอนด์แล้วถือเป็นตัวตนที่เชื่อถือได้ ลายเซ็นของ JWT ต้องใช้คีย์ลับเท่านั้นจึงจะตรวจสอบได้ และคีย์ลับอยู่ที่แบ็กเอนด์เท่านั้น การ parse ที่ฟรอนต์เอนด์ใช้ได้แค่เป็น placeholder แสดงผลแบบ「ไม่ปลอดภัย」เท่านั้น การตัดสินใจเรื่องสิทธิ์ใด ๆ ต้องยึดผลของ
verifyWsaที่แบ็กเอนด์เป็นหลัก
แบ็กเอนด์: ตรวจสอบ wsa
3.1 ใช้ SDK(แนะนำ)
import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';
app.post('/session/exchange', (req, res) => {
try {
const identity = verifyWsa(req.body.wsa, {
secret: process.env.EXTENSION_APP_SECRET, // คีย์ลับ per-app ของ Tier2 หรือคีย์ลับร่วมของ Tier1
audience: 'app.example.com', // host ของแอปส่วนขยาย ต้องเท่ากับ aud
// issuer: 'gptbots-workspace', // ค่าเริ่มต้น
// leewaySeconds: 30, // ระยะผ่อนผันการคลาดเคลื่อนของนาฬิกา ค่าเริ่มต้น 30s
// algorithms: ['HS256'], // ค่าเริ่มต้น
});
// แยกผู้เช่าหลายราย: จัดคำขอให้อยู่ภายใต้ identity.workspaceId
const sid = createSession(identity);
res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' });
res.json(identity);
} catch (e) {
const code = e instanceof WsaVerificationError ? e.code : 'Error';
res.status(401).json({ code });
}
});
ไม่ใช้ SDK
wsa เป็น HS256 JWT มาตรฐาน ไลบรารี JWT ของภาษาใดก็ตรวจสอบได้ ตัวอย่าง Java / Node / Python ดูที่ 05-การตรวจสอบโทเค็นและความปลอดภัย §3 ไม่ว่าจะใช้หรือไม่ใช้ SDK ทั้งสี่รายการคือ ลายเซ็น / iss / aud / exp ต้องตรวจสอบทั้งหมด
ลงทะเบียนแอปส่วนขยาย(รับคีย์ลับ)
OWNER/ADMIN ของพื้นที่ทำงาน: พื้นที่ทำงาน → การจัดการพื้นที่ → แอปส่วนขยาย → เพิ่ม กรอก:
| ฟิลด์ | คำอธิบาย |
|---|---|
| ชื่อแอป | ชื่อที่แสดง แนะนำ ≤ 12 ตัวอักษรจีนเพื่อเลี่ยงการตัดข้อความ |
| ไอคอนแอป | ไอคอนสี่เหลี่ยมจัตุรัส แนะนำ ≥ 128×128 |
| URL หน้าเข้าแอป | app_home_url ของคุณ ใช้เป็นคีย์เฉพาะ ตอนลงลายเซ็นจะจับคู่แบบเข้มงวดตามสตริงเต็ม |
| โหมดยืนยันสิทธิ์ | เลือก workspace_account(ต้องมีการส่งข้อมูลตัวตน) |
หลังส่งข้อมูลแล้ว ระบบจะแสดง App Secret เป็นข้อความธรรมดาเพียงครั้งเดียว คัดลอกเก็บไว้ทันที(เมื่อปิดแล้วทำได้เพียงหมุนเวียนคีย์ใหม่)
Checklist หน้า Landing
- เมื่อได้รับ
?wsa=ให้POST ไปยังบริการแบ็กเอนด์ของแอปส่วนขยายก่อนเพื่อตรวจสอบ แล้วจึงเชื่อเนื้อหา - เมื่อตรวจสอบผ่านทันที
history.replaceStateลบwsa(consumeHandoffทำให้เป็นค่าเริ่มต้นแล้ว) - แลกเป็นเซสชันของตัวเอง คำขอ XHR/fetch/img ถัดไปไม่ส่งผ่าน
wsaอีก - จัดการกรณีแยก「ผู้ใช้เข้าถึงโดยตรง ไม่มี
wsa」(นำไปล็อกอินหรือแบบไม่ระบุตัวตน) - ตรวจสอบไม่ผ่านให้แสดงข้อความที่อ่านเข้าใจได้ตาม
WsaVerificationError.code
รายละเอียดความปลอดภัยและการตรวจสอบหลายภาษา: การตรวจสอบโทเค็นและความปลอดภัย อยากเปลี่ยนเป็น「ปุ่มล็อกอินในแอป」: การเข้าสู่ระบบแบบ Pull
