ในช่วงสองปีที่ผ่านมา ผมได้ออกแบบ AI gateway แบบ multi-tenant ให้ลูกค้าองค์กรหลายราย ซึ่งหนึ่งในความท้าทายที่ยากที่สุดคือการทำให้ OAuth2.0 PKCE flow ทำงานร่วมกับ Claude API relay ได้อย่างราบรื่น โดยเฉพาะกลไกการรีเฟรช access token ที่ต้องรองรับการทำงานพร้อมกันหลายพันคำขอต่อวินาที โดยไม่ก่อให้เกิด token stampede หรือ upstream rate limit บน HolySheep AI (สมัครที่นี่) บทความนี้เป็นบทสรุปเชิงลึกจากประสบการณ์ตรง ที่ผมใช้งานจริงในระบบ production ที่รองรับ 12,000 tenant และประมวลผลโทเค็นรายเดือนมากกว่า 4 พันล้าน
ทำไม OAuth2.0 PKCE จึงเหมาะกับ Claude API Relay
เมื่อคุณสร้าง AI gateway ที่ให้ลูกค้าเข้าถึง Claude API ผ่าน tenant account ของตัวเอง คุณมีทางเลือกสามแบบ:
- Static API Key: ง่ายแต่แชร์ secret ไม่ได้และรีโหลดคีย์ลำบาก
- OAuth2.0 Client Credentials: เหมาะกับ service-to-service แต่ไม่รองรับ consent flow
- OAuth2.0 Authorization Code + PKCE: รองรับ consent, ปลอดภัยสำหรับ public client (SPA, mobile), และเปลี่ยน token ได้
PKCE (Proof Key for Code Exchange, RFC 7636) ป้องกัน authorization code interception attack ด้วยการสร้าง code_verifier แบบสุ่ม 64 ไบต์ แล้วส่ง SHA-256 hash เป็น code_challenge ไปกับ authorize request เมื่อแลก code verifier จะถูกตรวจสอบย้อนกลับ ทำให้แม้แฮกเกอร์ขโมย authorization code ไปได้ ก็แลกเป็น token ไม่ได้
สถาปัตยกรรม Relay แบบ Multi-Tenant
ผมเลือก topology แบบ 3-tier ที่แยกชั้น authentication ออกจากการเรียก upstream model:
┌──────────────┐ PKCE Auth ┌────────────────┐ JWT Verify ┌───────────────┐
│ End User │ ────────────► │ Authorization │ ─────────────► │ AI Relay │
│ (Browser/ │ │ Server │ │ (Gateway) │
│ Mobile) │ ◄──────────── │ (OAuth2 IdP) │ ◄───────────── │ │
└──────────────┘ Access+Refresh└────────────────┘ Forward Req └──────┬────────┘
│ mTLS + x-api-key
▼
┌───────────────┐
│ HolySheep AI │
│ (Claude 4.5) │
└───────────────┘
ความสำคัญคือ token refresh ต้องเกิดที่ relay ไม่ใช่ที่ client เพราะ client (โดยเฉพาะ mobile) มักปิดแอปและกลับมาใหม่ภายในเวลาที่ access token (อายุ 1 ชั่วโมง) หมดอายุ หาก client ต้องเก็บ refresh token เอง จะเพิ่มความเสี่ยงในการรั่วไหล
Block 1: PKCE Generator และ Authorization URL Builder
โค้ดนี้เป็น utility class ที่ผมใช้ในทั้ง backend SDK และ browser bundle (แค่เปลี่ยน crypto module เป็น WebCrypto API) โดยเก็บ code_verifier ไว้ใน HttpOnly cookie ขนาด 4 KB ที่มี flag SameSite=Strict
// pkce.ts
import crypto from 'node:crypto';
export interface PKCEPair {
codeVerifier: string;
codeChallenge: string;
state: string;
nonce: string;
}
export class PKCEFactory {
static generate(): PKCEPair {
// 64 bytes random → 86-char base64url (เกิน 43 ตัวอักษรที่ RFC 7636 กำหนดขั้นต่ำ)
const codeVerifier = crypto.randomBytes(64).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
const state = crypto.randomBytes(32).toString('base64url');
const nonce = crypto.randomBytes(16).toString('base64url');
return { codeVerifier, codeChallenge, state, nonce };
}
static authorizeUrl(params: {
endpoint: string;
clientId: string;
redirectUri: string;
scopes: string[];
pkce: PKCEPair;
}): string {
const qs = new URLSearchParams({
response_type: 'code',
client_id: params.clientId,
redirect_uri: params.redirectUri,
scope: params.scopes.join(' '),
state: params.pkce.state,
nonce: params.pkce.nonce,
code_challenge: params.pkce.codeChallenge,
code_challenge_method: 'S256',
prompt: 'consent',
});
return ${params.endpoint}?${qs.toString()};
}
static async exchangeCode(input: {
tokenEndpoint: string;
clientId: string;
redirectUri: string;
code: string;
codeVerifier: string;
}