@nihplod/sso-sdk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +374 -360
- package/dist/index.d.mts +99 -12
- package/dist/index.d.ts +99 -12
- package/dist/index.js +413 -75
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +408 -74
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +17 -2
- package/dist/next/index.d.ts +17 -2
- package/dist/next/index.js +276 -45
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +274 -45
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +71 -8
- package/dist/react/index.d.ts +71 -8
- package/dist/react/index.js +476 -111
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +476 -111
- package/dist/react/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/errors.ts","../src/core/pkce.ts","../src/core/storage.ts","../src/core/SsoClient.ts"],"sourcesContent":["/**\r\n * SSO SDK 错误类型\r\n */\r\n\r\n/** SSO 错误码 */\r\nexport type SsoErrorCode =\r\n | \"invalid_config\"\r\n | \"state_mismatch\"\r\n | \"pkce_required\"\r\n | \"token_request_failed\"\r\n | \"session_expired\"\r\n | \"no_refresh_token\"\r\n | \"userinfo_failed\"\r\n | \"not_authenticated\"\r\n | \"sso_server_error\"\r\n | \"network_error\"\r\n | \"id_token_invalid\"\r\n | \"id_token_unsupported_alg\"\r\n | \"id_token_missing_secret\"\r\n | \"id_token_invalid_signature\"\r\n | \"id_token_issuer_mismatch\"\r\n | \"id_token_audience_mismatch\"\r\n | \"id_token_expired\"\r\n | \"id_token_missing_sub\"\r\n | \"id_token_at_hash_mismatch\";\r\n\r\n/** OAuth 2.0 标准错误码 */\r\nexport type OAuthErrorCode =\r\n | \"invalid_request\"\r\n | \"invalid_client\"\r\n | \"invalid_grant\"\r\n | \"unauthorized_client\"\r\n | \"unsupported_grant_type\"\r\n | \"invalid_scope\"\r\n | \"access_denied\"\r\n | \"server_error\"\r\n | \"rate_limited\";\r\n\r\n/**\r\n * SSO SDK 自定义错误\r\n */\r\nexport class SsoError extends Error {\r\n public readonly code: SsoErrorCode;\r\n public readonly description: string;\r\n public readonly cause?: unknown;\r\n\r\n constructor(code: SsoErrorCode, description: string, cause?: unknown) {\r\n super(`[SSO SDK] ${code}: ${description}`);\r\n this.name = \"SsoError\";\r\n this.code = code;\r\n this.description = description;\r\n this.cause = cause;\r\n }\r\n}\r\n\r\n/**\r\n * OAuth 2.0 服务端返回的错误\r\n */\r\nexport class OAuthError extends Error {\r\n public readonly code: OAuthErrorCode;\r\n public readonly description: string;\r\n public readonly uri?: string;\r\n\r\n constructor(\r\n code: OAuthErrorCode,\r\n description: string,\r\n uri?: string\r\n ) {\r\n super(`[OAuth] ${code}: ${description}`);\r\n this.name = \"OAuthError\";\r\n this.code = code;\r\n this.description = description;\r\n this.uri = uri;\r\n }\r\n}\r\n","/**\r\n * PKCE (Proof Key for Code Exchange) 工具函数\r\n *\r\n * 实现 RFC 7636 规范:\r\n * - code_verifier: 43-128 字符的随机字符串\r\n * - code_challenge: SHA-256(code_verifier) 的 base64url 编码\r\n *\r\n * 仅支持 S256 模式,不提供 plain 模式。\r\n */\r\n\r\nconst VALID_CHARS =\r\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\r\n\r\n/**\r\n * 生成密码学安全随机字符串\r\n *\r\n * 使用拒绝采样法(rejection sampling)避免取模非均匀分布。\r\n * VALID_CHARS 有 66 个字符,Uint8(0-255) 取模会偏向前 58 个字符。\r\n * 丢弃 >=198 的字节值使分布完全均匀(198 = floor(256/66) * 66)。\r\n */\r\nfunction generateRandomString(length: number): string {\r\n const mask = 198; // 66 * 3,最大均匀取值\r\n let result = \"\";\r\n\r\n while (result.length < length) {\r\n // 一次生成分块避免过小 array 的循环开销\r\n const chunkSize = Math.min(length * 2, 256);\r\n const array = new Uint8Array(chunkSize);\r\n crypto.getRandomValues(array);\r\n\r\n for (let i = 0; i < chunkSize && result.length < length; i++) {\r\n if (array[i] >= mask) continue; // 拒绝采样:丢弃 >=198 的字节\r\n result += VALID_CHARS[array[i] % VALID_CHARS.length];\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * 生成 code_verifier\r\n *\r\n * 生成 43-128 字符之间的安全随机字符串,\r\n * 使用 `crypto.getRandomValues` 保证密码学安全性。\r\n *\r\n * @param length - 长度(默认 64),范围 43-128\r\n */\r\nexport function generateCodeVerifier(length: number = 64): string {\r\n if (length < 43 || length > 128) {\r\n throw new Error(\"code_verifier length must be between 43 and 128\");\r\n }\r\n return generateRandomString(length);\r\n}\r\n\r\n/**\r\n * 计算 code_challenge (S256)\r\n *\r\n * 对 code_verifier 进行 SHA-256 哈希,然后进行 base64url 编码。\r\n *\r\n * @param codeVerifier - PKCE code_verifier\r\n */\r\nexport async function generateCodeChallenge(\r\n codeVerifier: string\r\n): Promise<string> {\r\n const encoder = new TextEncoder();\r\n const data = encoder.encode(codeVerifier);\r\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\r\n\r\n // 转为 base64url(无 padding)\r\n const bytes = new Uint8Array(hash);\r\n let binary = \"\";\r\n for (let i = 0; i < bytes.byteLength; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\r\n}\r\n\r\n/**\r\n * 生成 state 参数\r\n *\r\n * 32 字节 hex 随机字符串,用于 CSRF 防护。\r\n */\r\nexport function generateState(): string {\r\n const array = new Uint8Array(32);\r\n crypto.getRandomValues(array);\r\n return Array.from(array, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\r\n}\r\n","/**\n * Token 存储抽象层\n *\n * 默认使用 localStorage,以支持多 Tab 间自动同步 token 并避免并发刷新。\n * 对 XSS 敏感的子项目可通过 setTokenStorage() 注入更安全的自定义实现\n *(如内存存储、Service Worker 封装、或带加密的 storage)。\n *\n * 多 client 隔离:\n * - token / state / return_url 均支持按 clientId 隔离 key\n * - 不传 clientId 时使用全局 key,保持向后兼容\n */\n\n/** Token 数据 */\nexport interface TokenData {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token: string;\n id_token?: string;\n\n /** Token 签发时间(epoch ms),用于计算过期 */\n issued_at: number;\n\n /** 访问令牌实际过期时间(epoch ms)= issued_at + expires_in * 1000 */\n expires_at: number;\n}\n\n/** 存储接口 */\nexport interface TokenStorage {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\n/** 默认 localStorage/sessionStorage key 前缀 */\nconst STORAGE_PREFIX = \"nihplod_sso_\";\n\n/** 常量 key 名称 */\nconst TOKEN_KEY = \"token\";\nconst VERIFIER_KEY_PREFIX = \"pkce_verifier_\";\nconst STATE_KEY = \"oauth_state\";\nconst RETURN_URL_KEY = \"return_url\";\n\nfunction buildKey(base: string, clientId?: string): string {\n return clientId ? `${base}:${clientId}` : base;\n}\n\n/**\n * 内存存储适配器(Public Client 浏览器默认)\n *\n * 安全优势:\n * - refresh_token 不落盘,XSS 无法窃取长期凭证。\n * - 页面刷新后用户需重新授权,符合纯前端 Public Client 的安全模型。\n *\n * 代价:多 Tab 间不会自动同步 token;SDK 内部已用锁机制避免并发刷新。\n */\nfunction createMemoryStorageAdapter(): TokenStorage {\n const store = new Map<string, string>();\n return {\n get(key: string) {\n return store.get(key) ?? null;\n },\n set(key: string, value: string) {\n store.set(key, value);\n },\n remove(key: string) {\n store.delete(key);\n },\n };\n}\n\nconst memoryStorageAdapter = createMemoryStorageAdapter();\n\n/**\n * 浏览器 localStorage 实现\n *\n * 适用于:\n * - Next.js BFF / Confidential Client(refresh_token 不直接暴露给浏览器)。\n * - 需要多 Tab 共享 token 的场景。\n *\n * 注意:Public Client 在浏览器中直接存储 refresh_token 会增加 XSS 风险,\n * 建议改用内存存储或 Service Worker 封装。\n */\nconst localStorageAdapter: TokenStorage = {\n get(key: string) {\n if (typeof localStorage === \"undefined\") return null;\n return localStorage.getItem(STORAGE_PREFIX + key);\n },\n set(key: string, value: string) {\n if (typeof localStorage === \"undefined\") return;\n localStorage.setItem(STORAGE_PREFIX + key, value);\n },\n remove(key: string) {\n if (typeof localStorage === \"undefined\") return;\n localStorage.removeItem(STORAGE_PREFIX + key);\n },\n};\n\n/**\n * 创建安全存储实现\n *\n * @param options.persist 是否持久化到 localStorage。默认 false(内存存储)。\n * - Confidential/BFF 子项目可设为 true。\n * - Public Client 在浏览器中建议保持 false。\n */\nexport function createSecureStorage(options: { persist?: boolean } = {}): TokenStorage {\n return options.persist ? localStorageAdapter : memoryStorageAdapter;\n}\n\nlet _storage: TokenStorage = memoryStorageAdapter;\n\n/**\n * 设置自定义存储实现\n */\nexport function setTokenStorage(storage: TokenStorage): void {\n _storage = storage;\n}\n\n/**\n * 获取当前存储实现\n */\nexport function getTokenStorage(): TokenStorage {\n return _storage;\n}\n\n// ============================================\n// Token 存取\n// ============================================\n\nexport function saveTokenData(data: TokenData, clientId?: string): void {\n _storage.set(buildKey(TOKEN_KEY, clientId), JSON.stringify(data));\n}\n\nexport function getTokenData(clientId?: string): TokenData | null {\n const raw = _storage.get(buildKey(TOKEN_KEY, clientId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as TokenData;\n } catch {\n return null;\n }\n}\n\nexport function removeTokenData(clientId?: string): void {\n _storage.remove(buildKey(TOKEN_KEY, clientId));\n}\n\n// ============================================\n// PKCE 临时数据存取\n// ============================================\n\nexport function savePkceVerifier(clientId: string, verifier: string): void {\n _storage.set(VERIFIER_KEY_PREFIX + clientId, verifier);\n}\n\nexport function getPkceVerifier(clientId: string): string | null {\n return _storage.get(VERIFIER_KEY_PREFIX + clientId);\n}\n\nexport function removePkceVerifier(clientId: string): void {\n _storage.remove(VERIFIER_KEY_PREFIX + clientId);\n}\n\n// ============================================\n// State 参数存取\n// ============================================\n\nexport function saveOAuthState(state: string, clientId?: string): void {\n _storage.set(buildKey(STATE_KEY, clientId), state);\n}\n\nexport function getOAuthState(clientId?: string): string | null {\n return _storage.get(buildKey(STATE_KEY, clientId));\n}\n\nexport function removeOAuthState(clientId?: string): void {\n _storage.remove(buildKey(STATE_KEY, clientId));\n}\n\n// ============================================\n// 返回 URL 存取\n// ============================================\n\nexport function saveReturnUrl(url: string, clientId?: string): void {\n _storage.set(buildKey(RETURN_URL_KEY, clientId), url);\n}\n\nexport function getReturnUrl(clientId?: string): string | null {\n return _storage.get(buildKey(RETURN_URL_KEY, clientId));\n}\n\nexport function removeReturnUrl(clientId?: string): void {\n _storage.remove(buildKey(RETURN_URL_KEY, clientId));\n}\n\n// ============================================\n// 清理所有 SSO 数据\n// ============================================\n\nexport function clearAllSsoData(clientId?: string): void {\n if (clientId) {\n removeTokenData(clientId);\n removeOAuthState(clientId);\n removeReturnUrl(clientId);\n removePkceVerifier(clientId);\n return;\n }\n\n removeTokenData();\n removeOAuthState();\n removeReturnUrl();\n\n // 清理所有 PKCE verifier(遍历已知前缀,localStorage key 可枚举)\n if (typeof localStorage !== \"undefined\") {\n const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key?.startsWith(prefix)) {\n _storage.remove(key.slice(STORAGE_PREFIX.length));\n }\n }\n }\n}\n\n/**\n * 清理指定 clientId 列表的 PKCE verifier\n *\n * 适用于非浏览器环境或已知 clientId 场景。\n */\nexport function clearVerifiersForClients(clientIds: string[]): void {\n for (const clientId of clientIds) {\n _storage.remove(VERIFIER_KEY_PREFIX + clientId);\n }\n}\n","/**\r\n * SSO Client 核心类\r\n *\r\n * 封装完整的 OAuth 2.0 授权码 + PKCE S256 流程:\r\n * - login(): 发起授权请求\r\n * - handleCallback(): 处理回调,交换 token\r\n * - refreshToken(): 刷新 access token(原子轮换)\r\n * - getUserInfo(): 获取用户信息\r\n * - logout(): 登出\r\n * - isAuthenticated(): 检查认证状态\r\n */\r\n\r\nimport { SsoError } from \"./errors\";\r\nimport {\r\n generateCodeVerifier,\r\n generateCodeChallenge,\r\n generateState,\r\n} from \"./pkce\";\r\nimport {\r\n saveTokenData,\r\n getTokenData,\r\n removeTokenData,\r\n savePkceVerifier,\r\n getPkceVerifier,\r\n removePkceVerifier,\r\n saveOAuthState,\r\n getOAuthState,\r\n removeOAuthState,\r\n saveReturnUrl,\r\n getReturnUrl,\r\n removeReturnUrl,\r\n clearAllSsoData,\r\n type TokenData,\r\n} from \"./storage\";\r\n\r\n// ============================================\r\n// ID Token 验证辅助函数(浏览器原生 Web Crypto)\r\n// ============================================\r\n\r\nfunction base64UrlDecode(input: string): Uint8Array {\r\n const base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\r\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\r\n const binary = atob(padded);\r\n const bytes = new Uint8Array(binary.length);\r\n for (let i = 0; i < binary.length; i++) {\r\n bytes[i] = binary.charCodeAt(i);\r\n }\r\n return bytes;\r\n}\r\n\r\n/** 解码 JWT payload(不验证签名) */\r\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\r\n try {\r\n const parts = token.split(\".\");\r\n if (parts.length !== 3) return null;\r\n const json = new TextDecoder().decode(base64UrlDecode(parts[1]));\r\n return JSON.parse(json) as Record<string, unknown>;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** 解码 JWT header */\r\nfunction decodeJwtHeader(token: string): Record<string, unknown> | null {\r\n try {\r\n const parts = token.split(\".\");\r\n if (parts.length !== 3) return null;\r\n const json = new TextDecoder().decode(base64UrlDecode(parts[0]));\r\n return JSON.parse(json) as Record<string, unknown>;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** 标准化 issuer/base URL,去除末尾斜杠 */\r\nfunction normalizeIssuer(url: string): string {\r\n return url.replace(/\\/+$/, \"\");\r\n}\r\n\r\ninterface JwksKey {\r\n kty: string;\r\n kid?: string;\r\n alg?: string;\r\n use?: string;\r\n n?: string;\r\n e?: string;\r\n x5c?: string[];\r\n}\r\n\r\ninterface Jwks {\r\n keys: JwksKey[];\r\n}\r\n\r\nlet cachedJwks: { baseUrl: string; jwks: Jwks; fetchedAt: number } | null = null;\r\nconst JWKS_CACHE_TTL_MS = 5 * 60 * 1000;\r\n\r\n/** 从 SSO 中心拉取 JWKS */\r\nasync function fetchJwks(baseUrl: string): Promise<Jwks | null> {\r\n const now = Date.now();\r\n if (cachedJwks && cachedJwks.baseUrl === baseUrl && now - cachedJwks.fetchedAt < JWKS_CACHE_TTL_MS) {\r\n return cachedJwks.jwks;\r\n }\r\n\r\n try {\r\n const res = await fetch(`${baseUrl}/api/oauth/jwks`);\r\n if (!res.ok) return null;\r\n const jwks = (await res.json()) as Jwks;\r\n cachedJwks = { baseUrl, jwks, fetchedAt: now };\r\n return jwks;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** 计算 OIDC at_hash(access_token SHA-256 前 128bit base64url) */\r\nasync function computeAtHash(accessToken: string): Promise<string> {\r\n const hash = await crypto.subtle.digest(\r\n \"SHA-256\",\r\n new TextEncoder().encode(accessToken)\r\n );\r\n const bytes = new Uint8Array(hash);\r\n const half = bytes.slice(0, bytes.length / 2);\r\n let binary = \"\";\r\n for (const b of half) {\r\n binary += String.fromCharCode(b);\r\n }\r\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\r\n}\r\n\r\n/** 使用 RS256 公钥验证 ID Token 签名 */\r\nasync function verifyRs256(token: string, jwk: JwksKey): Promise<boolean> {\r\n try {\r\n const [headerB64, payloadB64, signature] = token.split(\".\");\r\n if (!signature || !jwk.n || !jwk.e) return false;\r\n\r\n const cryptoKey = await crypto.subtle.importKey(\r\n \"jwk\",\r\n { kty: \"RSA\", n: jwk.n, e: jwk.e, alg: \"RS256\", ext: false },\r\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\r\n false,\r\n [\"verify\"]\r\n );\r\n\r\n const signatureBytes = base64UrlDecode(signature);\r\n const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);\r\n return await crypto.subtle.verify(\r\n \"RSASSA-PKCS1-v1_5\",\r\n cryptoKey,\r\n signatureBytes as unknown as BufferSource,\r\n data as unknown as BufferSource\r\n );\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/** 验证 ID Token(iss、aud、exp、sub、签名、at_hash) */\r\nasync function validateIdToken(\r\n idToken: string,\r\n expectedIssuer: string,\r\n expectedClientId: string,\r\n accessToken: string\r\n) {\r\n const header = decodeJwtHeader(idToken);\r\n if (!header) {\r\n throw new SsoError(\"id_token_invalid\", \"ID Token 格式错误\");\r\n }\r\n\r\n const alg = header.alg;\r\n if (typeof alg !== \"string\" || (alg !== \"RS256\" && alg !== \"HS256\")) {\r\n throw new SsoError(\"id_token_unsupported_alg\", `不支持的 ID Token 签名算法: ${alg}`);\r\n }\r\n\r\n // RS256:通过 JWKS 验证签名\r\n if (alg === \"RS256\") {\r\n const jwks = await fetchJwks(normalizeIssuer(expectedIssuer));\r\n if (!jwks) {\r\n throw new SsoError(\"id_token_invalid_signature\", \"无法获取 JWKS 验证 ID Token 签名\");\r\n }\r\n const kid = typeof header.kid === \"string\" ? header.kid : undefined;\r\n const key = jwks.keys.find(\r\n (k) =>\r\n k.kty === \"RSA\" &&\r\n k.alg === \"RS256\" &&\r\n k.use === \"sig\" &&\r\n (kid ? k.kid === kid : true)\r\n );\r\n if (!key) {\r\n throw new SsoError(\"id_token_invalid_signature\", \"JWKS 中未找到匹配的 RS256 公钥\");\r\n }\r\n const validSig = await verifyRs256(idToken, key);\r\n if (!validSig) {\r\n throw new SsoError(\"id_token_invalid_signature\", \"ID Token 签名验证失败\");\r\n }\r\n }\r\n\r\n // HS256:对称密钥无法安全分发给 Public Client,因此仅校验声明、不校验签名。\r\n // Confidential Client 如确需验证签名,应迁移到 RS256。\r\n if (alg === \"HS256\") {\r\n // eslint-disable-next-line no-console\r\n console.warn(\r\n \"[SSO SDK] ID Token 使用 HS256 签名,Public Client 无法安全验证签名。\" +\r\n \"建议主站配置 JWT_ID_TOKEN_PRIVATE_KEY / JWT_ID_TOKEN_PUBLIC_KEY 启用 RS256。\"\r\n );\r\n }\r\n\r\n const payload = decodeJwtPayload(idToken);\r\n if (!payload) {\r\n throw new SsoError(\"id_token_invalid\", \"ID Token payload 解析失败\");\r\n }\r\n\r\n const normalizedExpectedIssuer = normalizeIssuer(expectedIssuer);\r\n const tokenIssuer = typeof payload.iss === \"string\" ? normalizeIssuer(payload.iss) : \"\";\r\n if (tokenIssuer !== normalizedExpectedIssuer) {\r\n throw new SsoError(\"id_token_issuer_mismatch\", \"ID Token issuer 不匹配\");\r\n }\r\n if (\r\n payload.aud !== expectedClientId &&\r\n !((payload.aud as string[] | undefined)?.includes(expectedClientId))\r\n ) {\r\n throw new SsoError(\"id_token_audience_mismatch\", \"ID Token audience 不匹配\");\r\n }\r\n if (typeof payload.exp === \"number\" && Date.now() >= payload.exp * 1000) {\r\n throw new SsoError(\"id_token_expired\", \"ID Token 已过期\");\r\n }\r\n if (typeof payload.sub !== \"string\" || !payload.sub) {\r\n throw new SsoError(\"id_token_missing_sub\", \"ID Token 缺少 sub\");\r\n }\r\n\r\n // 校验 at_hash:确保 ID Token 与当前 access_token 绑定\r\n if (typeof payload.at_hash === \"string\" && payload.at_hash) {\r\n const actual = await computeAtHash(accessToken);\r\n if (actual !== payload.at_hash) {\r\n throw new SsoError(\"id_token_at_hash_mismatch\", \"ID Token at_hash 不匹配\");\r\n }\r\n }\r\n\r\n return { sub: payload.sub };\r\n}\r\n\r\n// ============================================\r\n// 类型定义\r\n// ============================================\r\n\r\n/** SSO 客户端配置 */\r\nexport interface SsoClientConfig {\r\n /** OAuth Client ID(从管理后台获取) */\r\n clientId: string;\r\n\r\n /** OAuth Client Secret(从管理后台获取)\r\n *\r\n * ⚠️ 安全提醒:client_secret 不应暴露在前端代码中。\r\n * 对于浏览器端 SPA(Public Client),应省略此字段;SDK 会使用 PKCE 完成授权,\r\n * 且 token 端点不会发送 client_secret。\r\n * 对于 BFF/Next.js 等 Confidential Client,可传入 client_secret。\r\n */\r\n clientSecret?: string;\r\n\r\n /** 回调 URL(必须与注册的 redirect_uri 完全一致) */\r\n redirectUri: string;\r\n\r\n /** SSO 中心地址,如 \"https://nihplod.cn\" */\r\n ssoBaseUrl: string;\r\n\r\n /** 请求的 scope(空格分隔),如 \"openid profile phone\" */\r\n scopes?: string;\r\n}\r\n\r\n/** 用户信息 */\r\nexport interface SsoUser {\r\n sub: string;\r\n nickname?: string;\r\n avatar?: string;\r\n phone?: string;\r\n membership_level?: string;\r\n total_points?: number;\r\n}\r\n\r\n/** Token 响应 */\r\nexport interface TokenResponse {\r\n access_token: string;\r\n token_type: string;\r\n expires_in: number;\r\n refresh_token: string;\r\n id_token?: string;\r\n}\r\n\r\n/** OIDC Discovery 文档 */\r\nexport interface OidcDiscovery {\r\n issuer: string;\r\n authorization_endpoint: string;\r\n token_endpoint: string;\r\n userinfo_endpoint: string;\r\n jwks_uri: string;\r\n introspection_endpoint: string;\r\n revocation_endpoint?: string;\r\n end_session_endpoint?: string;\r\n scopes_supported: string[];\r\n response_types_supported: string[];\r\n grant_types_supported: string[];\r\n code_challenge_methods_supported: string[];\r\n}\r\n\r\n// ============================================\r\n// SsoClient\r\n// ============================================\r\n\r\nexport class SsoClient {\r\n public readonly config: SsoClientConfig;\r\n private _discovery: OidcDiscovery | null = null;\r\n private _discoveryFetchedAt: number = 0;\r\n private _refreshLock: Promise<TokenData> | null = null;\r\n\r\n /** Discovery 文档缓存 TTL(5 分钟) */\r\n private static readonly DISCOVERY_TTL_MS = 5 * 60 * 1000;\r\n\r\n /** Discovery fetch 超时(10 秒) */\r\n private static readonly DISCOVERY_TIMEOUT_MS = 10000;\r\n\r\n constructor(config: SsoClientConfig) {\r\n if (!config.clientId) throw new SsoError(\"invalid_config\", \"clientId 不能为空\");\r\n if (!config.redirectUri) throw new SsoError(\"invalid_config\", \"redirectUri 不能为空\");\r\n if (!config.ssoBaseUrl) throw new SsoError(\"invalid_config\", \"ssoBaseUrl 不能为空\");\r\n\r\n // 规范化 ssoBaseUrl:移除末尾斜杠\r\n const base = config.ssoBaseUrl.replace(/\\/+$/, \"\");\r\n this.config = { ...config, ssoBaseUrl: base };\r\n }\r\n\r\n // ============================================\r\n // 内部方法\r\n // ============================================\r\n\r\n /**\r\n * 获取 OIDC Discovery 文档(带缓存 + 超时)\r\n *\r\n * 缓存 5 分钟,超时 10 秒。\r\n * 失败时返回 null(上层调用方回退到硬编码默认端点)。\r\n */\r\n private async _getDiscovery(): Promise<OidcDiscovery | null> {\r\n const now = Date.now();\r\n\r\n // 命中缓存且未过期\r\n if (\r\n this._discovery &&\r\n now - this._discoveryFetchedAt < SsoClient.DISCOVERY_TTL_MS\r\n ) {\r\n return this._discovery;\r\n }\r\n\r\n const url = `${this.config.ssoBaseUrl}/api/oauth/.well-known/openid-configuration`;\r\n\r\n try {\r\n const controller = new AbortController();\r\n const timeoutId = setTimeout(\r\n () => controller.abort(),\r\n SsoClient.DISCOVERY_TIMEOUT_MS\r\n );\r\n\r\n const res = await fetch(url, { signal: controller.signal });\r\n clearTimeout(timeoutId);\r\n\r\n if (!res.ok) {\r\n console.warn(\r\n `[SSO SDK] OIDC Discovery 请求失败: HTTP ${res.status}, 回退到默认端点`\r\n );\r\n return null;\r\n }\r\n\r\n this._discovery = (await res.json()) as OidcDiscovery;\r\n this._discoveryFetchedAt = now;\r\n return this._discovery;\r\n } catch (err) {\r\n console.warn(\r\n `[SSO SDK] OIDC Discovery 请求异常: ${err instanceof Error ? err.message : String(err)}, 回退到默认端点`\r\n );\r\n // 不清空已有缓存(即使过期也优于无数据)\r\n return this._discovery || null;\r\n }\r\n }\r\n\r\n /** 获取 authorize 端点 URL(优先 Discovery,回退默认) */\r\n private async _getAuthorizeEndpoint(): Promise<string> {\r\n const d = await this._getDiscovery();\r\n if (d) return d.authorization_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/authorize`;\r\n }\r\n\r\n /** 获取 token 端点 URL(优先 Discovery,回退默认) */\r\n private async _getTokenEndpoint(): Promise<string> {\r\n const d = await this._getDiscovery();\r\n if (d) return d.token_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/token`;\r\n }\r\n\r\n /** 获取 userinfo 端点 URL(优先 Discovery,回退默认) */\r\n private async _getUserinfoEndpoint(): Promise<string> {\r\n const d = await this._getDiscovery();\r\n if (d) return d.userinfo_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/userinfo`;\r\n }\r\n\r\n // ============================================\r\n // 公共 API\r\n // ============================================\r\n\r\n /**\r\n * 发起 SSO 登录\r\n *\r\n * 生成 PKCE code_verifier/code_challenge 和 state 参数,\r\n * 构建 authorize URL,通过 302 跳转到 SSO 登录页。\r\n *\r\n * @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage)\r\n */\r\n async login(returnUrl?: string): Promise<void> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n\r\n // 保存 PKCE verifier 和 state 到 sessionStorage\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n\r\n if (returnUrl) {\r\n saveReturnUrl(returnUrl);\r\n }\r\n\r\n // 构建 authorize URL\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n\r\n // 跳转\r\n window.location.href = `${authorizeEndpoint}?${params.toString()}`;\r\n }\r\n\r\n /**\r\n * 构建登录 URL(不跳转,返回 URL 字符串)\r\n *\r\n * 适用于需要手动处理跳转的场景。\r\n */\r\n async getLoginUrl(returnUrl?: string): Promise<string> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n // 统一使用全局 returnUrl key,与 login() / CallbackPage 保持一致\r\n if (returnUrl) saveReturnUrl(returnUrl);\r\n\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n\r\n return `${authorizeEndpoint}?${params.toString()}`;\r\n }\r\n\r\n /**\r\n * 处理 OAuth 回调\r\n *\r\n * 解析回调 URL,校验 state 参数,用授权码交换 token。\r\n * 成功后 token 自动保存到 sessionStorage。\r\n *\r\n * @param callbackUrl - 完整的回调 URL(window.location.href)\r\n * @returns TokenData 或 null\r\n */\r\n async handleCallback(\r\n callbackUrl: string\r\n ): Promise<TokenData> {\r\n const url = new URL(callbackUrl);\r\n const params = url.searchParams;\r\n\r\n // 检查错误\r\n const error = params.get(\"error\");\r\n if (error) {\r\n const desc = params.get(\"error_description\") || error;\r\n throw new SsoError(\"token_request_failed\", `授权失败: ${desc}`);\r\n }\r\n\r\n const code = params.get(\"code\");\r\n const returnedState = params.get(\"state\");\r\n\r\n if (!code) {\r\n throw new SsoError(\"token_request_failed\", \"回调 URL 中缺少 authorization code\");\r\n }\r\n\r\n // 校验 state 参数(CSRF 防护)\r\n const savedState = getOAuthState(this.config.clientId);\r\n if (!savedState || savedState !== returnedState) {\r\n removeOAuthState(this.config.clientId);\r\n throw new SsoError(\r\n \"state_mismatch\",\r\n \"State 参数不匹配,可能存在 CSRF 攻击\"\r\n );\r\n }\r\n removeOAuthState(this.config.clientId);\r\n\r\n // 获取 code_verifier\r\n const verifier = getPkceVerifier(this.config.clientId);\r\n if (!verifier) {\r\n throw new SsoError(\r\n \"pkce_required\",\r\n \"code_verifier 不存在(可能已过期或来自其他标签页)\"\r\n );\r\n }\r\n removePkceVerifier(this.config.clientId);\r\n\r\n // 交换 token\r\n const tokenEndpoint = await this._getTokenEndpoint();\r\n const body = new URLSearchParams();\r\n body.set(\"grant_type\", \"authorization_code\");\r\n body.set(\"code\", code);\r\n body.set(\"code_verifier\", verifier);\r\n body.set(\"client_id\", this.config.clientId);\r\n body.set(\"redirect_uri\", this.config.redirectUri);\r\n if (this.config.clientSecret) {\r\n body.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n\r\n let res: Response;\r\n try {\r\n res = await fetch(tokenEndpoint, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: body.toString(),\r\n });\r\n } catch (err) {\r\n throw new SsoError(\"network_error\", \"网络请求失败\", err);\r\n }\r\n\r\n if (!res.ok) {\r\n let errData: Record<string, unknown> = {};\r\n try { errData = await res.json(); } catch { /* ignore */ }\r\n throw new SsoError(\r\n \"token_request_failed\",\r\n (errData.error_description as string) || `Token 请求失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n const data: TokenResponse = await res.json();\r\n\r\n // OIDC:验证 ID Token 签名、基本声明及 at_hash\r\n if (data.id_token) {\r\n try {\r\n await validateIdToken(\r\n data.id_token,\r\n this.config.ssoBaseUrl,\r\n this.config.clientId,\r\n data.access_token\r\n );\r\n } catch (err) {\r\n // 验证失败:不保存任何 token,防止伪造 ID Token\r\n removeTokenData(this.config.clientId);\r\n throw err;\r\n }\r\n }\r\n\r\n const now = Date.now();\r\n const tokenData: TokenData = {\r\n access_token: data.access_token,\r\n token_type: data.token_type,\r\n expires_in: data.expires_in,\r\n refresh_token: data.refresh_token,\r\n id_token: data.id_token,\r\n issued_at: now,\r\n expires_at: now + data.expires_in * 1000,\r\n };\r\n\r\n saveTokenData(tokenData, this.config.clientId);\r\n return tokenData;\r\n }\r\n\r\n /**\r\n * 刷新 Access Token\r\n *\r\n * 使用 refresh_token 换取新的 access_token。\r\n * 采用互斥锁防止并发刷新。\r\n * 支持 Refresh Token 原子轮换。\r\n */\r\n async refreshToken(): Promise<TokenData> {\r\n // 互斥锁:避免并发刷新\r\n if (this._refreshLock) return this._refreshLock;\r\n\r\n this._refreshLock = this._doRefreshToken();\r\n try {\r\n return await this._refreshLock;\r\n } finally {\r\n this._refreshLock = null;\r\n }\r\n }\r\n\r\n private async _doRefreshToken(): Promise<TokenData> {\r\n const current = getTokenData(this.config.clientId);\r\n if (!current?.refresh_token) {\r\n throw new SsoError(\"no_refresh_token\", \"没有可用的 refresh_token\");\r\n }\r\n\r\n const tokenEndpoint = await this._getTokenEndpoint();\r\n const body = new URLSearchParams();\r\n body.set(\"grant_type\", \"refresh_token\");\r\n body.set(\"refresh_token\", current.refresh_token);\r\n body.set(\"client_id\", this.config.clientId);\r\n if (this.config.clientSecret) {\r\n body.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n\r\n let res: Response;\r\n try {\r\n res = await fetch(tokenEndpoint, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: body.toString(),\r\n });\r\n } catch (err) {\r\n throw new SsoError(\"network_error\", \"刷新 Token 网络请求失败\", err);\r\n }\r\n\r\n if (!res.ok) {\r\n let errData: Record<string, unknown> = {};\r\n try { errData = await res.json(); } catch { /* ignore */ }\r\n const errorCode = (errData.error as string) || \"\";\r\n // invalid_grant 表示 refresh_token 已被撤销或过期,应清除本地登录态\r\n if (errorCode === \"invalid_grant\" || res.status === 401) {\r\n removeTokenData(this.config.clientId);\r\n }\r\n throw new SsoError(\r\n errorCode === \"invalid_grant\" ? \"session_expired\" : \"token_request_failed\",\r\n (errData.error_description as string) || `刷新 Token 失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n const data: TokenResponse = await res.json();\r\n const now = Date.now();\r\n const tokenData: TokenData = {\r\n access_token: data.access_token,\r\n token_type: data.token_type,\r\n expires_in: data.expires_in,\r\n refresh_token: data.refresh_token,\r\n id_token: data.id_token,\r\n issued_at: now,\r\n expires_at: now + data.expires_in * 1000,\r\n };\r\n\r\n saveTokenData(tokenData, this.config.clientId);\r\n return tokenData;\r\n }\r\n\r\n /**\r\n * 获取用户信息\r\n *\r\n * 若 access_token 已过期则自动刷新后再请求。\r\n */\r\n async getUserInfo(): Promise<SsoUser> {\r\n let tokenData = getTokenData(this.config.clientId);\r\n\r\n if (!tokenData) {\r\n throw new SsoError(\"not_authenticated\", \"未登录\");\r\n }\r\n\r\n // 若已过期,先刷新\r\n if (Date.now() >= tokenData.expires_at) {\r\n tokenData = await this.refreshToken();\r\n }\r\n\r\n const userinfoEndpoint = await this._getUserinfoEndpoint();\r\n\r\n let res: Response;\r\n try {\r\n res = await fetch(userinfoEndpoint, {\r\n headers: {\r\n Authorization: `Bearer ${tokenData.access_token}`,\r\n },\r\n });\r\n } catch (err) {\r\n throw new SsoError(\"network_error\", \"获取用户信息网络请求失败\", err);\r\n }\r\n\r\n if (!res.ok) {\r\n if (res.status === 401) {\r\n removeTokenData(this.config.clientId);\r\n throw new SsoError(\"not_authenticated\", \"Token 已失效\");\r\n }\r\n throw new SsoError(\r\n \"userinfo_failed\",\r\n `获取用户信息失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n return (await res.json()) as SsoUser;\r\n }\r\n\r\n /**\r\n * 获取当前 access_token\r\n *\r\n * 若已过期则自动刷新。用于子项目自行发起 API 请求时获取 Bearer token。\r\n * 若无 token 返回 null;若刷新失败则抛出错误(与 getUserInfo 行为一致)。\r\n */\r\n async getAccessToken(): Promise<string | null> {\r\n let tokenData = getTokenData(this.config.clientId);\r\n if (!tokenData) return null;\r\n\r\n if (Date.now() >= tokenData.expires_at) {\r\n tokenData = await this.refreshToken();\r\n }\r\n\r\n return tokenData.access_token;\r\n }\r\n\r\n /**\r\n * 检查是否已认证(不发起网络请求)\r\n *\r\n * 仅检查本地是否存在未过期的 access_token。\r\n */\r\n isAuthenticated(): boolean {\r\n const tokenData = getTokenData(this.config.clientId);\r\n if (!tokenData) return false;\r\n return Date.now() < tokenData.expires_at;\r\n }\r\n\r\n /**\r\n * 登出\r\n *\r\n * 清除本地所有 token 和临时数据,并尝试撤销服务端 refresh_token。\r\n * @param redirectToSso - 是否重定向到 SSO 登出页(默认 false)\r\n */\r\n async logout(redirectToSso: boolean = false): Promise<void> {\r\n // 获取 refresh_token 用于服务端撤销(需在 clearAllSsoData 之前)\r\n const tokenData = getTokenData(this.config.clientId);\r\n const refreshToken = tokenData?.refresh_token;\r\n const idTokenHint = tokenData?.id_token;\r\n\r\n clearAllSsoData(this.config.clientId);\r\n\r\n // 尝试调用服务端 token revocation 端点(best-effort)\r\n // 仅 Confidential Client 携带 client_secret;Public Client 不传 secret。\r\n if (refreshToken && this.config.clientId) {\r\n try {\r\n const discovery = await this._getDiscovery();\r\n const revokeUrl =\r\n discovery?.revocation_endpoint ||\r\n `${this.config.ssoBaseUrl}/api/oauth/revoke`;\r\n const revokeBody = new URLSearchParams({\r\n token: refreshToken,\r\n token_type_hint: \"refresh_token\",\r\n client_id: this.config.clientId,\r\n });\r\n if (this.config.clientSecret) {\r\n revokeBody.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n await fetch(revokeUrl, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: revokeBody.toString(),\r\n });\r\n } catch {\r\n // 撤销失败不影响本地登出(best-effort)\r\n }\r\n }\r\n\r\n if (redirectToSso) {\r\n // OIDC RP-Initiated Logout:携带 client_id / post_logout_redirect_uri / id_token_hint / state\r\n const discovery = await this._getDiscovery();\r\n const endSessionEndpoint =\r\n discovery?.end_session_endpoint || `${this.config.ssoBaseUrl}/logout`;\r\n const logoutUrl = new URL(endSessionEndpoint);\r\n logoutUrl.searchParams.set(\"client_id\", this.config.clientId);\r\n if (this.config.redirectUri) {\r\n logoutUrl.searchParams.set(\r\n \"post_logout_redirect_uri\",\r\n this.config.redirectUri\r\n );\r\n }\r\n if (idTokenHint) {\r\n logoutUrl.searchParams.set(\"id_token_hint\", idTokenHint);\r\n }\r\n const state = generateState();\r\n logoutUrl.searchParams.set(\"state\", state);\r\n window.location.href = logoutUrl.toString();\r\n }\r\n }\r\n\r\n /**\r\n * 获取 OIDC Discovery 文档\r\n *\r\n * 用于调试和获取 SSO 中心完整配置。\r\n * 可能返回 null(当 Discovery 端点不可达且无缓存时)。\r\n */\r\n async getDiscovery(): Promise<OidcDiscovery | null> {\r\n return this._getDiscovery();\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/errors.ts","../src/core/pkce.ts","../src/core/storage.ts","../src/core/security.ts","../src/core/id-token.ts","../src/core/SsoClient.ts"],"sourcesContent":["/**\r\n * SSO SDK 错误类型\r\n */\r\n\r\n/** SSO 错误码 */\r\nexport type SsoErrorCode =\r\n | \"invalid_config\"\r\n | \"state_mismatch\"\r\n | \"pkce_required\"\r\n | \"token_request_failed\"\r\n | \"session_expired\"\r\n | \"no_refresh_token\"\r\n | \"userinfo_failed\"\r\n | \"not_authenticated\"\r\n | \"authorization_code_expired\"\r\n | \"authorization_code_used\"\r\n | \"client_disabled\"\r\n | \"user_denied_authorization\"\r\n | \"account_disabled\"\r\n | \"sso_server_error\"\r\n | \"rate_limited\"\r\n | \"network_error\"\r\n | \"popup_blocked\"\r\n | \"popup_closed\"\r\n | \"id_token_invalid\"\r\n | \"id_token_unsupported_alg\"\r\n | \"id_token_hs256_unsupported\"\r\n | \"id_token_missing_secret\"\r\n | \"id_token_invalid_signature\"\r\n | \"id_token_issuer_mismatch\"\r\n | \"id_token_audience_mismatch\"\r\n | \"id_token_expired\"\r\n | \"id_token_missing_sub\"\r\n | \"id_token_at_hash_mismatch\";\r\n\r\n/**\r\n * 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode\r\n *\r\n * @param oauthError 服务端返回的 error 字段\r\n * @param context 调用上下文:\"token_exchange\"(授权码换 token)或 \"refresh\"(刷新)。\r\n * invalid_grant 在换 token 场景多为授权码过期/已用,在刷新场景多为会话失效,\r\n * 需按上下文细化映射。\r\n */\r\nexport function mapOAuthErrorToSsoCode(\r\n oauthError: string,\r\n context: \"token_exchange\" | \"refresh\" = \"token_exchange\"\r\n): SsoErrorCode {\r\n switch (oauthError) {\r\n case \"invalid_grant\":\r\n // 刷新场景的 invalid_grant 表示 refresh_token 已撤销/过期(调用方另会清除本地登录态)\r\n return context === \"refresh\" ? \"session_expired\" : \"authorization_code_expired\";\r\n case \"invalid_client\":\r\n return \"client_disabled\";\r\n case \"access_denied\":\r\n return \"user_denied_authorization\";\r\n case \"server_error\":\r\n return \"sso_server_error\";\r\n case \"rate_limited\":\r\n // 限流是独立的可重试错误,不应误报为 network_error\r\n return \"rate_limited\";\r\n case \"invalid_scope\":\r\n case \"invalid_request\":\r\n case \"unauthorized_client\":\r\n case \"unsupported_grant_type\":\r\n default:\r\n return \"token_request_failed\";\r\n }\r\n}\r\n\r\n/**\r\n * SSO SDK 自定义错误\r\n */\r\nexport class SsoError extends Error {\r\n public readonly code: SsoErrorCode;\r\n public readonly description: string;\r\n public readonly cause?: unknown;\r\n\r\n constructor(code: SsoErrorCode, description: string, cause?: unknown) {\r\n super(`[SSO SDK] ${code}: ${description}`);\r\n this.name = \"SsoError\";\r\n this.code = code;\r\n this.description = description;\r\n this.cause = cause;\r\n }\r\n}\r\n\r\n/**\r\n * OAuth 2.0 协议层错误(RFC 6749 §5.2)\r\n *\r\n * 用于表示服务端原样返回的 OAuth 错误(error / error_description / error_uri),\r\n * 与 SDK 语义的 SsoError 区分。\r\n */\r\nexport class OAuthError extends Error {\r\n public readonly code: string;\r\n public readonly description: string;\r\n public readonly uri?: string;\r\n\r\n constructor(code: string, description: string, uri?: string) {\r\n super(`[OAuth] ${code}: ${description}`);\r\n this.name = \"OAuthError\";\r\n this.code = code;\r\n this.description = description;\r\n this.uri = uri;\r\n }\r\n}\r\n","/**\r\n * PKCE (Proof Key for Code Exchange) 工具函数\r\n *\r\n * 实现 RFC 7636 规范:\r\n * - code_verifier: 43-128 字符的随机字符串\r\n * - code_challenge: SHA-256(code_verifier) 的 base64url 编码\r\n *\r\n * 仅支持 S256 模式,不提供 plain 模式。\r\n */\r\n\r\nconst VALID_CHARS =\r\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\r\n\r\n/**\r\n * 生成密码学安全随机字符串\r\n *\r\n * 使用拒绝采样法(rejection sampling)避免取模非均匀分布。\r\n * VALID_CHARS 有 66 个字符,Uint8(0-255) 取模会偏向前 58 个字符。\r\n * 丢弃 >=198 的字节值使分布完全均匀(198 = floor(256/66) * 66)。\r\n */\r\nfunction generateRandomString(length: number): string {\r\n const mask = 198; // 66 * 3,最大均匀取值\r\n let result = \"\";\r\n\r\n while (result.length < length) {\r\n // 一次生成分块避免过小 array 的循环开销\r\n const chunkSize = Math.min(length * 2, 256);\r\n const array = new Uint8Array(chunkSize);\r\n crypto.getRandomValues(array);\r\n\r\n for (let i = 0; i < chunkSize && result.length < length; i++) {\r\n if (array[i] >= mask) continue; // 拒绝采样:丢弃 >=198 的字节\r\n result += VALID_CHARS[array[i] % VALID_CHARS.length];\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * 生成 code_verifier\r\n *\r\n * 生成 43-128 字符之间的安全随机字符串,\r\n * 使用 `crypto.getRandomValues` 保证密码学安全性。\r\n *\r\n * @param length - 长度(默认 64),范围 43-128\r\n */\r\nexport function generateCodeVerifier(length: number = 64): string {\r\n if (length < 43 || length > 128) {\r\n throw new Error(\"code_verifier length must be between 43 and 128\");\r\n }\r\n return generateRandomString(length);\r\n}\r\n\r\n/**\r\n * 计算 code_challenge (S256)\r\n *\r\n * 对 code_verifier 进行 SHA-256 哈希,然后进行 base64url 编码。\r\n *\r\n * @param codeVerifier - PKCE code_verifier\r\n */\r\nexport async function generateCodeChallenge(\r\n codeVerifier: string\r\n): Promise<string> {\r\n const encoder = new TextEncoder();\r\n const data = encoder.encode(codeVerifier);\r\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\r\n\r\n // 转为 base64url(无 padding)\r\n const bytes = new Uint8Array(hash);\r\n let binary = \"\";\r\n for (let i = 0; i < bytes.byteLength; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\r\n}\r\n\r\n/**\r\n * 生成 state 参数\r\n *\r\n * 32 字节 hex 随机字符串,用于 CSRF 防护。\r\n */\r\nexport function generateState(): string {\r\n const array = new Uint8Array(32);\r\n crypto.getRandomValues(array);\r\n return Array.from(array, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\r\n}\r\n","/**\n * Token 存储抽象层\n *\n * 分两类存储:\n * - Token 数据:默认使用内存存储,Public Client 浏览器中 refresh_token 不落盘,\n * XSS 无法窃取长期凭证。对需要多 Tab 共享 token 或 BFF/Confidential Client 场景,\n * 可通过 setTokenStorage() 注入 localStorage 实现(如 createSecureStorage({ persist: true }))。\n * - 临时数据(PKCE verifier / state / returnUrl / popup nonce):必须跨整页重定向存活\n * (login() 会 302 跳转到 SSO 中心再回来),因此默认写入 sessionStorage;\n * SSR 等无 sessionStorage 环境自动降级为内存 Map。\n *\n * 多 client 隔离:\n * - token / state / return_url 均支持按 clientId 隔离 key\n * - 不传 clientId 时使用全局 key,保持向后兼容\n */\n\n/** Token 数据 */\nexport interface TokenData {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token: string;\n id_token?: string;\n\n /** Token 签发时间(epoch ms),用于计算过期 */\n issued_at: number;\n\n /** 访问令牌实际过期时间(epoch ms)= issued_at + expires_in * 1000 */\n expires_at: number;\n}\n\n/** 存储接口 */\nexport interface TokenStorage {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\n/** 默认 localStorage/sessionStorage key 前缀 */\nconst STORAGE_PREFIX = \"nihplod_sso_\";\n\n/** 常量 key 名称 */\nconst TOKEN_KEY = \"token\";\nconst VERIFIER_KEY_PREFIX = \"pkce_verifier_\";\nconst STATE_KEY = \"oauth_state\";\nconst RETURN_URL_KEY = \"return_url\";\n\nfunction buildKey(base: string, clientId?: string): string {\n return clientId ? `${base}:${clientId}` : base;\n}\n\n/**\n * 内存存储适配器(Public Client 浏览器默认)\n *\n * 安全优势:\n * - refresh_token 不落盘,XSS 无法窃取长期凭证。\n * - 页面刷新后用户需重新授权,符合纯前端 Public Client 的安全模型。\n *\n * 代价:多 Tab 间不会自动同步 token;SDK 内部已用锁机制避免并发刷新。\n */\nfunction createMemoryStorageAdapter(): TokenStorage {\n const store = new Map<string, string>();\n return {\n get(key: string) {\n return store.get(key) ?? null;\n },\n set(key: string, value: string) {\n store.set(key, value);\n },\n remove(key: string) {\n store.delete(key);\n },\n };\n}\n\nconst memoryStorageAdapter = createMemoryStorageAdapter();\n\n/**\n * 浏览器 localStorage 实现\n *\n * 适用于:\n * - Next.js BFF / Confidential Client(refresh_token 不直接暴露给浏览器)。\n * - 需要多 Tab 共享 token 的场景。\n *\n * 注意:Public Client 在浏览器中直接存储 refresh_token 会增加 XSS 风险,\n * 建议改用内存存储或 Service Worker 封装。\n */\nconst localStorageAdapter: TokenStorage = {\n get(key: string) {\n if (typeof localStorage === \"undefined\") return null;\n return localStorage.getItem(STORAGE_PREFIX + key);\n },\n set(key: string, value: string) {\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.setItem(STORAGE_PREFIX + key, value);\n } catch (err) {\n // QuotaExceededError 等写入失败不应中断登录流程\n console.warn(\n `[SSO SDK] localStorage 写入失败(${err instanceof Error ? err.name : String(err)}),数据未持久化`\n );\n }\n },\n remove(key: string) {\n if (typeof localStorage === \"undefined\") return;\n localStorage.removeItem(STORAGE_PREFIX + key);\n },\n};\n\n/**\n * 浏览器 sessionStorage 实现(临时数据专用)\n *\n * PKCE verifier / state / returnUrl / popup nonce 必须跨整页重定向存活\n * (login() 跳转到 SSO 中心后回调页面仍需读取),不能放模块级内存;\n * sessionStorage 在整页跳转后保留,且随标签页关闭自动清除,恰好匹配其生命周期。\n * SSR / 隐私模式写入失败时降级为内存 Map,保证不抛异常。\n */\nfunction createSessionStorageAdapter(): TokenStorage {\n const fallback = new Map<string, string>();\n return {\n get(key: string) {\n if (typeof sessionStorage !== \"undefined\") {\n return sessionStorage.getItem(STORAGE_PREFIX + key) ?? fallback.get(key) ?? null;\n }\n return fallback.get(key) ?? null;\n },\n set(key: string, value: string) {\n if (typeof sessionStorage === \"undefined\") {\n fallback.set(key, value);\n return;\n }\n try {\n sessionStorage.setItem(STORAGE_PREFIX + key, value);\n } catch {\n // 隐私模式等场景写入失败时降级到内存\n fallback.set(key, value);\n }\n },\n remove(key: string) {\n fallback.delete(key);\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.removeItem(STORAGE_PREFIX + key);\n },\n };\n}\n\n/** 临时数据存储(跨整页重定向存活),独立于 token 存储,不受 setTokenStorage() 影响 */\nconst _transient: TokenStorage = createSessionStorageAdapter();\n\n/**\n * 创建存储实现\n *\n * @param options.persist 是否持久化到 localStorage。默认 false(内存存储)。\n * ⚠️ 安全警告:persist=true 会将 refresh_token 明文写入 localStorage,\n * 任何 XSS 均可在不被检测的情况下读取。仅在 BFF/Confidential Client\n * 且 refresh_token 不直接暴露给浏览器的场景下使用。\n * - Confidential/BFF 子项目可设为 true。\n * - Public Client 在浏览器中应保持 false(默认)。\n */\nexport function createSecureStorage(options: { persist?: boolean } = {}): TokenStorage {\n return options.persist ? localStorageAdapter : memoryStorageAdapter;\n}\n\nlet _storage: TokenStorage = memoryStorageAdapter;\n\n/**\n * 设置自定义存储实现\n */\nexport function setTokenStorage(storage: TokenStorage): void {\n _storage = storage;\n}\n\n/**\n * 获取当前存储实现\n */\nexport function getTokenStorage(): TokenStorage {\n return _storage;\n}\n\n// ============================================\n// Token 存取\n// ============================================\n\nexport function saveTokenData(data: TokenData, clientId?: string): void {\n _storage.set(buildKey(TOKEN_KEY, clientId), JSON.stringify(data));\n}\n\nexport function getTokenData(clientId?: string): TokenData | null {\n const raw = _storage.get(buildKey(TOKEN_KEY, clientId));\n if (!raw) return null;\n try {\n // 过期不物理删除:refresh_token 通常比 access_token 长寿,\n // 删除会同时丢弃 refresh_token 使刷新路径失效。\n // 是否过期由调用方依据 expires_at 判断(isAuthenticated / getUserInfo 等)。\n return JSON.parse(raw) as TokenData;\n } catch {\n return null;\n }\n}\n\nexport function removeTokenData(clientId?: string): void {\n _storage.remove(buildKey(TOKEN_KEY, clientId));\n}\n\n// ============================================\n// PKCE 临时数据存取(transient:sessionStorage,跨整页重定向存活)\n// ============================================\n\nexport function savePkceVerifier(clientId: string, verifier: string): void {\n _transient.set(VERIFIER_KEY_PREFIX + clientId, verifier);\n}\n\nexport function getPkceVerifier(clientId: string): string | null {\n return _transient.get(VERIFIER_KEY_PREFIX + clientId);\n}\n\nexport function removePkceVerifier(clientId: string): void {\n _transient.remove(VERIFIER_KEY_PREFIX + clientId);\n}\n\n// ============================================\n// State 参数存取(transient)\n// ============================================\n\nexport function saveOAuthState(state: string, clientId?: string): void {\n _transient.set(buildKey(STATE_KEY, clientId), state);\n}\n\nexport function getOAuthState(clientId?: string): string | null {\n return _transient.get(buildKey(STATE_KEY, clientId));\n}\n\nexport function removeOAuthState(clientId?: string): void {\n _transient.remove(buildKey(STATE_KEY, clientId));\n}\n\n// ============================================\n// 返回 URL 存取(transient)\n// ============================================\n\nexport function saveReturnUrl(url: string, clientId?: string): void {\n _transient.set(buildKey(RETURN_URL_KEY, clientId), url);\n}\n\nexport function getReturnUrl(clientId?: string): string | null {\n return _transient.get(buildKey(RETURN_URL_KEY, clientId));\n}\n\nexport function removeReturnUrl(clientId?: string): void {\n _transient.remove(buildKey(RETURN_URL_KEY, clientId));\n}\n\n// ============================================\n// 清理所有 SSO 数据\n// ============================================\n\nexport function clearAllSsoData(clientId?: string): void {\n if (clientId) {\n removeTokenData(clientId);\n removeOAuthState(clientId);\n removeReturnUrl(clientId);\n removePkceVerifier(clientId);\n // popup nonce 复用 verifier key 空间(`${clientId}_popup_nonce`)\n removePkceVerifier(`${clientId}_popup_nonce`);\n return;\n }\n\n removeTokenData();\n removeOAuthState();\n removeReturnUrl();\n\n // 清理所有 PKCE verifier:当前版本存于 sessionStorage,同时清 localStorage 中可能的旧版本残留\n const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;\n const stores = [\n typeof sessionStorage !== \"undefined\" ? sessionStorage : null,\n typeof localStorage !== \"undefined\" ? localStorage : null,\n ];\n for (const store of stores) {\n if (!store) continue;\n const keys: string[] = [];\n for (let i = 0; i < store.length; i++) {\n const key = store.key(i);\n if (key?.startsWith(prefix)) keys.push(key);\n }\n for (const key of keys) {\n _transient.remove(key.slice(STORAGE_PREFIX.length));\n store.removeItem(key);\n }\n }\n}\n\n/**\n * 清理指定 clientId 列表的 PKCE verifier\n *\n * 适用于非浏览器环境或已知 clientId 场景。\n */\nexport function clearVerifiersForClients(clientIds: string[]): void {\n for (const clientId of clientIds) {\n _transient.remove(VERIFIER_KEY_PREFIX + clientId);\n }\n}\n","/**\n * 安全相关小工具:returnUrl 开放重定向校验、常量时间字符串比较\n */\n\n/**\n * 校验 returnUrl 是否可信(防开放重定向)。\n * 仅允许:相对路径(且不以 // 开头)或与 currentOrigin 完全同源的绝对 URL。\n */\nexport function isTrustedReturnUrl(url: string, currentOrigin: string): boolean {\n if (!url) return false;\n if (url.startsWith(\"/\") && !url.startsWith(\"//\")) return true;\n try {\n return new URL(url).origin === currentOrigin;\n } catch {\n return false;\n }\n}\n\n/**\n * 常量时间字符串比较(避免 state / at_hash / nonce 等机密值的时序侧信道)。\n * 长度不同也执行完整循环,不提前返回。\n */\nexport function timingSafeEqualString(a: string, b: string): boolean {\n const ba = new TextEncoder().encode(a);\n const bb = new TextEncoder().encode(b);\n if (ba.length === 0 || bb.length === 0) return ba.length === bb.length;\n const len = Math.max(ba.length, bb.length);\n let diff = ba.length ^ bb.length;\n for (let i = 0; i < len; i++) {\n diff |= ba[i % ba.length] ^ bb[i % bb.length];\n }\n return diff === 0;\n}\n","/**\n * ID Token 验证工具(浏览器 / Edge Runtime 通用)\n *\n * 提取 SsoClient 与 Next.js callback 中重复的 ID Token 校验逻辑,\n * 包括 base64url 解码、JWKS 获取、RS256 签名验证、at_hash 计算等。\n */\n\nimport { SsoError } from \"./errors\";\nimport { timingSafeEqualString } from \"./security\";\n\n// 运行时 crypto.subtle 检测:现代浏览器 / Edge Runtime / Node 20+ 均提供全局 crypto\nconst _crypto: Pick<Crypto, \"subtle\"> | null =\n typeof crypto !== \"undefined\" && (crypto as Crypto).subtle ? (crypto as Crypto) : null;\n\nfunction getCrypto(): Crypto | null {\n return (_crypto as Crypto | null) ?? null;\n}\n\nexport interface JwksKey {\n kty: string;\n kid?: string;\n alg?: string;\n use?: string;\n n?: string;\n e?: string;\n x5c?: string[];\n}\n\nexport interface Jwks {\n keys: JwksKey[];\n}\n\nexport interface ValidateIdTokenResult {\n sub: string;\n}\n\nexport interface ValidateIdTokenOptions {\n /**\n * 发现 SSO 中心已启用 RS256 时,是否拒绝 HS256 ID Token。\n * 默认 true:只要 JWKS 中存在 RS256 签名密钥,就拒绝 HS256(安全推荐)。\n */\n rejectHs256WhenRs256Available?: boolean;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\n/** 解码 JWT payload(不验证签名) */\nexport function decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n const json = new TextDecoder().decode(base64UrlDecode(parts[1]));\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/** 解码 JWT header */\nexport function decodeJwtHeader(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n const json = new TextDecoder().decode(base64UrlDecode(parts[0]));\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/** 标准化 issuer/base URL,去除末尾斜杠 */\nfunction normalizeIssuer(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\n/** OIDC Discovery 文档(仅取本模块关心的字段) */\ninterface OidcDiscoveryDoc {\n issuer?: string;\n jwks_uri?: string;\n}\n\nlet cachedJwks: { baseUrl: string; jwks: Jwks; fetchedAt: number } | null = null;\nlet cachedDiscovery: { baseUrl: string; doc: OidcDiscoveryDoc | null; fetchedAt: number } | null = null;\nconst JWKS_CACHE_TTL_MS = 5 * 60 * 1000;\n\n/** 拉取 OIDC Discovery 文档(带内存缓存;失败返回 null 不抛错) */\nasync function fetchDiscoveryDoc(baseUrl: string): Promise<OidcDiscoveryDoc | null> {\n const now = Date.now();\n if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {\n return cachedDiscovery.doc;\n }\n\n try {\n const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`);\n const doc = res.ok ? ((await res.json()) as OidcDiscoveryDoc) : null;\n cachedDiscovery = { baseUrl, doc, fetchedAt: now };\n return doc;\n } catch {\n cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };\n return null;\n }\n}\n\nexport interface FetchJwksOptions {\n /**\n * 绕过内存缓存与浏览器 HTTP 缓存(cache: \"no-cache\")强制重取 JWKS,\n * 并同步刷新模块级缓存。用于密钥轮换后 kid 失配的自愈重试。\n */\n forceRefresh?: boolean;\n}\n\n/** 从 SSO 中心拉取 JWKS(带内存缓存;优先 Discovery 的 jwks_uri,回退硬编码路径) */\nexport async function fetchJwks(baseUrl: string, options: FetchJwksOptions = {}): Promise<Jwks | null> {\n const now = Date.now();\n if (!options.forceRefresh && cachedJwks && cachedJwks.baseUrl === baseUrl && now - cachedJwks.fetchedAt < JWKS_CACHE_TTL_MS) {\n return cachedJwks.jwks;\n }\n\n const discovery = await fetchDiscoveryDoc(baseUrl);\n const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;\n\n try {\n const res = await fetch(jwksUri, options.forceRefresh ? { cache: \"no-cache\" } : undefined);\n if (!res.ok) return null;\n const jwks = (await res.json()) as Jwks;\n cachedJwks = { baseUrl, jwks, fetchedAt: now };\n return jwks;\n } catch {\n return null;\n }\n}\n\n/** 清空 JWKS / Discovery 内存缓存(测试与密钥轮换场景使用) */\nexport function clearIdTokenCaches(): void {\n cachedJwks = null;\n cachedDiscovery = null;\n}\n\n/** 使用 RS256 公钥验证 ID Token 签名 */\nexport async function verifyRs256Signature(token: string, jwk: JwksKey): Promise<boolean> {\n try {\n const [headerB64, payloadB64, signature] = token.split(\".\");\n if (!signature || !jwk.n || !jwk.e) return false;\n\n const c = getCrypto();\n if (!c) return false;\n\n const cryptoKey = await c.subtle.importKey(\n \"jwk\",\n { kty: \"RSA\", n: jwk.n, e: jwk.e, alg: \"RS256\", ext: false },\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\n false,\n [\"verify\"]\n );\n\n const signatureBytes = base64UrlDecode(signature);\n const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);\n return await c.subtle.verify(\n \"RSASSA-PKCS1-v1_5\",\n cryptoKey,\n signatureBytes as unknown as BufferSource,\n data as unknown as BufferSource\n );\n } catch {\n return false;\n }\n}\n\n/** 计算 OIDC at_hash(access_token SHA-256 前 128bit base64url) */\nexport async function computeAtHash(accessToken: string): Promise<string> {\n const c = getCrypto();\n if (!c) return \"\";\n const hash = await c.subtle.digest(\"SHA-256\", new TextEncoder().encode(accessToken));\n const bytes = new Uint8Array(hash);\n const half = bytes.slice(0, bytes.length / 2);\n let binary = \"\";\n for (const b of half) {\n binary += String.fromCharCode(b);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** 验证 ID Token(iss、aud、exp、iat、azp、sub、签名、at_hash) */\nexport async function validateIdToken(\n idToken: string,\n accessToken: string,\n expectedIssuer: string,\n expectedClientId: string,\n options: ValidateIdTokenOptions = {}\n): Promise<ValidateIdTokenResult> {\n const { rejectHs256WhenRs256Available = true } = options;\n\n const header = decodeJwtHeader(idToken);\n if (!header) {\n throw new SsoError(\"id_token_invalid\", \"ID Token 格式错误\");\n }\n\n const alg = header.alg;\n if (typeof alg !== \"string\" || (alg !== \"RS256\" && alg !== \"HS256\")) {\n throw new SsoError(\"id_token_unsupported_alg\", `不支持的 ID Token 签名算法: ${alg}`);\n }\n\n const baseUrl = normalizeIssuer(expectedIssuer);\n // issuer 以 Discovery 文档的 issuer 为准(防伪站伪造 iss 通过校验),回退调用方传入值\n const discovery = await fetchDiscoveryDoc(baseUrl);\n const normalizedIssuer = normalizeIssuer(discovery?.issuer || baseUrl);\n\n // RS256:通过 JWKS 验证签名\n if (alg === \"RS256\") {\n const jwks = await fetchJwks(baseUrl);\n if (!jwks) {\n throw new SsoError(\"id_token_invalid_signature\", \"无法获取 JWKS 验证 ID Token 签名\");\n }\n const kid = typeof header.kid === \"string\" ? header.kid : undefined;\n // 有 kid 时精确匹配;无 kid 时逐个尝试所有 RS256 签名公钥\n const matchCandidates = (set: Jwks) =>\n set.keys.filter(\n (k) =>\n k.kty === \"RSA\" &&\n k.alg === \"RS256\" &&\n k.use === \"sig\" &&\n (kid ? k.kid === kid : true)\n );\n const verifyAny = async (keys: JwksKey[]): Promise<boolean> => {\n for (const key of keys) {\n if (await verifyRs256Signature(idToken, key)) return true;\n }\n return false;\n };\n\n let candidates = matchCandidates(jwks);\n let validSig = candidates.length > 0 ? await verifyAny(candidates) : false;\n\n // 密钥轮换自愈:按 kid 过滤无候选,或所有候选验签失败时,\n // 可能是本地 JWKS 缓存陈旧 —— 强制重取一次 JWKS 后重试,仍失败才抛错\n if (candidates.length === 0 || !validSig) {\n const freshJwks = await fetchJwks(baseUrl, { forceRefresh: true });\n if (freshJwks) {\n candidates = matchCandidates(freshJwks);\n validSig = candidates.length > 0 ? await verifyAny(candidates) : false;\n }\n }\n\n if (candidates.length === 0) {\n throw new SsoError(\"id_token_invalid_signature\", \"JWKS 中未找到匹配的 RS256 公钥\");\n }\n if (!validSig) {\n throw new SsoError(\"id_token_invalid_signature\", \"ID Token 签名验证失败\");\n }\n }\n\n // HS256:对称密钥无法安全分发给 Public Client / BFF。\n // SDK 拒绝 HS256 ID Token:主站需配置 RS256 密钥对以启用安全验证。\n if (alg === \"HS256\") {\n if (rejectHs256WhenRs256Available) {\n const jwks = await fetchJwks(baseUrl);\n const hasRs256 = jwks?.keys?.some((k) => k.alg === \"RS256\" && k.use === \"sig\");\n if (hasRs256) {\n throw new SsoError(\"id_token_unsupported_alg\", \"SSO 中心已配置 RS256,拒绝 HS256 ID Token\");\n }\n }\n throw new SsoError(\n \"id_token_hs256_unsupported\",\n \"ID Token 使用 HS256 对称签名,SDK 无法安全验证。\" +\n \"请联系管理员在 SSO 主站配置 RS256 密钥对(JWT_ID_TOKEN_PRIVATE_KEY / JWT_ID_TOKEN_PUBLIC_KEY)后重新签发。\"\n );\n }\n\n const payload = decodeJwtPayload(idToken);\n if (!payload) {\n throw new SsoError(\"id_token_invalid\", \"ID Token payload 解析失败\");\n }\n\n const tokenIssuer = typeof payload.iss === \"string\" ? normalizeIssuer(payload.iss) : \"\";\n if (tokenIssuer !== normalizedIssuer) {\n throw new SsoError(\"id_token_issuer_mismatch\", \"ID Token issuer 不匹配\");\n }\n\n // aud 校验:单值或数组包含 clientId;多 aud 时按 OIDC Core §3.1.3.7 要求 azp 必须等于 clientId\n const aud = payload.aud;\n const audList = Array.isArray(aud) ? aud : typeof aud === \"string\" ? [aud] : [];\n if (!audList.includes(expectedClientId)) {\n throw new SsoError(\"id_token_audience_mismatch\", \"ID Token audience 不匹配\");\n }\n if (audList.length > 1 && payload.azp !== expectedClientId) {\n throw new SsoError(\"id_token_audience_mismatch\", \"ID Token 多 audience 时 azp 必须为当前 client\");\n }\n\n // exp 必需且不得过期\n if (typeof payload.exp !== \"number\") {\n throw new SsoError(\"id_token_invalid\", \"ID Token 缺少 exp 声明\");\n }\n if (Date.now() >= payload.exp * 1000) {\n throw new SsoError(\"id_token_expired\", \"ID Token 已过期\");\n }\n\n // iat 不得在未来(允许 60s clock skew)\n if (typeof payload.iat === \"number\" && payload.iat * 1000 > Date.now() + 60_000) {\n throw new SsoError(\"id_token_invalid\", \"ID Token iat 在未来,疑似伪造或时钟异常\");\n }\n\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new SsoError(\"id_token_missing_sub\", \"ID Token 缺少 sub\");\n }\n\n // 校验 at_hash:确保 ID Token 与当前 access_token 绑定(常量时间比较)\n if (typeof payload.at_hash === \"string\" && payload.at_hash) {\n const actual = await computeAtHash(accessToken);\n if (!timingSafeEqualString(actual, payload.at_hash)) {\n throw new SsoError(\"id_token_at_hash_mismatch\", \"ID Token at_hash 不匹配\");\n }\n }\n\n return { sub: payload.sub };\n}\n","/**\r\n * SSO Client 核心类\r\n *\r\n * 封装完整的 OAuth 2.0 授权码 + PKCE S256 流程:\r\n * - login(): 发起授权请求\r\n * - handleCallback(): 处理回调,交换 token\r\n * - refreshToken(): 刷新 access token(原子轮换)\r\n * - getUserInfo(): 获取用户信息\r\n * - logout(): 登出\r\n * - isAuthenticated(): 检查认证状态\r\n */\r\n\r\nimport { SsoError, mapOAuthErrorToSsoCode } from \"./errors\";\r\nimport {\r\n generateCodeVerifier,\r\n generateCodeChallenge,\r\n generateState,\r\n} from \"./pkce\";\r\nimport {\r\n saveTokenData,\r\n getTokenData,\r\n removeTokenData,\r\n savePkceVerifier,\r\n getPkceVerifier,\r\n removePkceVerifier,\r\n saveOAuthState,\r\n getOAuthState,\r\n removeOAuthState,\r\n saveReturnUrl,\r\n clearAllSsoData,\r\n type TokenData,\r\n} from \"./storage\";\r\nimport { validateIdToken } from \"./id-token\";\r\nimport { isTrustedReturnUrl, timingSafeEqualString } from \"./security\";\r\n\r\n// ============================================\r\n// 类型定义\r\n// ============================================\r\n\r\n/** SSO 客户端配置 */\r\nexport interface SsoClientConfig {\r\n /** OAuth Client ID(从管理后台获取) */\r\n clientId: string;\r\n\r\n /** OAuth Client Secret(从管理后台获取)\r\n *\r\n * ⚠️ 安全提醒:client_secret 不应暴露在前端代码中。\r\n * 对于浏览器端 SPA(Public Client),应省略此字段;SDK 会使用 PKCE 完成授权,\r\n * 且 token 端点不会发送 client_secret。\r\n * 对于 BFF/Next.js 等 Confidential Client,可传入 client_secret。\r\n */\r\n clientSecret?: string;\r\n\r\n /** 回调 URL(必须与注册的 redirect_uri 完全一致) */\r\n redirectUri: string;\r\n\r\n /** SSO 中心地址,如 \"https://nihplod.cn\" */\r\n ssoBaseUrl: string;\r\n\r\n /** 请求的 scope(空格分隔),如 \"openid profile phone\" */\r\n scopes?: string;\r\n\r\n /** RP-Initiated Logout 返回地址(可选)。不传时回退到 redirectUri */\r\n postLogoutRedirectUri?: string;\r\n}\r\n\r\n/** 用户信息 */\r\nexport interface SsoUser {\r\n sub: string;\r\n nickname?: string;\r\n avatar?: string;\r\n phone?: string;\r\n membership_level?: string;\r\n total_points?: number;\r\n}\r\n\r\n/** Token 响应 */\r\nexport interface TokenResponse {\r\n access_token: string;\r\n token_type: string;\r\n expires_in: number;\r\n refresh_token: string;\r\n id_token?: string;\r\n}\r\n\r\n/** OIDC Discovery 文档 */\r\nexport interface OidcDiscovery {\r\n issuer: string;\r\n authorization_endpoint: string;\r\n token_endpoint: string;\r\n userinfo_endpoint: string;\r\n jwks_uri: string;\r\n introspection_endpoint: string;\r\n revocation_endpoint?: string;\r\n end_session_endpoint?: string;\r\n scopes_supported: string[];\r\n response_types_supported: string[];\r\n grant_types_supported: string[];\r\n code_challenge_methods_supported: string[];\r\n}\r\n\r\n// ============================================\r\n// SsoClient\r\n// ============================================\r\n\r\nexport class SsoClient {\r\n public readonly config: SsoClientConfig;\r\n private _discovery: OidcDiscovery | null = null;\r\n private _discoveryFetchedAt: number = 0;\r\n private _refreshLock: Promise<TokenData> | null = null;\r\n\r\n /** Discovery 文档缓存 TTL(5 分钟) */\r\n private static readonly DISCOVERY_TTL_MS = 5 * 60 * 1000;\r\n\r\n /** Discovery fetch 超时(10 秒) */\r\n private static readonly DISCOVERY_TIMEOUT_MS = 10000;\r\n\r\n constructor(config: SsoClientConfig) {\r\n if (!config.clientId) throw new SsoError(\"invalid_config\", \"clientId 不能为空\");\r\n if (!config.redirectUri) throw new SsoError(\"invalid_config\", \"redirectUri 不能为空\");\r\n if (!config.ssoBaseUrl) throw new SsoError(\"invalid_config\", \"ssoBaseUrl 不能为空\");\r\n\r\n // 规范化 ssoBaseUrl:移除末尾斜杠\r\n const base = config.ssoBaseUrl.replace(/\\/+$/, \"\");\r\n this.config = { ...config, ssoBaseUrl: base };\r\n }\r\n\r\n // ============================================\r\n // 内部方法\r\n // ============================================\r\n\r\n /**\r\n * 获取 OIDC Discovery 文档(带缓存 + 超时)\r\n *\r\n * 缓存 5 分钟,超时 10 秒。\r\n * 失败时返回 null(上层调用方回退到硬编码默认端点)。\r\n */\r\n private async _getDiscovery(): Promise<OidcDiscovery | null> {\r\n const now = Date.now();\r\n\r\n // 命中缓存且未过期\r\n if (\r\n this._discovery &&\r\n now - this._discoveryFetchedAt < SsoClient.DISCOVERY_TTL_MS\r\n ) {\r\n return this._discovery;\r\n }\r\n\r\n const url = `${this.config.ssoBaseUrl}/api/oauth/.well-known/openid-configuration`;\r\n\r\n try {\r\n const controller = new AbortController();\r\n const timeoutId = setTimeout(\r\n () => controller.abort(),\r\n SsoClient.DISCOVERY_TIMEOUT_MS\r\n );\r\n\r\n const res = await fetch(url, { signal: controller.signal });\r\n clearTimeout(timeoutId);\r\n\r\n if (!res.ok) {\r\n console.warn(\r\n `[SSO SDK] OIDC Discovery 请求失败: HTTP ${res.status}, 回退到默认端点`\r\n );\r\n return null;\r\n }\r\n\r\n this._discovery = (await res.json()) as OidcDiscovery;\r\n this._discoveryFetchedAt = now;\r\n return this._discovery;\r\n } catch (err) {\r\n console.warn(\r\n `[SSO SDK] OIDC Discovery 请求异常: ${err instanceof Error ? err.message : String(err)}, 回退到默认端点`\r\n );\r\n // 不清空已有缓存(即使过期也优于无数据)\r\n return this._discovery || null;\r\n }\r\n }\r\n\r\n /** 获取 authorize 端点 URL(优先 Discovery,回退默认) */\r\n private async _getAuthorizeEndpoint(): Promise<string> {\r\n const d = await this._getDiscovery();\r\n if (d) return d.authorization_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/authorize`;\r\n }\r\n\r\n /** 获取 token 端点 URL(优先 Discovery,回退默认) */\r\n private async _getTokenEndpoint(): Promise<string> {\r\n const d = await this._getDiscovery();\r\n if (d) return d.token_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/token`;\r\n }\r\n\r\n /** 获取 userinfo 端点 URL(优先 Discovery,回退默认) */\r\n private async _getUserinfoEndpoint(): Promise<string> {\r\n const d = await this._getDiscovery();\r\n if (d) return d.userinfo_endpoint;\r\n return `${this.config.ssoBaseUrl}/api/oauth/userinfo`;\r\n }\r\n\r\n /**\r\n * 开放重定向防护:仅保存相对路径或与当前页面同源的 returnUrl,\r\n * 其余(如 https://evil.com)忽略并告警,防止回调后跳转到钓鱼站点。\r\n */\r\n private _saveReturnUrlIfTrusted(returnUrl: string): void {\r\n if (isTrustedReturnUrl(returnUrl, window.location.origin)) {\r\n saveReturnUrl(returnUrl);\r\n } else {\r\n console.warn(`[SSO SDK] returnUrl 未通过同源校验,已忽略: ${returnUrl}`);\r\n }\r\n }\r\n\r\n // ============================================\r\n // 公共 API\r\n // ============================================\r\n\r\n /**\r\n * 发起 SSO 登录\r\n *\r\n * 生成 PKCE code_verifier/code_challenge 和 state 参数,\r\n * 构建 authorize URL,通过 302 跳转到 SSO 登录页。\r\n *\r\n * @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;\r\n * 仅允许相对路径或同源绝对 URL,否则忽略并告警)\r\n */\r\n async login(returnUrl?: string): Promise<void> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n\r\n // 保存 PKCE verifier 和 state 到 sessionStorage(整页重定向后仍可读取)\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n\r\n if (returnUrl) {\r\n this._saveReturnUrlIfTrusted(returnUrl);\r\n }\r\n\r\n // 构建 authorize URL\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n\r\n // 跳转\r\n window.location.href = `${authorizeEndpoint}?${params.toString()}`;\r\n }\r\n\r\n /**\r\n * 构建登录 URL(不跳转,返回 URL 字符串)\r\n *\r\n * 适用于需要手动处理跳转的场景。\r\n */\r\n async getLoginUrl(returnUrl?: string): Promise<string> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n // 统一使用全局 returnUrl key,与 login() / CallbackPage 保持一致\r\n if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);\r\n\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n\r\n return `${authorizeEndpoint}?${params.toString()}`;\r\n }\r\n\r\n /**\r\n * 弹窗模式 SSO 登录\r\n *\r\n * 打开一个小窗口进行登录,认证完成后窗口自动关闭,\r\n * 主页面不丢失状态(适用于 SPA 中需要保持表单/浏览上下文的场景)。\r\n *\r\n * 流程:\r\n * 1. window.open() 打开授权 URL 到弹窗\r\n * 2. 用户在弹窗中完成登录\r\n * 3. 弹窗加载 CallbackPage 时检测到 window.opener,通过 postMessage 回传回调 URL\r\n * 4. 主页面收到消息后调用 handleCallback() 交换 token\r\n * 5. 弹窗自动关闭\r\n *\r\n * @param options - 弹窗配置\r\n * @param options.returnUrl - 登录成功后的返回地址\r\n * @param options.width - 弹窗宽度(默认 480)\r\n * @param options.height - 弹窗高度(默认 640)\r\n * @returns TokenData\r\n *\r\n * @example\r\n * ```typescript\r\n * const client = new SsoClient({ clientId: \"xxx\", redirectUri: \"https://myapp.com/callback\", ssoBaseUrl: \"https://nihplod.cn\" });\r\n * try {\r\n * const tokenData = await client.loginPopup({ returnUrl: \"/dashboard\" });\r\n * console.log(\"登录成功\", tokenData);\r\n * } catch (err) {\r\n * if (err instanceof SsoError && err.code === \"popup_blocked\") {\r\n * // 弹窗被拦截,回退到同页重定向\r\n * await client.login(\"/dashboard\");\r\n * }\r\n * }\r\n * ```\r\n */\r\n async loginPopup(\r\n options: { returnUrl?: string; width?: number; height?: number } = {}\r\n ): Promise<TokenData> {\r\n const verifier = generateCodeVerifier();\r\n const challenge = await generateCodeChallenge(verifier);\r\n const state = generateState();\r\n const popupNonce = generateState(); // 独立的 nonce 用于 postMessage 来源验证\r\n\r\n savePkceVerifier(this.config.clientId, verifier);\r\n saveOAuthState(state, this.config.clientId);\r\n // 保存 popup nonce 到 sessionStorage,用于 postMessage 校验\r\n savePkceVerifier(`${this.config.clientId}_popup_nonce`, popupNonce);\r\n\r\n if (options.returnUrl) {\r\n this._saveReturnUrlIfTrusted(options.returnUrl);\r\n }\r\n\r\n const authorizeEndpoint = await this._getAuthorizeEndpoint();\r\n const params = new URLSearchParams();\r\n params.set(\"response_type\", \"code\");\r\n params.set(\"client_id\", this.config.clientId);\r\n params.set(\"redirect_uri\", this.config.redirectUri);\r\n params.set(\"scope\", this.config.scopes || \"openid profile\");\r\n params.set(\"state\", state);\r\n params.set(\"code_challenge\", challenge);\r\n params.set(\"code_challenge_method\", \"S256\");\r\n // 弹窗 nonce:回调页通过 postMessage 回传,主窗口校验防伪造\r\n params.set(\"popup_nonce\", popupNonce);\r\n\r\n const width = options.width || 480;\r\n const height = options.height || 640;\r\n const left = Math.max(0, (window.screen.width - width) / 2);\r\n const top = Math.max(0, (window.screen.height - height) / 2);\r\n\r\n const popupFeatures = [\r\n `width=${width}`,\r\n `height=${height}`,\r\n `left=${Math.round(left)}`,\r\n `top=${Math.round(top)}`,\r\n \"resizable=yes\",\r\n \"scrollbars=yes\",\r\n \"status=yes\",\r\n ].join(\",\");\r\n\r\n const popup = window.open(\r\n `${authorizeEndpoint}?${params.toString()}`,\r\n \"nihplod_sso_popup\",\r\n popupFeatures\r\n );\r\n\r\n if (!popup) {\r\n removePkceVerifier(this.config.clientId);\r\n removeOAuthState(this.config.clientId);\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n throw new SsoError(\r\n \"popup_blocked\",\r\n \"弹窗被浏览器拦截,请允许弹窗后重试\"\r\n );\r\n }\r\n\r\n // 计算 redirect_uri 的 origin,用于 postMessage 来源校验\r\n const redirectUriOrigin = new URL(this.config.redirectUri).origin;\r\n\r\n try {\r\n // 聚焦弹窗\r\n popup.focus();\r\n\r\n return await new Promise<TokenData>((resolve, reject) => {\r\n let completed = false;\r\n\r\n const handleMessage = (event: MessageEvent) => {\r\n if (completed) return;\r\n // 校验消息来源:必须来自 redirect_uri 对应的子项目 origin(回调页运行位置)\r\n if (event.origin !== redirectUriOrigin) return;\r\n if (!event.data || event.data.type !== \"nihplod_sso_popup_callback\") return;\r\n if (!event.data.callbackUrl) return;\r\n\r\n // 校验 popup nonce:防止伪造的 postMessage(fail-closed:本地 nonce 缺失同样拒绝)\r\n const savedNonce = getPkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n if (!savedNonce || !timingSafeEqualString(event.data.nonce ?? \"\", savedNonce)) return;\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n\r\n completed = true;\r\n cleanup();\r\n\r\n if (popup && !popup.closed) {\r\n popup.close();\r\n }\r\n\r\n this.handleCallback(event.data.callbackUrl)\r\n .then(resolve)\r\n .catch(reject);\r\n };\r\n\r\n const pollTimer = setInterval(() => {\r\n if (popup.closed) {\r\n if (!completed) {\r\n completed = true;\r\n cleanup();\r\n reject(\r\n new SsoError(\r\n \"popup_closed\",\r\n \"登录窗口已关闭\"\r\n )\r\n );\r\n }\r\n }\r\n }, 500);\r\n\r\n const cleanup = () => {\r\n clearInterval(pollTimer);\r\n window.removeEventListener(\"message\", handleMessage);\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n };\r\n\r\n window.addEventListener(\"message\", handleMessage);\r\n });\r\n } catch (err) {\r\n removePkceVerifier(this.config.clientId);\r\n removeOAuthState(this.config.clientId);\r\n removePkceVerifier(`${this.config.clientId}_popup_nonce`);\r\n throw err;\r\n }\r\n }\r\n\r\n /**\r\n * 处理 OAuth 回调\r\n *\r\n * 解析回调 URL,校验 state 参数,用授权码交换 token。\r\n * 成功后 token 自动保存到 token 存储(默认内存,可通过 setTokenStorage 定制)。\r\n *\r\n * @param callbackUrl - 完整的回调 URL(window.location.href)\r\n * @returns TokenData 或 null\r\n */\r\n async handleCallback(\r\n callbackUrl: string\r\n ): Promise<TokenData> {\r\n const url = new URL(callbackUrl);\r\n const params = url.searchParams;\r\n\r\n // 检查错误\r\n const error = params.get(\"error\");\r\n if (error) {\r\n const desc = params.get(\"error_description\") || error;\r\n throw new SsoError(\"token_request_failed\", `授权失败: ${desc}`);\r\n }\r\n\r\n const code = params.get(\"code\");\r\n const returnedState = params.get(\"state\");\r\n\r\n if (!code) {\r\n throw new SsoError(\"token_request_failed\", \"回调 URL 中缺少 authorization code\");\r\n }\r\n\r\n // 校验 state 参数(CSRF 防护,常量时间比较)\r\n const savedState = getOAuthState(this.config.clientId);\r\n if (!savedState || !timingSafeEqualString(savedState, returnedState ?? \"\")) {\r\n // state 不匹配:state 与 verifier 一并清除,避免残留半套临时数据\r\n removeOAuthState(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n throw new SsoError(\r\n \"state_mismatch\",\r\n \"State 参数不匹配,可能存在 CSRF 攻击\"\r\n );\r\n }\r\n\r\n // 获取 code_verifier\r\n const verifier = getPkceVerifier(this.config.clientId);\r\n if (!verifier) {\r\n throw new SsoError(\r\n \"pkce_required\",\r\n \"code_verifier 不存在(可能已过期或来自其他标签页)\"\r\n );\r\n }\r\n\r\n // 交换 token\r\n const tokenEndpoint = await this._getTokenEndpoint();\r\n const body = new URLSearchParams();\r\n body.set(\"grant_type\", \"authorization_code\");\r\n body.set(\"code\", code);\r\n body.set(\"code_verifier\", verifier);\r\n body.set(\"client_id\", this.config.clientId);\r\n body.set(\"redirect_uri\", this.config.redirectUri);\r\n if (this.config.clientSecret) {\r\n body.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n\r\n let res: Response;\r\n try {\r\n res = await fetch(tokenEndpoint, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: body.toString(),\r\n });\r\n } catch (err) {\r\n throw new SsoError(\"network_error\", \"网络请求失败\", err);\r\n }\r\n\r\n if (!res.ok) {\r\n let errData: Record<string, unknown> = {};\r\n try { errData = await res.json(); } catch { /* ignore */ }\r\n const serverError = (errData.error as string) || \"\";\r\n throw new SsoError(\r\n mapOAuthErrorToSsoCode(serverError),\r\n (errData.error_description as string) || `Token 请求失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n // 先解析响应体,成功后再清除 state 和 verifier:\r\n // 若 JSON 畸形导致解析抛错,保留 state/verifier 允许用户重试回调\r\n const data: TokenResponse = await res.json();\r\n\r\n // Token 交换成功后清除 state 和 verifier(一次性临时数据)\r\n removeOAuthState(this.config.clientId);\r\n removePkceVerifier(this.config.clientId);\r\n\r\n // OIDC:验证 ID Token 签名、基本声明及 at_hash\r\n if (data.id_token) {\r\n try {\r\n await validateIdToken(\r\n data.id_token,\r\n data.access_token,\r\n this.config.ssoBaseUrl,\r\n this.config.clientId\r\n );\r\n } catch (err) {\r\n // 验证失败:不保存任何 token,防止伪造 ID Token\r\n removeTokenData(this.config.clientId);\r\n throw err;\r\n }\r\n }\r\n\r\n const now = Date.now();\r\n const tokenData: TokenData = {\r\n access_token: data.access_token,\r\n token_type: data.token_type,\r\n expires_in: data.expires_in,\r\n refresh_token: data.refresh_token,\r\n id_token: data.id_token,\r\n issued_at: now,\r\n expires_at: now + data.expires_in * 1000,\r\n };\r\n\r\n saveTokenData(tokenData, this.config.clientId);\r\n return tokenData;\r\n }\r\n\r\n /**\r\n * 刷新 Access Token\r\n *\r\n * 使用 refresh_token 换取新的 access_token。\r\n * 采用互斥锁防止并发刷新。\r\n * 支持 Refresh Token 原子轮换。\r\n */\r\n async refreshToken(): Promise<TokenData> {\r\n // 互斥锁:避免并发刷新\r\n if (this._refreshLock) return this._refreshLock;\r\n\r\n this._refreshLock = this._doRefreshToken();\r\n try {\r\n return await this._refreshLock;\r\n } finally {\r\n this._refreshLock = null;\r\n }\r\n }\r\n\r\n private async _doRefreshToken(): Promise<TokenData> {\r\n const current = getTokenData(this.config.clientId);\r\n if (!current?.refresh_token) {\r\n throw new SsoError(\"no_refresh_token\", \"没有可用的 refresh_token\");\r\n }\r\n\r\n const tokenEndpoint = await this._getTokenEndpoint();\r\n const body = new URLSearchParams();\r\n body.set(\"grant_type\", \"refresh_token\");\r\n body.set(\"refresh_token\", current.refresh_token);\r\n body.set(\"client_id\", this.config.clientId);\r\n if (this.config.clientSecret) {\r\n body.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n\r\n let res: Response | null = null;\r\n let lastErr: unknown;\r\n for (let attempt = 0; attempt < 2; attempt++) {\r\n try {\r\n res = await fetch(tokenEndpoint, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: body.toString(),\r\n });\r\n lastErr = null;\r\n break;\r\n } catch (err) {\r\n lastErr = err;\r\n if (attempt < 1) await new Promise((r) => setTimeout(r, 1000));\r\n }\r\n }\r\n if (lastErr || !res) {\r\n throw new SsoError(\"network_error\", \"刷新 Token 网络请求失败\", lastErr);\r\n }\r\n\r\n if (!res.ok) {\r\n let errData: Record<string, unknown> = {};\r\n try { errData = await res.json(); } catch { /* ignore */ }\r\n const errorCode = (errData.error as string) || \"\";\r\n // invalid_grant 表示 refresh_token 已被撤销或过期,应清除本地登录态\r\n if (errorCode === \"invalid_grant\" || res.status === 401) {\r\n removeTokenData(this.config.clientId);\r\n }\r\n throw new SsoError(\r\n mapOAuthErrorToSsoCode(errorCode, \"refresh\"),\r\n (errData.error_description as string) || `刷新 Token 失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n const data: TokenResponse = await res.json();\r\n\r\n if (data.id_token) {\r\n try {\r\n await validateIdToken(\r\n data.id_token,\r\n data.access_token,\r\n this.config.ssoBaseUrl,\r\n this.config.clientId\r\n );\r\n } catch (err) {\r\n removeTokenData(this.config.clientId);\r\n throw err;\r\n }\r\n }\r\n\r\n const now = Date.now();\r\n const tokenData: TokenData = {\r\n access_token: data.access_token,\r\n token_type: data.token_type,\r\n expires_in: data.expires_in,\r\n // RFC 6749 §6:刷新响应可省略 refresh_token,此时沿用旧值\r\n refresh_token: data.refresh_token || current.refresh_token,\r\n id_token: data.id_token,\r\n issued_at: now,\r\n expires_at: now + data.expires_in * 1000,\r\n };\r\n\r\n saveTokenData(tokenData, this.config.clientId);\r\n return tokenData;\r\n }\r\n\r\n /**\r\n * 获取用户信息\r\n *\r\n * 若 access_token 已过期则自动刷新后再请求。\r\n */\r\n async getUserInfo(): Promise<SsoUser> {\r\n let tokenData = getTokenData(this.config.clientId);\r\n\r\n if (!tokenData) {\r\n throw new SsoError(\"not_authenticated\", \"未登录\");\r\n }\r\n\r\n // 若已过期,先刷新\r\n if (Date.now() >= tokenData.expires_at) {\r\n tokenData = await this.refreshToken();\r\n }\r\n\r\n const userinfoEndpoint = await this._getUserinfoEndpoint();\r\n\r\n let res: Response;\r\n try {\r\n res = await fetch(userinfoEndpoint, {\r\n headers: {\r\n Authorization: `Bearer ${tokenData.access_token}`,\r\n },\r\n });\r\n } catch (err) {\r\n throw new SsoError(\"network_error\", \"获取用户信息网络请求失败\", err);\r\n }\r\n\r\n if (!res.ok) {\r\n if (res.status === 401) {\r\n removeTokenData(this.config.clientId);\r\n throw new SsoError(\"not_authenticated\", \"Token 已失效\");\r\n }\r\n throw new SsoError(\r\n \"userinfo_failed\",\r\n `获取用户信息失败: HTTP ${res.status}`\r\n );\r\n }\r\n\r\n return (await res.json()) as SsoUser;\r\n }\r\n\r\n /**\r\n * 获取当前 access_token\r\n *\r\n * 若已过期则自动刷新。用于子项目自行发起 API 请求时获取 Bearer token。\r\n * 若无 token 返回 null;若刷新失败则抛出错误(与 getUserInfo 行为一致)。\r\n */\r\n async getAccessToken(): Promise<string | null> {\r\n let tokenData = getTokenData(this.config.clientId);\r\n if (!tokenData) return null;\r\n\r\n if (Date.now() >= tokenData.expires_at) {\r\n tokenData = await this.refreshToken();\r\n }\r\n\r\n return tokenData.access_token;\r\n }\r\n\r\n /**\r\n * 检查是否已认证(不发起网络请求)\r\n *\r\n * 仅检查本地是否存在未过期的 access_token。\r\n */\r\n isAuthenticated(): boolean {\r\n const tokenData = getTokenData(this.config.clientId);\r\n if (!tokenData) return false;\r\n return Date.now() < tokenData.expires_at;\r\n }\r\n\r\n /**\r\n * 登出\r\n *\r\n * 清除本地所有 token 和临时数据,并尝试撤销服务端 refresh_token。\r\n * @param redirectToSso - 是否重定向到 SSO 登出页(默认 false)\r\n */\r\n async logout(redirectToSso: boolean = false): Promise<void> {\r\n // 获取 refresh_token 用于服务端撤销(需在 clearAllSsoData 之前)\r\n const tokenData = getTokenData(this.config.clientId);\r\n const refreshToken = tokenData?.refresh_token;\r\n const idTokenHint = tokenData?.id_token;\r\n\r\n clearAllSsoData(this.config.clientId);\r\n\r\n // 尝试调用服务端 token revocation 端点(best-effort)\r\n // 仅 Confidential Client 携带 client_secret;Public Client 不传 secret。\r\n if (refreshToken && this.config.clientId) {\r\n try {\r\n const discovery = await this._getDiscovery();\r\n const revokeUrl =\r\n discovery?.revocation_endpoint ||\r\n `${this.config.ssoBaseUrl}/api/oauth/revoke`;\r\n const revokeBody = new URLSearchParams({\r\n token: refreshToken,\r\n token_type_hint: \"refresh_token\",\r\n client_id: this.config.clientId,\r\n });\r\n if (this.config.clientSecret) {\r\n revokeBody.set(\"client_secret\", this.config.clientSecret);\r\n }\r\n await fetch(revokeUrl, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: revokeBody.toString(),\r\n });\r\n } catch {\r\n // 撤销失败不影响本地登出(best-effort)\r\n }\r\n }\r\n\r\n if (redirectToSso) {\r\n // OIDC RP-Initiated Logout:携带 client_id / post_logout_redirect_uri / id_token_hint / state\r\n const discovery = await this._getDiscovery();\r\n const endSessionEndpoint =\r\n discovery?.end_session_endpoint || `${this.config.ssoBaseUrl}/api/oauth/end-session`;\r\n const logoutUrl = new URL(endSessionEndpoint);\r\n logoutUrl.searchParams.set(\"client_id\", this.config.clientId);\r\n const postLogoutUri = this.config.postLogoutRedirectUri || this.config.redirectUri;\r\n if (postLogoutUri) {\r\n logoutUrl.searchParams.set(\r\n \"post_logout_redirect_uri\",\r\n postLogoutUri\r\n );\r\n }\r\n if (idTokenHint) {\r\n logoutUrl.searchParams.set(\"id_token_hint\", idTokenHint);\r\n }\r\n const state = generateState();\r\n logoutUrl.searchParams.set(\"state\", state);\r\n window.location.href = logoutUrl.toString();\r\n }\r\n }\r\n\r\n /**\r\n * 获取 OIDC Discovery 文档\r\n *\r\n * 用于调试和获取 SSO 中心完整配置。\r\n * 可能返回 null(当 Discovery 端点不可达且无缓存时)。\r\n */\r\n async getDiscovery(): Promise<OidcDiscovery | null> {\r\n return this._getDiscovery();\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
package/dist/next/index.d.mts
CHANGED
|
@@ -46,6 +46,13 @@ interface SsoMiddlewareConfig {
|
|
|
46
46
|
* 浏览器端 Public Client 应省略。
|
|
47
47
|
*/
|
|
48
48
|
clientSecret?: string;
|
|
49
|
+
/**
|
|
50
|
+
* 是否对 ssoCookieName 对应的主站 Cookie 进行 Introspection 二次验证。
|
|
51
|
+
* 默认 true(推荐)。设为 false 时仅检查 Cookie 存在性,延迟最低但可能放行
|
|
52
|
+
* 已失效/被撤销的会话 —— 中间件本质上只是 UX 层,敏感数据必须在
|
|
53
|
+
* Route Handler / Server Component 中二次校验。
|
|
54
|
+
*/
|
|
55
|
+
validateSsoCookie?: boolean;
|
|
49
56
|
/** Access Token Cookie 名称,默认 __Host-nihplod_sso_at */
|
|
50
57
|
accessTokenCookieName?: string;
|
|
51
58
|
/** State Cookie 名称,默认 __Host-nihplod_sso_state */
|
|
@@ -123,13 +130,17 @@ declare function createCallbackRouteHandler(config: CallbackRouteConfig): (reque
|
|
|
123
130
|
* ```ts
|
|
124
131
|
* import { createLogoutRouteHandler } from "@nihplod/sso-sdk/next";
|
|
125
132
|
*
|
|
126
|
-
*
|
|
133
|
+
* const handler = createLogoutRouteHandler({
|
|
127
134
|
* clientId: "my-app",
|
|
128
135
|
* clientSecret: "optional-secret",
|
|
129
136
|
* ssoBaseUrl: "https://nihplod.cn",
|
|
130
137
|
* redirectUri: "https://myapp.com/api/auth/callback",
|
|
131
138
|
* postLogoutRedirectUri: "https://myapp.com/",
|
|
132
139
|
* });
|
|
140
|
+
*
|
|
141
|
+
* // 推荐使用 POST 触发登出(防登出 CSRF),GET 保留用于兼容 <a> 标签跳转
|
|
142
|
+
* export const GET = handler;
|
|
143
|
+
* export const POST = handler;
|
|
133
144
|
* ```
|
|
134
145
|
*/
|
|
135
146
|
|
|
@@ -163,6 +174,8 @@ interface LogoutRouteConfig {
|
|
|
163
174
|
verifierCookieName?: string;
|
|
164
175
|
/** 回调路径(用于清除 verifier cookie),默认 "/api/auth/callback" */
|
|
165
176
|
callbackPath?: string;
|
|
177
|
+
/** Logout State Cookie 名称(RP-Initiated Logout CSRF 防护),默认 __Host-nihplod_sso_logout_state */
|
|
178
|
+
logoutStateCookieName?: string;
|
|
166
179
|
}
|
|
167
180
|
declare function createLogoutRouteHandler(config: LogoutRouteConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
168
181
|
|
|
@@ -178,9 +191,11 @@ declare function createLogoutRouteHandler(config: LogoutRouteConfig): (request:
|
|
|
178
191
|
*/
|
|
179
192
|
declare const DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_at";
|
|
180
193
|
declare const DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_rt";
|
|
194
|
+
declare const DEFAULT_ID_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_id";
|
|
181
195
|
declare const DEFAULT_STATE_COOKIE_NAME = "__Host-nihplod_sso_state";
|
|
182
196
|
declare const DEFAULT_RETURN_COOKIE_NAME = "__Host-nihplod_sso_return";
|
|
183
197
|
declare const DEFAULT_VERIFIER_COOKIE_NAME = "__Secure-nihplod_sso_verifier";
|
|
198
|
+
declare const DEFAULT_LOGOUT_STATE_COOKIE_NAME = "__Host-nihplod_sso_logout_state";
|
|
184
199
|
/** __Host- 前缀 Cookie 的安全选项(Path 必须为 /) */
|
|
185
200
|
declare function getHostCookieOptions(maxAge?: number): {
|
|
186
201
|
httpOnly: true;
|
|
@@ -198,4 +213,4 @@ declare function getSecureCookieOptions(maxAge?: number, path?: string): {
|
|
|
198
213
|
maxAge?: number;
|
|
199
214
|
};
|
|
200
215
|
|
|
201
|
-
export { type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutRouteConfig, type SsoMiddlewareConfig, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions };
|
|
216
|
+
export { type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_ID_TOKEN_COOKIE_NAME, DEFAULT_LOGOUT_STATE_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutRouteConfig, type SsoMiddlewareConfig, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions };
|
package/dist/next/index.d.ts
CHANGED
|
@@ -46,6 +46,13 @@ interface SsoMiddlewareConfig {
|
|
|
46
46
|
* 浏览器端 Public Client 应省略。
|
|
47
47
|
*/
|
|
48
48
|
clientSecret?: string;
|
|
49
|
+
/**
|
|
50
|
+
* 是否对 ssoCookieName 对应的主站 Cookie 进行 Introspection 二次验证。
|
|
51
|
+
* 默认 true(推荐)。设为 false 时仅检查 Cookie 存在性,延迟最低但可能放行
|
|
52
|
+
* 已失效/被撤销的会话 —— 中间件本质上只是 UX 层,敏感数据必须在
|
|
53
|
+
* Route Handler / Server Component 中二次校验。
|
|
54
|
+
*/
|
|
55
|
+
validateSsoCookie?: boolean;
|
|
49
56
|
/** Access Token Cookie 名称,默认 __Host-nihplod_sso_at */
|
|
50
57
|
accessTokenCookieName?: string;
|
|
51
58
|
/** State Cookie 名称,默认 __Host-nihplod_sso_state */
|
|
@@ -123,13 +130,17 @@ declare function createCallbackRouteHandler(config: CallbackRouteConfig): (reque
|
|
|
123
130
|
* ```ts
|
|
124
131
|
* import { createLogoutRouteHandler } from "@nihplod/sso-sdk/next";
|
|
125
132
|
*
|
|
126
|
-
*
|
|
133
|
+
* const handler = createLogoutRouteHandler({
|
|
127
134
|
* clientId: "my-app",
|
|
128
135
|
* clientSecret: "optional-secret",
|
|
129
136
|
* ssoBaseUrl: "https://nihplod.cn",
|
|
130
137
|
* redirectUri: "https://myapp.com/api/auth/callback",
|
|
131
138
|
* postLogoutRedirectUri: "https://myapp.com/",
|
|
132
139
|
* });
|
|
140
|
+
*
|
|
141
|
+
* // 推荐使用 POST 触发登出(防登出 CSRF),GET 保留用于兼容 <a> 标签跳转
|
|
142
|
+
* export const GET = handler;
|
|
143
|
+
* export const POST = handler;
|
|
133
144
|
* ```
|
|
134
145
|
*/
|
|
135
146
|
|
|
@@ -163,6 +174,8 @@ interface LogoutRouteConfig {
|
|
|
163
174
|
verifierCookieName?: string;
|
|
164
175
|
/** 回调路径(用于清除 verifier cookie),默认 "/api/auth/callback" */
|
|
165
176
|
callbackPath?: string;
|
|
177
|
+
/** Logout State Cookie 名称(RP-Initiated Logout CSRF 防护),默认 __Host-nihplod_sso_logout_state */
|
|
178
|
+
logoutStateCookieName?: string;
|
|
166
179
|
}
|
|
167
180
|
declare function createLogoutRouteHandler(config: LogoutRouteConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
168
181
|
|
|
@@ -178,9 +191,11 @@ declare function createLogoutRouteHandler(config: LogoutRouteConfig): (request:
|
|
|
178
191
|
*/
|
|
179
192
|
declare const DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_at";
|
|
180
193
|
declare const DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_rt";
|
|
194
|
+
declare const DEFAULT_ID_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_id";
|
|
181
195
|
declare const DEFAULT_STATE_COOKIE_NAME = "__Host-nihplod_sso_state";
|
|
182
196
|
declare const DEFAULT_RETURN_COOKIE_NAME = "__Host-nihplod_sso_return";
|
|
183
197
|
declare const DEFAULT_VERIFIER_COOKIE_NAME = "__Secure-nihplod_sso_verifier";
|
|
198
|
+
declare const DEFAULT_LOGOUT_STATE_COOKIE_NAME = "__Host-nihplod_sso_logout_state";
|
|
184
199
|
/** __Host- 前缀 Cookie 的安全选项(Path 必须为 /) */
|
|
185
200
|
declare function getHostCookieOptions(maxAge?: number): {
|
|
186
201
|
httpOnly: true;
|
|
@@ -198,4 +213,4 @@ declare function getSecureCookieOptions(maxAge?: number, path?: string): {
|
|
|
198
213
|
maxAge?: number;
|
|
199
214
|
};
|
|
200
215
|
|
|
201
|
-
export { type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutRouteConfig, type SsoMiddlewareConfig, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions };
|
|
216
|
+
export { type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_ID_TOKEN_COOKIE_NAME, DEFAULT_LOGOUT_STATE_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutRouteConfig, type SsoMiddlewareConfig, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions };
|