eCourt-Doc-Edit/web/app/utils/oidc.ts
2026-08-20 06:20:16 +07:00

75 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string> {
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<string, unknown> | 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<string, unknown>
} catch {
return null
}
}