เริ่มต้น
ต่อแอป Next.js
ลงทะเบียนแอป ใส่ SDK ป้องกันหน้า และตรวจสิทธิ์ ภายในไม่ถึงชั่วโมง
SDK @suphark/auth-client ให้แอป Next.js มีหน้าล็อกอิน session cookie ที่เข้ารหัส ต่ออายุโทเค็นอัตโนมัติ และตรวจสิทธิ์ ได้ในไม่กี่ไฟล์ ทุกอย่างด้านล่างรันบน Node.js runtime
ลงทะเบียนแอปใน Pi Auth
ในคอนโซลเปิด แอป → ลงทะเบียนแอป (แอปขององค์กร) หรือถ้าเป็น platform admin ลงทะเบียนแอปของแพลตฟอร์มให้ทุกองค์กรติดตั้งได้ ตั้ง redirect URI เป็น
https://your-app.example.com/auth/callbackแล้วคัดลอกclient_idและclient_secret— secret แสดงครั้งเดียวในกล่องเดียวกันเลือกวิธีล็อกอินที่แอปนี้เปิด (อีเมล+รหัสผ่าน, passkey, ลิงก์ทางอีเมล, Google, SSO ขององค์กร) — ตัวอย่างหน้าเข้าสู่ระบบทางขวาเปลี่ยนตามทันที และหน้า
/sign-inจริงจะแสดงเฉพาะที่เปิดพร้อมชื่อแอป แก้ทีหลังได้จากเมนู "แก้ไข"ติดตั้ง SDK
terminalpnpm add @suphark/auth-client # หรือ npm i / bun addตั้งค่า environment
ตัวแปร ค่า PI_AUTH_ISSUERhttps://auth.suphark.com — เฉพาะ origin ไม่มี /api/auth PI_AUTH_CLIENT_IDจากขั้นที่ 1 PI_AUTH_CLIENT_SECRETจากขั้นที่ 1 (ไม่ต้องใส่ถ้าเป็น public client) PI_AUTH_SESSION_SECRETข้อความสุ่มยาวอย่างน้อย 32 ตัวอักษร — ใช้เข้ารหัส session cookie APP_URLorigin สาธารณะของแอป ใช้ประกอบ redirect URI สร้าง client
src/lib/pi-auth.tsimport { createPiAuth } from "@suphark/auth-client/next"; export const piAuth = createPiAuth({ // issuer / clientId / clientSecret / sessionSecret / redirectUri อ่านจาก env ข้างบนเอง (SDK ≥ 0.7.1) session: { cookieName: "myapp.session", maxAgeSeconds: 60 * 60 * 8 }, // แอปที่ผูกกับองค์กรเดียว: ตั้ง PI_AUTH_TENANT=acme หรือใส่ defaultTenant: "acme" — ใส่ tenant ให้ทุกครั้งที่ล็อกอิน และปฏิเสธ token ขององค์กรอื่น // "local" = ปุ่มออกจากระบบออกเฉพาะแอปนี้ (ยังล็อกอินที่ Pi Auth อยู่ → เข้าใหม่ได้ทันที); ค่าเริ่มต้น "all" = ออกทุกแอป logoutScope: "local", // ไม่บังคับ: sync แถวผู้ใช้ของแอป (เรียกตอนเห็นผู้ใช้ ไม่เกินทุก 15 นาทีต่อผู้ใช้) // onUserSync: (user) => db.user.upsert({ where: { piAuthId: user.id }, update: {...}, create: {...} }), }); // layout: วาง <SessionExpiredRedirect /> (จาก @suphark/auth-client/react) ครั้งเดียว — server action ที่ session หมดจะพาไปล็อกอินแล้วกลับหน้าเดิม // ในหน้า / server action: ต้องล็อกอิน (+ มีสิทธิ์ / ยืนยันตัวตนสดสำหรับงานสำคัญ) — ไม่ต้องเขียน wrapper เอง // const user = await piAuth.requireUser({ permission: "app:use" }); // await piAuth.requireUser({ permission: "app:admin", fresh: 300 });ไม่ส่งค่าให้
createPiAuthก็ได้ — อ่านจาก env ข้างบน (PI_AUTH_TENANT→defaultTenant); ขาดตัวไหนจะบอกชื่อ env ที่ขาดครบในครั้งเดียว ถ้ามีUPSTASH_REDIS_REST_URL/TOKENระบบถอน session (back-channel logout) ครบทุก instance ให้อัตโนมัติผู้ใช้ที่อยู่หลายองค์กร: ส่งไป
/auth/login?tenant=<slug>(ชนะdefaultTenant); ออกจากระบบ:/auth/logout?scope=local&next=/loginหรือ?scope=all— หรือใช้piAuth.logoutUrl({ scope, next })ติดตั้ง route ของ auth
route แบบ catch-all ตัวเดียวให้บริการ
/auth/login,/auth/callback,/auth/logout,/auth/sessionและ/auth/backchannel-logoutapp/auth/[...pi]/route.tsimport { piAuth } from "@/lib/pi-auth"; export const { GET, POST } = piAuth.handlers;ป้องกันหน้าด้วย middleware
middleware พาผู้ที่ยังไม่ล็อกอินไปหน้าล็อกอิน และต่ออายุโทเค็นที่ใกล้หมดให้เบื้องหลัง ระบุ path ที่เปิดสาธารณะไว้
proxy.ts (Next 16) / middleware.tsimport { piAuth } from "@/lib/pi-auth"; export default piAuth.middleware({ publicPaths: ["/", "/api/webhooks"] });อ่านผู้ใช้และตรวจสิทธิ์
server component / route handlerimport { piAuth } from "@/lib/pi-auth"; const user = await piAuth.getCurrentUser(); // null ถ้ายังไม่ล็อกอิน piAuth.requirePermission(user, "memo:manage"); // throw 403 problem user?.tenant?.slug; // "acme" user?.org?.path; // "/acme/eng/it" user?.perms; // ["memo:create", ...]client component"use client"; import { usePiAuth } from "@suphark/auth-client/react"; export function ApproveButton() { const { user, hasPermission } = usePiAuth(); if (!hasPermission("memo:approve")) return null; return <button>อนุมัติ</button>; }ประกาศ permission และผูก role
ส่ง permission manifest ให้องค์กรเห็นว่า
memo:manageหมายถึงอะไร แล้วให้ผู้ดูแลติดตั้งแอปและ ผูก role ตราบใดที่ยังไม่ผูกpermsจะว่างและrequirePermissionจะปฏิเสธ
SDK ทำอะไรให้บ้าง
- รัน authorization code flow พร้อม PKCE,
stateและnonceทุกครั้งที่ล็อกอิน - เก็บ access/refresh token ใน cookie ที่เข้ารหัส A256GCM (HttpOnly, SameSite=Lax, Secure บน https) — JavaScript ในเบราว์เซอร์แตะไม่ถึง
- ตรวจ JWT กับ JWKS ของ issuer พร้อม cache และรองรับการหมุน key, เผื่อเวลาเคลื่อน 60 วินาที
- ต่ออายุโทเค็นใน middleware ก่อนหมดอายุ; รองรับ back-channel logout เมื่อ session ถูกถอนใน Pi Auth
- รับ
?next=เฉพาะ path ภายใน จึงใช้เป็น open redirect ไม่ได้ /auth/login?tenant=<slug>ขอองค์กรที่เจาะจง ถ้าได้โทเค็นของ tenant อื่นจะปฏิเสธด้วยTENANT_MISMATCH
เมื่อล็อกอินไม่สำเร็จ
callback ที่ล้มเหลวจะไม่โชว์ JSON ดิบให้ผู้ใช้: SDK ล้าง transaction แล้วพาไป errorRedirect (ค่าเริ่มต้น /) พร้อม ?auth_error=<code> — ส่งเฉพาะรหัส ไม่ส่งข้อความ และรับเฉพาะ path ภายใน อ่านที่หน้านั้นแล้วแสดงข้อความของแอปเอง:
import { readAuthError } from "@suphark/auth-client/next";
export default async function Home({ searchParams }) {
const authError = readAuthError(await searchParams);
// { code: "TENANT_MISMATCH", tenant: "beta" } | { code: "TRANSACTION_EXPIRED" } | null
return authError ? <Banner code={authError.code} tenant={authError.tenant} /> : <App />;
}| รหัส | ความหมาย | ควรแสดงอะไร |
|---|---|---|
TRANSACTION_EXPIRED | เริ่มล็อกอินเกิน 10 นาที หรือ cookie หาย | “ล็อกอินใหม่อีกครั้ง” |
STATE_MISMATCH | callback ไม่ตรงกับการล็อกอินที่เริ่มไว้ (กด back, ลิงก์เก่า) | “ล็อกอินใหม่อีกครั้ง” |
TENANT_MISMATCH | ขอ ?tenant=X แต่ผู้ใช้ไม่ได้เป็นสมาชิก X; tenant ที่ขออยู่ใน tenant | “คุณไม่ได้เป็นสมาชิกของ X” + ปุ่มล็อกอินโดยไม่ระบุ tenant |
TOKEN_REQUEST_FAILED | Pi Auth ปฏิเสธการแลก code (client secret หรือ redirect URI ผิด) | ข้อความทั่วไป; ตรวจ env ของแอป |
INVALID_TOKEN | token ตรวจไม่ผ่าน (issuer, audience, ลายเซ็น) | ข้อความทั่วไป; ตรวจ PI_AUTH_ISSUER |
ส่ง onError(error, request) ให้ createPiAuth ถ้าต้องการตอบเองทั้งหมด request ที่ขอ application/json ยังได้ JSON แบบเดิม
เรียก Pi Auth จากฝั่งหลังบ้าน
งานที่ไม่มีผู้ใช้ล็อกอิน (sync ตอนกลางคืน, ตัวหาผู้อนุมัติที่ต้องใช้ผังองค์กร) ให้สร้าง service account แล้วส่ง credential ของมันใน service:
const svc = piAuth.serviceClient(); // ต้องตั้ง service: { clientId, clientSecret, scopes } ใน createPiAuth
await svc.directory.resolve({
positionCode: "DIR",
withinOrgPathOf: user!.tenant!.membershipId!,
}); // → ผู้อำนวยการที่อยู่เหนือหน่วยงานของสมาชิกคนนี้