/** * Helpers สำหรับ OIDC + PKCE — เป็น pure functions (แยกจาก useAuth) เพื่อ unit test ได้ง่าย * เว็บเป็น top-level page จึงใช้ browser redirect ปกติได้ (ไม่ต้องมี Office Dialog API แบบ add-in) */ /** เข้ารหัส bytes เป็น base64url (ไม่มี padding) ตาม RFC 7636 */ export function toBase64Url(bytes: Uint8Array): string { let binary = '' for (const b of bytes) binary += String.fromCharCode(b) return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } /** code_verifier ของ PKCE — 48 random bytes → base64url 64 อักขระ (อยู่ในช่วง 43–128 ตาม spec) */ export function generateCodeVerifier(): string { const bytes = new Uint8Array(48) crypto.getRandomValues(bytes) return toBase64Url(bytes) } /** state สุ่มสำหรับกัน CSRF — เทียบตอนรับ code กลับ */ export function generateState(): string { const bytes = new Uint8Array(16) crypto.getRandomValues(bytes) return toBase64Url(bytes) } /** code_challenge = base64url(SHA-256(verifier)) วิธี S256 */ export async function createCodeChallenge(verifier: string): Promise { const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)) return toBase64Url(new Uint8Array(digest)) } export interface AuthorizeUrlParams { authorizeEndpoint: string clientId: string redirectUri: string state: string codeChallenge: string scope?: string } /** ประกอบ authorization URL แบบ authorization code + PKCE S256 */ export function buildAuthorizeUrl(params: AuthorizeUrlParams): string { const qs = new URLSearchParams({ client_id: params.clientId, response_type: 'code', scope: params.scope ?? 'openid', redirect_uri: params.redirectUri, state: params.state, code_challenge: params.codeChallenge, code_challenge_method: 'S256', }) return `${params.authorizeEndpoint}?${qs.toString()}` } /** รูป response ของ token endpoint (เฉพาะ field ที่เราใช้) */ export interface TokenResponse { access_token: string refresh_token?: string id_token?: string expires_in?: number } /** decode payload ของ JWT เพื่ออ่าน claims (exp / preferred_username) — token ไม่ถูกต้องคืน null ไม่ throw */ export function parseJwtPayload(token: string): Record | null { const parts = token.split('.') if (parts.length !== 3) return null try { const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/') const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)) return JSON.parse(new TextDecoder().decode(bytes)) as Record } catch { return null } }