04 · การเข้าสู่ระบบแบบ Pull(Login with GPTBots Workspace / M-Auth)
เมื่อแอปส่วนขยายของนักพัฒนาเป็นเว็บไซต์อิสระ และต้องการให้บริการปุ่ม 「Login with GPTBots Workspace」 หนึ่งปุ่ม ผู้ใช้คลิกแล้วไปที่การล็อกอิน GPTBots เลือกพื้นที่ทำงาน แล้วนำสถานะล็อกอินและข้อมูลตัวตนของผู้ใช้ที่ล็อกอินกลับมา
ใช้ OAuth2 authorization code + PKCE: เบราว์เซอร์ได้แค่ code แบบใช้ครั้งเดียว ส่วน wsa ตัวจริงแบ็กเอนด์ของคุณเป็นผู้แลกด้วย code + PKCE code_verifier, wsa ไม่มีวันเข้าไปใน URL / ประวัติ / Referer ของเบราว์เซอร์ — ปลอดภัยกว่าแบบ Push
การตรวจสอบหลังได้
wsaมาแล้ว เหมือนกันทุกประการกับแบบ Push(ดู 05)บทความนี้พูดถึงแค่「วิธีรับwsaมา」เท่านั้น
1. ลำดับเหตุการณ์แบบครบวงจร
sequenceDiagram
participant WS as หน้า「ส่วนขยาย」ของพื้นที่ทำงาน
participant GB as แพลตฟอร์ม GPTBots
participant FE as หน้า Landing ของแอปส่วนขยาย
participant BE as แบ็กเอนด์ของแอปส่วนขยาย
Note over WS: ผู้ใช้คลิกไอคอนแอป
WS->>GB: POST sign-token
Note over GB: ตรวจสอบว่าผู้คลิกเป็นสมาชิกของ workspace นี้<br/>ใช้คีย์ลับลงลายเซ็น wsa(JWT อายุ 5 นาที, aud=host ของคุณ)
GB-->>WS: คืนค่า wsa
WS->>FE: เปิด app_home_url?wsa=JWT
Note over FE: consumeHandoff(), อ่าน ?wsa=
FE->>BE: POST /session/exchange(พก wsa มา)
Note over BE: verifyWsa() → identity<br/>สร้าง session ของตัวเอง
BE-->>FE: identity
Note over FE: history.replaceState ลบ ?wsa=
Endpoint ของแพลตฟอร์ม
| วัตถุประสงค์ | เมธอด | เส้นทาง |
|---|---|---|
| ทางเข้าขออนุญาต(นำทางในเบราว์เซอร์) | GET | /api/console/account/extension-app/authorize |
| แลกโทเค็น(แบ็กเอนด์ → แบ็กเอนด์) | POST | /api/console/account/extension-app/token |
พารามิเตอร์ /authorize
| พารามิเตอร์ | จำเป็น | คำอธิบาย |
|---|---|---|
client_id |
ใช่ | URL หน้าเข้าแอปส่วนขยาย(app home URL) |
redirect_uri |
ใช่ | ที่อยู่ callback ซึ่ง host ต้องอยู่โดเมนเดียวกับ client_id(scheme + host เดียวกัน) |
state |
ใช่ | สตริงสุ่ม CSRF, ตอน callback จะพากลับมาตรวจสอบตามเดิม |
code_challenge |
ใช่ | base64url(sha256(code_verifier)), ไม่มี padding |
code_challenge_method |
ใช่ | รับเฉพาะ S256(แยกตัวพิมพ์ใหญ่-เล็ก) |
workspace_id |
ไม่ | เลือกพื้นที่ทำงานล่วงหน้า ข้ามหน้าเลือกองค์กร |
/authorize จะ 302 ตามสถานะล็อกอินไปที่: หน้าล็อกอิน GPTBots(ยังไม่ล็อกอิน)/ หน้าเลือกองค์กร(ล็อกอินแล้วแต่ยังไม่เลือกองค์กร)/ redirect_uri?code&state(เลือกองค์กรแล้ว)
คำขอและการตอบกลับ /token
body ของคำขอ(แบ็กเอนด์เรียกใช้):
{ "code": "…", "codeVerifier": "…" }
การตอบกลับเมื่อสำเร็จ:
{
"code": 0,
"msg": "OK",
"data": { "wsa": "eyJhbGciOiJIUzI1NiJ9...", "token_type": "Bearer", "expires_in": 300 }
}
การเชื่อมต่อ SDK
ฟรอนต์เอนด์: เริ่มการล็อกอิน(คลิกปุ่ม)
import { startWorkspaceLogin } from '@gptbots/workspace-extension-sdk';
// เมื่อคลิก「Login with GPTBots Workspace」:
await startWorkspaceLogin({
authorizeUrl: 'https://www.gptbots.ai/api/console/account/extension-app/authorize',
clientId: 'https://app.example.com/land', // = app home URL ที่คุณลงทะเบียน
redirectUri: 'https://app.example.com/callback', // host ต้องอยู่โดเมนเดียวกับ clientId
// workspaceId: 'p-xxx', // ตัวเลือก: เลือกพื้นที่ทำงานล่วงหน้า ข้ามหน้าเลือกองค์กร
// state: '...', // ตัวเลือก: ค่าเริ่มต้นสร้าง CSRF state สุ่ม 16 ไบต์อัตโนมัติ
});
// SDK ทำอัตโนมัติ: สร้าง PKCE(verifier→challenge), เก็บ verifier+state ใน sessionStorage,
// ตรวจสอบว่า authorizeUrl เป็น absolute URL, ต้องอยู่ในบริบทที่ปลอดภัย(HTTPS/localhost), แล้วเปลี่ยนเส้นทางไปที่ /authorize
ฟรอนต์เอนด์: หน้า Landing ของ callback
sequenceDiagram
participant FE as ฟรอนต์เอนด์ของแอปส่วนขยาย
participant GB as GPTBots
participant BE as แบ็กเอนด์ของแอปส่วนขยาย
Note over FE: startWorkspaceLogin()<br/>สร้าง PKCE(verifier→challenge)<br/>เก็บ sessionStorage, 302 เปลี่ยนเส้นทาง
FE->>GB: GET /authorize
alt ยังไม่ล็อกอิน
GB-->>FE: 302 ไปหน้าล็อกอิน GPTBots(ใช้การล็อกอินเดิมซ้ำ)
else ล็อกอินแล้ว, ยังไม่เลือกองค์กร
GB-->>FE: 302 หน้าเลือกองค์กร
else ล็อกอินแล้ว, เลือกองค์กรแล้ว
Note over GB: ออก code ใช้ครั้งเดียว(Redis,<br/>ผูกกับ account/project/app/redirect/challenge)
GB-->>FE: 302 redirect_uri?code&state
end
Note over FE: completeWorkspaceLogin()<br/>ตรวจสอบ state(CSRF), ดึง verifier ออกมา
FE->>BE: POST {code, codeVerifier}
Note over BE: exchangeWorkspaceCode()
BE->>GB: POST /token {code, verifier}
Note over GB: ตรวจสอบ code(ใช้ครั้งเดียว GETDEL) + PKCE<br/>ลงลายเซ็น wsa
GB-->>BE: คืนค่า wsa
Note over BE: verifyWsa(wsa) → สร้าง session ของตัวเอง
BE-->>FE: identity
แบ็กเอนด์: แลก wsa และตรวจสอบ
import { exchangeWorkspaceCode, verifyWsa } from '@gptbots/workspace-extension-verify';
// POST /session/workspace-login { code, codeVerifier }
app.post('/session/workspace-login', async (req, res) => {
try {
const { wsa } = await exchangeWorkspaceCode({
tokenUrl: 'https://www.gptbots.ai/api/console/account/extension-app/token',
code: req.body.code,
codeVerifier: req.body.codeVerifier,
// timeoutMs: 10000, // ค่าเริ่มต้น 10s, กันแพลตฟอร์มตอบช้าค้างคำขอของคุณ; ส่ง 0 เพื่อปิด
});
const identity = verifyWsa(wsa, {
secret: process.env.EXTENSION_APP_SECRET,
audience: 'app.example.com',
});
const sid = createSession(identity);
res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax' });
res.json(identity);
} catch (e) {
res.status(401).json({ error: String(e) });
}
});
wsaที่คืนมามีสัญญา JWT ใบเดียวกันกับแบบ Push, การใช้verifyWsaเหมือนกันทุกตัวอักษร
ข้อจำกัดด้านความปลอดภัย(ต้องอ่าน)
redirect_uriต้องอยู่โดเมนเดียวกับแอปที่ลงทะเบียน: scheme + host ต้องเท่ากับclient_idอย่างแม่นยำ/authorizeเป็น endpoint นำทางในเบราว์เซอร์ ไม่สามารถคืนข้อผิดพลาดแบบ JSON ได้; เมื่อredirect_uriขาดหาย / ไม่ใช่ http(s) / คนละโดเมน จะไม่มีวันเปลี่ยนเส้นทางไปยังที่อยู่ที่ไม่ผ่านการตรวจสอบ แต่จะกลับไปหน้าเลือกองค์กรพร้อม?error=invalid_requestนี่คือกุญแจสำคัญในการป้องกัน open redirect / การรั่วไหลของโทเค็น- บังคับใช้ PKCE: รับเฉพาะ
code_challenge_method=S256(แยกตัวพิมพ์ใหญ่-เล็ก),code_challenge = base64url(sha256(code_verifier))ไม่มี padding เวอร์ชันปัจจุบันไม่ใช้client_secretแต่ใช้ PKCE ผูก「เซสชันที่เริ่ม」กับ「เซสชันที่แลก」 codeใช้ครั้งเดียว: เก็บใน Redis, TTL 10 นาที, บริโภคแบบ atomic ตอนแลก, ไม่สามารถ replay ได้ replay/หมดอายุจะรายงาน403209 invalid_grant,code_verifierไม่ตรงจะรายงาน403210 invalid_verifierstate(CSRF): SDK เก็บstateและcode_verifierในsessionStorage, ตอน callback จะตรวจสอบว่าstateตรงกันจึงดำเนินต่อ- ขอบเขตองค์กร: ตอนขออนุญาตจะตรวจสอบว่าบัญชีเป็นสมาชิกของพื้นที่ทำงานที่เลือก และแอปนั้นใช้งานได้ในองค์กรนั้น(แอปส่วนขยายที่องค์กรใช้เองมองเห็นได้เฉพาะองค์กรที่เป็นเจ้าของ); หลังผู้ดูแลองค์กรปิดใช้งานแอปใด แม้แอปนั้นยังอยู่ในพจนานุกรมของแพลตฟอร์มก็จะไม่ออกโทเค็นให้องค์กรนั้น
รหัสข้อผิดพลาดในขั้นแลก /token
ข้อผิดพลาดเชิงโครงสร้างของ
/authorize(client_id/redirect_uriขาดหายหรือคนละโดเมน,code_challengeไม่ถูกต้อง,code_challenge_methodไม่ใช่S256)ไม่คืน JSON แต่จะ 302 กลับหน้าเลือกองค์กรพร้อม?error=invalid_requestตารางด้านล่างระบุเฉพาะรหัสข้อผิดพลาด JSON ในขั้น/token
| code | ความหมาย | เงื่อนไขที่ทำให้เกิด |
|---|---|---|
403209 |
Invalid grant | code ขาดหาย, หมดอายุ หรือถูกใช้ไปแล้ว(replay) |
403210 |
Invalid verifier | PKCE code_verifier ไม่ตรงกับ code_challenge |
ค่าของ ?error= บน callback URL: invalid_request(ข้อผิดพลาดเชิงโครงสร้าง)/ access_denied(ไม่ใช่สมาชิกหรือแอปใช้งานไม่ได้ในองค์กรนั้น)/ server_error(ล้มเหลวโดยไม่คาดคิด) completeWorkspaceLogin ของ SDK จะโยนมันเป็น WorkspaceLoginError('AuthorizeError')
ขั้นถัดไป: ไม่ว่าจะแบบ Push หรือ Pull เรื่องการตรวจสอบและความปลอดภัยให้อ่าน 05-การตรวจสอบโทเค็นและความปลอดภัย; API ครบถ้วนดูที่ 06-SDK-API-อ้างอิง
