@nihplod/sso-sdk 1.2.0 → 1.2.1
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/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.js +6 -4
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +6 -4
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +5 -0
- package/dist/react/index.d.ts +5 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/next/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/next/middleware.ts","../../src/next/constants.ts","../../src/next/callback.ts","../../src/core/errors.ts","../../src/core/security.ts","../../src/core/id-token.ts","../../src/next/logout.ts"],"sourcesContent":["/**\r\n * Next.js Middleware 辅助函数\r\n *\r\n * 在 Edge Runtime 中运行,自动检测未认证请求并重定向到 SSO 登录页。\r\n *\r\n * ⚠️ Edge Runtime 限制:\r\n * - 不能使用 Node.js crypto 模块\r\n * - PKCE code_challenge 使用 Web Crypto API 的 crypto.subtle.digest(SHA-256)\r\n * (Edge Runtime 18+ 完全支持此 API)\r\n *\r\n * 用法 (src/middleware.ts):\r\n * ```ts\r\n * import { createSsoMiddleware } from \"@nihplod/sso-sdk/next\";\r\n *\r\n * export const middleware = createSsoMiddleware({\r\n * clientId: \"my-app\",\r\n * ssoBaseUrl: \"https://nihplod.cn\",\r\n * redirectUri: \"https://myapp.com/api/auth/callback\",\r\n * publicPaths: [\"/\", \"/public\", \"/api/public\"],\r\n * });\r\n * ```\r\n */\r\n\r\nimport { NextRequest, NextResponse } from \"next/server\";\r\nimport {\r\n DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\r\n DEFAULT_STATE_COOKIE_NAME,\r\n DEFAULT_RETURN_COOKIE_NAME,\r\n DEFAULT_VERIFIER_COOKIE_NAME,\r\n getHostCookieOptions,\r\n getSecureCookieOptions,\r\n resolveInsecureLocalDev,\r\n toInsecureCookieName,\r\n} from \"./constants\";\r\n\r\n// ============================================\r\n// 类型定义\r\n// ============================================\r\n\r\nexport interface SsoMiddlewareConfig {\r\n /** OAuth Client ID */\r\n clientId: string;\r\n\r\n /** SSO 中心地址 */\r\n ssoBaseUrl: string;\r\n\r\n /** 回调 URL(须与注册的 redirect_uri 一致) */\r\n redirectUri: string;\r\n\r\n /** 请求的 scope(空格分隔),默认 \"openid profile\" */\r\n scopes?: string;\r\n\r\n /** 公开路由前缀(不需要认证) */\r\n publicPaths?: string[];\r\n\r\n /** 回调路径(不触发重定向),默认 \"/api/auth/callback\" */\r\n callbackPath?: string;\r\n\r\n /**\r\n * 主站用户会话 Cookie 名称,用于检测是否已有 SSO 会话。\r\n * 默认 \"__Host-user_token\"(与主站 C 端登录 Cookie 一致)。\r\n */\r\n ssoCookieName?: string;\r\n\r\n /**\r\n * OAuth Client Secret(Confidential Client 使用)\r\n * 浏览器端 Public Client 应省略。\r\n */\r\n clientSecret?: string;\r\n\r\n /**\r\n * 是否对 ssoCookieName 对应的主站 Cookie 进行 Introspection 二次验证。\r\n * 默认 true(推荐)。设为 false 时仅检查 Cookie 存在性,延迟最低但可能放行\r\n * 已失效/被撤销的会话 —— 中间件本质上只是 UX 层,敏感数据必须在\r\n * Route Handler / Server Component 中二次校验。\r\n */\r\n validateSsoCookie?: boolean;\r\n\r\n /** Access Token Cookie 名称,默认 __Host-nihplod_sso_at */\r\n accessTokenCookieName?: string;\r\n\r\n /** State Cookie 名称,默认 __Host-nihplod_sso_state */\r\n stateCookieName?: string;\r\n\r\n /** Return URL Cookie 名称,默认 __Host-nihplod_sso_return */\r\n returnUrlCookieName?: string;\r\n\r\n /** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */\r\n verifierCookieName?: string;\r\n\r\n /**\r\n * 本地 HTTP 开发模式(默认 false)。\r\n *\r\n * ⚠️ 仅限 http://localhost 开发:关闭 Cookie 的 Secure 属性并去除\r\n * __Host-/__Secure- 前缀(浏览器拒绝在 HTTP 下写入带这两个前缀的 Cookie,\r\n * 否则会出现「登录后 cookie 写不进去 → middleware 永远判定未登录 →\r\n * 反复跳 SSO」的无限重定向)。开启时启动告警;生产环境严禁启用——\r\n * 生产环境(NODE_ENV=production 且 ssoBaseUrl 为 https)下该开关会被\r\n * 强制忽略并告警,仍走 secure cookie。\r\n * middleware / callback / logout 三处配置需保持一致。\r\n */\r\n insecureLocalDev?: boolean;\r\n}\r\n\r\n// ============================================\r\n// 工具函数\r\n// ============================================\r\n\r\n/**\r\n * 生成安全随机字符串(Edge Runtime 兼容)\r\n * 使用 crypto.getRandomValues(Edge Runtime 支持)\r\n * 拒绝采样法保证 charset 均匀分布\r\n */\r\nfunction generateRandomString(length: number): string {\r\n const chars =\r\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\r\n const maxValid = Math.floor(256 / chars.length) * chars.length;\r\n let result = \"\";\r\n\r\n while (result.length < length) {\r\n const array = new Uint8Array(length * 2);\r\n crypto.getRandomValues(array);\r\n for (let i = 0; i < array.length && result.length < length; i++) {\r\n if (array[i] >= maxValid) continue;\r\n result += chars[array[i] % chars.length];\r\n }\r\n }\r\n return result;\r\n}\r\n\r\n/**\r\n * 计算 PKCE S256 code_challenge(Edge Runtime 兼容)\r\n *\r\n * 使用 Web Crypto API 的 crypto.subtle.digest(\"SHA-256\"),\r\n * Edge Runtime 18+ 完全支持。\r\n */\r\nasync function computeCodeChallenge(verifier: string): Promise<string> {\r\n const encoder = new TextEncoder();\r\n const data = encoder.encode(verifier);\r\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\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 * 路径匹配(支持 /:path* 通配符)\r\n */\r\nfunction matchesPath(pathname: string, paths: string[]): boolean {\r\n return paths.some((path) => {\r\n if (pathname === path) return true;\r\n if (path.endsWith(\"/:path*\") && pathname.startsWith(path.replace(\"/:path*\", \"\"))) return true;\r\n if (pathname.startsWith(path + \"/\")) return true;\r\n return false;\r\n });\r\n}\r\n\r\n/**\r\n * 通过主站 Introspection 端点校验 access_token 是否仍有效。\r\n * Confidential Client(BFF)应配置 clientSecret,防止伪造 Cookie 绕过。\r\n *\r\n * 附带进程级缓存(key = token 的 SHA-256 hex + clientId),\r\n * 减少同一用户 session 在短时间内对 SSO 中心的重复 introspection 调用。\r\n * 缓存带 TTL(30s)与容量上限(LRU 淘汰),避免无界增长。\r\n */\r\nconst introspectionCache = new Map<string, { active: boolean; until: number }>();\r\nconst INTROSPECT_CACHE_TTL_MS = 30_000; // 30 秒\r\nconst INTROSPECT_CACHE_MAX_ENTRIES = 500;\r\n\r\n/** 计算 token 的 SHA-256 hex(Edge Runtime Web Crypto),用作缓存 key 避免跨用户碰撞 */\r\nasync function introspectCacheKey(token: string, clientId: string): Promise<string> {\r\n const hash = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(token));\r\n const hex = Array.from(new Uint8Array(hash))\r\n .map((b) => b.toString(16).padStart(2, \"0\"))\r\n .join(\"\");\r\n return `${hex}|${clientId}`;\r\n}\r\n\r\n/** 读取缓存(命中时刷新插入顺序,实现 LRU) */\r\nfunction introspectCacheGet(key: string): { active: boolean; until: number } | null {\r\n const entry = introspectionCache.get(key);\r\n if (!entry) return null;\r\n if (entry.until <= Date.now()) {\r\n introspectionCache.delete(key);\r\n return null;\r\n }\r\n // LRU:重新插入到 Map 尾部\r\n introspectionCache.delete(key);\r\n introspectionCache.set(key, entry);\r\n return entry;\r\n}\r\n\r\n/** 写入缓存(先清过期项,再按插入顺序淘汰最旧项) */\r\nfunction introspectCacheSet(key: string, active: boolean): void {\r\n const now = Date.now();\r\n for (const [k, v] of introspectionCache) {\r\n if (v.until <= now) introspectionCache.delete(k);\r\n }\r\n while (introspectionCache.size >= INTROSPECT_CACHE_MAX_ENTRIES) {\r\n const oldest = introspectionCache.keys().next();\r\n if (oldest.done) break;\r\n introspectionCache.delete(oldest.value);\r\n }\r\n introspectionCache.set(key, { active, until: now + INTROSPECT_CACHE_TTL_MS });\r\n}\r\n\r\nasync function introspectAccessToken(\r\n token: string,\r\n ssoBaseUrl: string,\r\n clientId: string,\r\n clientSecret?: string\r\n): Promise<boolean> {\r\n const cacheKey = await introspectCacheKey(token, clientId);\r\n const cached = introspectCacheGet(cacheKey);\r\n if (cached) {\r\n return cached.active;\r\n }\r\n\r\n try {\r\n const body = new URLSearchParams({\r\n token,\r\n token_type_hint: \"access_token\",\r\n client_id: clientId,\r\n });\r\n if (clientSecret) {\r\n body.set(\"client_secret\", clientSecret);\r\n }\r\n\r\n const res = await fetch(`${ssoBaseUrl}/api/oauth/introspect`, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: body.toString(),\r\n });\r\n\r\n if (!res.ok) {\r\n // 仅缓存确定性的\"token 无效\"结果;网络错误不缓存,下次重试\r\n if (res.status !== 401 && res.status !== 403) {\r\n // 非认证错误(如 500、网络问题):不缓存,让下次请求重试\r\n return false;\r\n }\r\n introspectCacheSet(cacheKey, false);\r\n return false;\r\n }\r\n const data = (await res.json()) as { active?: boolean };\r\n const active = data.active === true;\r\n introspectCacheSet(cacheKey, active);\r\n return active;\r\n } catch {\r\n // 网络异常不缓存:下次请求会重试 introspection\r\n return false;\r\n }\r\n}\r\n\r\n// ============================================\r\n// Middleware 工厂函数\r\n// ============================================\r\n\r\nexport function createSsoMiddleware(config: SsoMiddlewareConfig) {\r\n const {\r\n clientId,\r\n clientSecret,\r\n ssoBaseUrl,\r\n redirectUri,\r\n scopes = \"openid profile\",\r\n publicPaths = [],\r\n callbackPath = \"/api/auth/callback\",\r\n ssoCookieName = \"__Host-user_token\",\r\n validateSsoCookie = true,\r\n insecureLocalDev: insecureLocalDevOpt = false,\r\n } = config;\r\n\r\n // 生产守卫:NODE_ENV=production 且 ssoBaseUrl 为 https 时强制忽略该开关(仍走 secure cookie)\r\n const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);\r\n\r\n // insecureLocalDev:HTTP 本地开发下去除 Cookie 前缀(浏览器拒绝无 Secure 的前缀 Cookie)\r\n const secureCookies = !insecureLocalDev;\r\n const accessTokenCookieName = insecureLocalDev\r\n ? toInsecureCookieName(config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME)\r\n : config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME;\r\n const stateCookieName = insecureLocalDev\r\n ? toInsecureCookieName(config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME)\r\n : config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME;\r\n const returnUrlCookieName = insecureLocalDev\r\n ? toInsecureCookieName(config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME)\r\n : config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME;\r\n const verifierCookieName = insecureLocalDev\r\n ? toInsecureCookieName(config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME)\r\n : config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME;\r\n\r\n // 规范化 ssoBaseUrl\r\n const normalizedBase = ssoBaseUrl.replace(/\\/+$/, \"\");\r\n\r\n if (process.env.NODE_ENV !== \"production\") {\r\n if (!validateSsoCookie) {\r\n console.warn(\r\n \"[SSO SDK] validateSsoCookie=false:中间件仅检查 Cookie 存在性,可能放行已失效的会话。\" +\r\n \"中间件只是 UX 层,敏感数据的鉴权必须在 Route Handler / Server Component 中完成。\"\r\n );\r\n }\r\n if (!clientSecret) {\r\n console.warn(\r\n \"[SSO SDK] 未配置 clientSecret(Public Client 模式):introspection 无客户端认证,\" +\r\n \"中间件判定结果仅作 UX 参考。Confidential Client(BFF)请配置 clientSecret。\"\r\n );\r\n }\r\n }\r\n\r\n return async function ssoMiddleware(request: NextRequest) {\r\n const { pathname } = request.nextUrl;\r\n\r\n // Exclude Next.js internal routes and static assets\r\n if (\r\n pathname.startsWith(\"/_next/\") ||\r\n pathname.startsWith(\"/favicon.ico\") ||\r\n pathname.match(/\\.(ico|png|jpg|jpeg|svg|css|js|woff2?)$/)\r\n ) {\r\n return NextResponse.next();\r\n }\r\n\r\n // Callback path: let the callback handler deal with it\r\n if (pathname === callbackPath) {\r\n return NextResponse.next();\r\n }\r\n\r\n // Public paths: no auth required\r\n const allPublicPaths = [callbackPath, ...publicPaths];\r\n if (matchesPath(pathname, allPublicPaths)) {\r\n return NextResponse.next();\r\n }\r\n\r\n // Check for existing SSO session\r\n const ssoSession = request.cookies.get(ssoCookieName);\r\n if (ssoSession?.value) {\r\n // 可选:对主站 Cookie 进行 Introspection 二次验证\r\n if (validateSsoCookie) {\r\n const tokenActive = await introspectAccessToken(\r\n ssoSession.value,\r\n normalizedBase,\r\n clientId,\r\n clientSecret\r\n );\r\n if (tokenActive) {\r\n return NextResponse.next();\r\n }\r\n // Token 无效:继续到 access_token cookie 检查或重定向\r\n } else {\r\n return NextResponse.next();\r\n }\r\n }\r\n\r\n // Check for access_token in cookie (set by callback handler)\r\n const accessTokenCookie = request.cookies.get(accessTokenCookieName);\r\n if (accessTokenCookie?.value) {\r\n // 调用 Introspection 精确校验 token 是否仍有效。\r\n // Confidential Client 携带 clientSecret;Public Client 仅传 clientId。\r\n const tokenActive = await introspectAccessToken(\r\n accessTokenCookie.value,\r\n normalizedBase,\r\n clientId,\r\n clientSecret\r\n );\r\n\r\n if (tokenActive) {\r\n return NextResponse.next();\r\n }\r\n // Token 无效或已过期:清除 cookie 并继续到 SSO 重定向\r\n }\r\n\r\n // No auth: redirect to SSO\r\n const state = generateRandomString(32);\r\n const verifier = generateRandomString(64);\r\n\r\n // 在 Edge Runtime 中计算 PKCE code_challenge(SHA-256 + base64url)\r\n const challenge = await computeCodeChallenge(verifier);\r\n\r\n const authorizeParams = new URLSearchParams();\r\n authorizeParams.set(\"response_type\", \"code\");\r\n authorizeParams.set(\"client_id\", clientId);\r\n authorizeParams.set(\"redirect_uri\", redirectUri);\r\n authorizeParams.set(\"scope\", scopes);\r\n authorizeParams.set(\"state\", state);\r\n authorizeParams.set(\"code_challenge\", challenge);\r\n authorizeParams.set(\"code_challenge_method\", \"S256\");\r\n\r\n const loginUrl = new URL(\"/api/oauth/authorize\", normalizedBase);\r\n loginUrl.search = authorizeParams.toString();\r\n\r\n const response = NextResponse.redirect(loginUrl);\r\n\r\n // Set state cookie for CSRF verification on callback\r\n response.cookies.set(stateCookieName, state, getHostCookieOptions(600, secureCookies));\r\n\r\n // Set PKCE verifier cookie(httpOnly,供 callback handler 使用)\r\n // 使用 __Secure- 前缀,允许写入 callbackPath\r\n response.cookies.set(verifierCookieName, verifier, getSecureCookieOptions(600, callbackPath, secureCookies));\r\n\r\n // Set return URL cookie(仅存储 pathname + search,截断至安全长度)\r\n const safeReturnUrl = (request.nextUrl.pathname + request.nextUrl.search).slice(0, 2048);\r\n response.cookies.set(returnUrlCookieName, safeReturnUrl, getHostCookieOptions(600, secureCookies));\r\n\r\n // 如果存在过期的 access_token cookie,立即清除\r\n if (accessTokenCookie?.value) {\r\n response.cookies.set(accessTokenCookieName, \"\", getHostCookieOptions(0, secureCookies));\r\n }\r\n\r\n return response;\r\n };\r\n}\r\n","/**\n * Next.js 集成默认 Cookie 名称\n *\n * 除 verifier 外统一使用 __Host- 前缀,要求:\n * - Secure\n * - Path=/\n * - 无 Domain 属性\n *\n * verifier 需要写入回调路径(非 /),因此使用 __Secure- 前缀。\n */\n\nexport const DEFAULT_ACCESS_TOKEN_COOKIE_NAME = \"__Host-nihplod_sso_at\";\nexport const DEFAULT_REFRESH_TOKEN_COOKIE_NAME = \"__Host-nihplod_sso_rt\";\nexport const DEFAULT_ID_TOKEN_COOKIE_NAME = \"__Host-nihplod_sso_id\";\nexport const DEFAULT_STATE_COOKIE_NAME = \"__Host-nihplod_sso_state\";\nexport const DEFAULT_RETURN_COOKIE_NAME = \"__Host-nihplod_sso_return\";\n// Verifier 需要写入回调路径(非 /),因此使用 __Secure- 前缀而非 __Host-\nexport const DEFAULT_VERIFIER_COOKIE_NAME = \"__Secure-nihplod_sso_verifier\";\n// RP-Initiated Logout 的 state cookie(登出 CSRF 防护)\nexport const DEFAULT_LOGOUT_STATE_COOKIE_NAME = \"__Host-nihplod_sso_logout_state\";\n\n/**\n * insecureLocalDev 场景:去除 __Host-/__Secure- 前缀。\n * 浏览器强制要求带这两个前缀的 Cookie 必须设置 Secure,\n * HTTP 本地开发时若保留前缀,即使 secure=false 也会被拒绝写入。\n */\nexport function toInsecureCookieName(name: string): string {\n return name.replace(/^__(Host|Secure)-/, \"\");\n}\n\n/**\n * 解析 insecureLocalDev 开关并输出告警(middleware / callback / logout 三处共用,保证行为一致)。\n *\n * 生产守卫:当 NODE_ENV=production 且 ssoBaseUrl 为 https 时强制忽略该开关,\n * Cookie 仍使用 Secure 属性并保留 __Host-/__Secure- 前缀,\n * 避免误配置把生产环境的 Cookie 降级为 HTTP 可写。\n *\n * 返回值为实际生效的 insecureLocalDev 状态。\n */\nexport function resolveInsecureLocalDev(enabled: boolean, ssoBaseUrl: string): boolean {\n if (!enabled) return false;\n if (process.env.NODE_ENV === \"production\" && /^https:\\/\\//i.test(ssoBaseUrl)) {\n console.warn(\n \"[SSO SDK] insecureLocalDev=true 已被忽略:当前为生产环境(NODE_ENV=production)且 ssoBaseUrl 使用 HTTPS,\" +\n \"Cookie 仍保持 Secure 属性与 __Host-/__Secure- 前缀。请从生产配置中移除该开关。\"\n );\n return false;\n }\n console.warn(\n \"[SSO SDK] insecureLocalDev=true:Cookie 的 Secure 属性已关闭且去除 __Host-/__Secure- 前缀。\" +\n \"仅限 http://localhost 本地开发使用,生产环境必须移除该配置(生产必须用 HTTPS)。\"\n );\n return true;\n}\n\n/** __Host- 前缀 Cookie 的安全选项(Path 必须为 /) */\nexport function getHostCookieOptions(\n maxAge?: number,\n secure: boolean = true\n): {\n httpOnly: true;\n secure: boolean;\n sameSite: \"lax\";\n path: \"/\";\n maxAge?: number;\n} {\n return {\n httpOnly: true,\n secure,\n sameSite: \"lax\",\n path: \"/\",\n ...(maxAge !== undefined ? { maxAge } : {}),\n };\n}\n\n/** __Secure- 前缀 Cookie 的安全选项(允许自定义 Path) */\nexport function getSecureCookieOptions(\n maxAge?: number,\n path = \"/\",\n secure: boolean = true\n): {\n httpOnly: true;\n secure: boolean;\n sameSite: \"lax\";\n path: string;\n maxAge?: number;\n} {\n return {\n httpOnly: true,\n secure,\n sameSite: \"lax\",\n path,\n ...(maxAge !== undefined ? { maxAge } : {}),\n };\n}\n","/**\r\n * App Router 回调 Route Handler\r\n *\r\n * 在 /api/auth/callback 路由中处理 OAuth 回调:\r\n * 1. 验证 state 参数\r\n * 2. 用授权码交换 token\r\n * 3. 设置 session cookie\r\n * 4. 重定向到原始页面\r\n *\r\n * ⚠️ 此 handler 仅运行在 Node.js Runtime(非 Edge),可以使用 crypto.subtle。\r\n *\r\n * 用法 (src/app/api/auth/callback/route.ts):\r\n * ```ts\r\n * import { createCallbackRouteHandler } from \"@nihplod/sso-sdk/next\";\r\n *\r\n * export const GET = createCallbackRouteHandler({\r\n * clientId: \"my-app\",\r\n * ssoBaseUrl: \"https://nihplod.cn\",\r\n * redirectUri: \"https://myapp.com/api/auth/callback\",\r\n * tokenCookieName: \"nihplod_sso_at\",\r\n * });\r\n * ```\r\n */\r\n\r\nimport { NextRequest, NextResponse } from \"next/server\";\r\nimport { validateIdToken } from \"../core/id-token\";\r\nimport { isTrustedReturnUrl } from \"../core/security\";\r\nimport {\r\n DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\r\n DEFAULT_REFRESH_TOKEN_COOKIE_NAME,\r\n DEFAULT_ID_TOKEN_COOKIE_NAME,\r\n DEFAULT_STATE_COOKIE_NAME,\r\n DEFAULT_RETURN_COOKIE_NAME,\r\n DEFAULT_VERIFIER_COOKIE_NAME,\r\n getHostCookieOptions,\r\n getSecureCookieOptions,\r\n resolveInsecureLocalDev,\r\n toInsecureCookieName,\r\n} from \"./constants\";\r\n\r\n// ============================================\r\n// 类型定义\r\n// ============================================\r\n\r\nexport interface CallbackRouteConfig {\r\n /** OAuth Client ID */\r\n clientId: string;\r\n\r\n /** SSO 中心地址 */\r\n ssoBaseUrl: string;\r\n\r\n /** 回调 URL(须与注册的 redirect_uri 一致) */\r\n redirectUri: string;\r\n\r\n /**\r\n * OAuth Client Secret(可选)。\r\n * 对于 Confidential Client(BFF/Next.js),应传入 clientSecret\r\n * 以提供第二因素认证。对于 Public Client(SPA),应省略此字段。\r\n */\r\n clientSecret?: string;\r\n\r\n /** 成功回调后重定向的默认路径,默认 \"/\" */\r\n defaultReturnPath?: string;\r\n\r\n /** Access Token Cookie 名称,默认 __Host-nihplod_sso_at */\r\n accessTokenCookieName?: string;\r\n\r\n /** Refresh Token Cookie 名称,默认 __Host-nihplod_sso_rt */\r\n refreshTokenCookieName?: string;\r\n\r\n /** ID Token Cookie 名称,默认 __Host-nihplod_sso_id */\r\n idTokenCookieName?: string;\r\n\r\n /** State Cookie 名称,默认 __Host-nihplod_sso_state */\r\n stateCookieName?: string;\r\n\r\n /** Return URL Cookie 名称,默认 __Host-nihplod_sso_return */\r\n returnUrlCookieName?: string;\r\n\r\n /** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */\r\n verifierCookieName?: string;\r\n\r\n /**\r\n * 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除\r\n * __Host-/__Secure- 前缀;必须与 createSsoMiddleware 的配置保持一致,\r\n * 否则读不到 middleware 写入的 state/verifier Cookie。生产严禁启用——\r\n * 生产环境(NODE_ENV=production 且 ssoBaseUrl 为 https)下该开关会被\r\n * 强制忽略并告警。\r\n */\r\n insecureLocalDev?: boolean;\r\n}\r\n\r\n// ============================================\r\n// 工具函数\r\n// ============================================\r\n\r\n// returnUrl 开放重定向校验统一使用 ../core/security 的 isTrustedReturnUrl\r\n\r\n// ID Token 预校验逻辑已收敛到 ../core/id-token.ts\r\n\r\n// ============================================\r\n// Route Handler 工厂函数\r\n// ============================================\r\n\r\nexport function createCallbackRouteHandler(config: CallbackRouteConfig) {\r\n const {\r\n clientId,\r\n ssoBaseUrl,\r\n redirectUri,\r\n clientSecret,\r\n defaultReturnPath = \"/\",\r\n insecureLocalDev: insecureLocalDevOpt = false,\r\n } = config;\r\n\r\n // 生产守卫:NODE_ENV=production 且 ssoBaseUrl 为 https 时强制忽略该开关(与 middleware 一致)\r\n const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);\r\n\r\n // insecureLocalDev:与 middleware 一致地去前缀 + 关 Secure,否则读不到 state/verifier\r\n const secureCookies = !insecureLocalDev;\r\n const pickName = (explicit: string | undefined, fallback: string) =>\r\n insecureLocalDev ? toInsecureCookieName(explicit ?? fallback) : explicit ?? fallback;\r\n const accessTokenCookieName = pickName(config.accessTokenCookieName, DEFAULT_ACCESS_TOKEN_COOKIE_NAME);\r\n const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);\r\n const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);\r\n const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);\r\n const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);\r\n const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);\r\n\r\n const normalizedBase = ssoBaseUrl.replace(/\\/+$/, \"\");\r\n\r\n return async function GET(request: NextRequest) {\r\n const { searchParams } = request.nextUrl;\r\n\r\n // 检查错误\r\n const error = searchParams.get(\"error\");\r\n if (error) {\r\n const desc = searchParams.get(\"error_description\") || error;\r\n return NextResponse.json(\r\n { error: \"authorization_failed\", error_description: desc },\r\n { status: 400 }\r\n );\r\n }\r\n\r\n const code = searchParams.get(\"code\");\r\n const returnedState = searchParams.get(\"state\");\r\n\r\n if (!code) {\r\n return NextResponse.json(\r\n {\r\n error: \"invalid_request\",\r\n error_description: \"缺少 authorization code\",\r\n },\r\n { status: 400 }\r\n );\r\n }\r\n\r\n // 验证 state(从 cookie 中读取原始 state,CSRF 必需)\r\n const savedState = request.cookies.get(stateCookieName)?.value;\r\n if (!savedState) {\r\n return NextResponse.json(\r\n {\r\n error: \"invalid_request\",\r\n error_description: \"State 参数缺失,请重新发起授权请求\",\r\n },\r\n { status: 400 }\r\n );\r\n }\r\n if (returnedState !== savedState) {\r\n return NextResponse.json(\r\n {\r\n error: \"invalid_request\",\r\n error_description: \"State 参数不匹配,可能存在 CSRF 攻击\",\r\n },\r\n { status: 400 }\r\n );\r\n }\r\n\r\n // 读取 PKCE code_verifier(middleware 存入的 httpOnly cookie)\r\n const verifier = request.cookies.get(verifierCookieName)?.value;\r\n if (!verifier) {\r\n return NextResponse.json(\r\n {\r\n error: \"invalid_request\",\r\n error_description: \"PKCE verifier 缺失,请重新发起授权请求\",\r\n },\r\n { status: 400 }\r\n );\r\n }\r\n\r\n // 交换 token\r\n const tokenEndpoint = `${normalizedBase}/api/oauth/token`;\r\n const body = new URLSearchParams();\r\n body.set(\"grant_type\", \"authorization_code\");\r\n body.set(\"code\", code);\r\n body.set(\"client_id\", clientId);\r\n body.set(\"redirect_uri\", redirectUri);\r\n body.set(\"code_verifier\", verifier);\r\n if (clientSecret) {\r\n body.set(\"client_secret\", clientSecret);\r\n }\r\n\r\n // 带重试的 token 交换(1 次重试 + 指数退避)\r\n let res: Response | null = null;\r\n let lastError: unknown;\r\n const maxRetries = 1;\r\n\r\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\r\n if (attempt > 0) {\r\n // 指数退避:1s * 2^attempt\r\n await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));\r\n }\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 lastError = null;\r\n break;\r\n } catch (err) {\r\n lastError = err;\r\n if (attempt >= maxRetries) {\r\n return NextResponse.json(\r\n { error: \"server_error\", error_description: \"Token 请求失败,已重试仍不可达\" },\r\n { status: 502 }\r\n );\r\n }\r\n }\r\n }\r\n\r\n if (lastError || !res) {\r\n return NextResponse.json(\r\n { error: \"server_error\", error_description: \"Token 请求失败\" },\r\n { status: 502 }\r\n );\r\n }\r\n\r\n if (!res.ok) {\r\n let errData: Record<string, unknown> = {};\r\n try {\r\n errData = await res.json();\r\n } catch { /* ignore */ }\r\n return NextResponse.json(\r\n {\r\n error: \"token_request_failed\",\r\n error_description:\r\n (errData.error_description as string) || `Token 请求失败: HTTP ${res.status}`,\r\n },\r\n { status: 502 }\r\n );\r\n }\r\n\r\n const tokenData: {\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 } = await res.json();\r\n\r\n // 服务端异常可能省略必要字段;不校验会把字符串 \"undefined\" 写进 cookie\r\n if (!tokenData.access_token || !tokenData.refresh_token) {\r\n return NextResponse.json(\r\n {\r\n error: \"server_error\",\r\n error_description: \"Token 响应缺少 access_token 或 refresh_token\",\r\n },\r\n { status: 502 }\r\n );\r\n }\r\n\r\n // 在设置 Cookie 前预校验 ID Token:防止伪造 token 写入浏览器\r\n if (tokenData.id_token) {\r\n try {\r\n await validateIdToken(\r\n tokenData.id_token,\r\n tokenData.access_token,\r\n normalizedBase,\r\n clientId\r\n );\r\n } catch (err) {\r\n return NextResponse.json(\r\n {\r\n error: \"id_token_invalid\",\r\n error_description: err instanceof Error ? err.message : \"ID Token 验证失败\",\r\n },\r\n { status: 400 }\r\n );\r\n }\r\n }\r\n\r\n // 读取 return URL,并做开放重定向防护\r\n const rawReturnUrl =\r\n request.cookies.get(returnUrlCookieName)?.value || defaultReturnPath;\r\n const returnUrl = isTrustedReturnUrl(rawReturnUrl, request.nextUrl.origin)\r\n ? rawReturnUrl\r\n : \"/\";\r\n\r\n // 重定向并设置 cookie\r\n const response = NextResponse.redirect(new URL(returnUrl, request.url));\r\n\r\n // 设置 access_token cookie (httpOnly, Secure, SameSite=Lax, Path=/)\r\n response.cookies.set(accessTokenCookieName, tokenData.access_token, {\r\n ...getHostCookieOptions(tokenData.expires_in, secureCookies),\r\n });\r\n\r\n // 设置 refresh_token cookie(使用服务端返回的过期时间动态计算)\r\n // 服务端 refresh_token 通常是 30 天,此处使用 expires_in 映射(若响应包含)\r\n // 回退到 30 天默认值\r\n const refreshMaxAge =\r\n (tokenData as Record<string, unknown>).refresh_expires_in != null\r\n ? (tokenData as Record<string, unknown>).refresh_expires_in as number\r\n : 30 * 24 * 60 * 60;\r\n\r\n response.cookies.set(refreshTokenCookieName, tokenData.refresh_token, {\r\n ...getHostCookieOptions(refreshMaxAge, secureCookies),\r\n });\r\n\r\n // 设置 id_token cookie,用于 RP-Initiated Logout 的 id_token_hint\r\n // 使用 ID Token 自身的过期时间(通常 1 小时),而非 refresh token 的 30 天\r\n if (tokenData.id_token) {\r\n let idTokenMaxAge = 3600; // 默认 1 小时\r\n try {\r\n const parts = tokenData.id_token.split(\".\");\r\n if (parts.length === 3) {\r\n const payload = JSON.parse(\r\n Buffer.from(parts[1].replace(/-/g, \"+\").replace(/_/g, \"/\"), \"base64\").toString(\"utf8\")\r\n ) as { exp?: number };\r\n if (payload.exp && typeof payload.exp === \"number\") {\r\n // 已过期(剩余 <= 0)的 id_token 不写入 cookie,避免携带失效凭证登出\r\n idTokenMaxAge = payload.exp - Math.floor(Date.now() / 1000);\r\n }\r\n }\r\n } catch {\r\n // 解码失败,使用默认值\r\n }\r\n if (idTokenMaxAge > 0) {\r\n response.cookies.set(idTokenCookieName, tokenData.id_token, {\r\n ...getHostCookieOptions(idTokenMaxAge, secureCookies),\r\n });\r\n }\r\n }\r\n\r\n // 清除临时 cookies: state / return URL\r\n response.cookies.set(stateCookieName, \"\", getHostCookieOptions(0, secureCookies));\r\n response.cookies.set(returnUrlCookieName, \"\", getHostCookieOptions(0, secureCookies));\r\n\r\n // 清除 PKCE verifier cookie,必须使用写入时的 path(callbackPath)\r\n // 由于 callback handler 不知道 middleware 的 callbackPath,这里保守地\r\n // 同时清除 path=/ 和 path=当前请求路径两种可能\r\n response.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, \"/\", secureCookies));\r\n response.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, request.nextUrl.pathname, secureCookies));\r\n\r\n return response;\r\n };\r\n}\r\n","/**\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 // 保留:当前 SDK 不主动抛出,预留给服务端错误透传/未来版本使用\r\n | \"authorization_code_used\"\r\n | \"client_disabled\"\r\n | \"user_denied_authorization\"\r\n // 保留:当前 SDK 不主动抛出,预留给服务端错误透传/未来版本使用\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 // 保留:当前 SDK 不主动抛出(HS256 一律拒绝,见 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 区分。当前 SDK 内部不抛出此类型(统一使用 SsoError +\r\n * mapOAuthErrorToSsoCode 映射),作为公共类型导出供子项目自行解析 OAuth 响应时使用。\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","/**\n * 安全相关小工具:returnUrl 开放重定向校验、常量时间字符串比较\n */\n\n/**\n * 校验 returnUrl 是否可信(防开放重定向)。\n * 仅允许:相对路径(且不以 // 开头)或与 currentOrigin 完全同源的绝对 URL。\n * 拒绝一切含反斜杠 \\ 的值:浏览器会把 \"/\\evil.com\" 归一化为 \"//evil.com\",\n * 形成协议相对 URL 开放重定向。\n */\nexport function isTrustedReturnUrl(url: string, currentOrigin: string): boolean {\n if (!url) return false;\n if (url.includes(\"\\\\\")) 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 必需且不得过期(允许 60s clock skew,与 iat 校验对称)\n if (typeof payload.exp !== \"number\") {\n throw new SsoError(\"id_token_invalid\", \"ID Token 缺少 exp 声明\");\n }\n if (Date.now() >= payload.exp * 1000 + 60_000) {\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","/**\n * App Router 登出 Route Handler\n *\n * 在 /api/auth/logout 路由中处理本地登出 + RP-Initiated Logout:\n * 1. 从 cookie 读取 refresh_token 并调用 revocation_endpoint 撤销\n * 2. 清除所有 SSO cookie\n * 3. 可选重定向到 SSO 中心登出页\n *\n * 用法 (src/app/api/auth/logout/route.ts):\n * ```ts\n * import { createLogoutRouteHandler } from \"@nihplod/sso-sdk/next\";\n *\n * const handler = createLogoutRouteHandler({\n * clientId: \"my-app\",\n * clientSecret: \"optional-secret\",\n * ssoBaseUrl: \"https://nihplod.cn\",\n * redirectUri: \"https://myapp.com/api/auth/callback\",\n * postLogoutRedirectUri: \"https://myapp.com/\",\n * });\n *\n * // 推荐使用 POST 触发登出(防登出 CSRF),GET 保留用于兼容 <a> 标签跳转\n * export const GET = handler;\n * export const POST = handler;\n * ```\n */\n\nimport { NextRequest, NextResponse } from \"next/server\";\nimport {\n DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\n DEFAULT_REFRESH_TOKEN_COOKIE_NAME,\n DEFAULT_ID_TOKEN_COOKIE_NAME,\n DEFAULT_STATE_COOKIE_NAME,\n DEFAULT_RETURN_COOKIE_NAME,\n DEFAULT_VERIFIER_COOKIE_NAME,\n DEFAULT_LOGOUT_STATE_COOKIE_NAME,\n getHostCookieOptions,\n getSecureCookieOptions,\n resolveInsecureLocalDev,\n toInsecureCookieName,\n} from \"./constants\";\n\n// ============================================\n// 类型定义\n// ============================================\n\nexport interface LogoutRouteConfig {\n /** OAuth Client ID */\n clientId: string;\n\n /** SSO 中心地址 */\n ssoBaseUrl: string;\n\n /** 回调 URL(与注册 redirect_uri 一致,用于构造 post_logout_redirect_uri) */\n redirectUri: string;\n\n /**\n * OAuth Client Secret(可选)。\n * 撤销 refresh_token 时,Confidential Client 需要认证。\n */\n clientSecret?: string;\n\n /** 登出后跳转回子项目的地址,默认取 redirectUri 的 origin */\n postLogoutRedirectUri?: string;\n\n /** 是否重定向到 SSO 中心登出页,默认 true */\n redirectToSso?: boolean;\n\n /** Access Token Cookie 名称 */\n accessTokenCookieName?: string;\n\n /** Refresh Token Cookie 名称 */\n refreshTokenCookieName?: string;\n\n /** ID Token Cookie 名称,默认 __Host-nihplod_sso_id */\n idTokenCookieName?: string;\n\n /** State Cookie 名称 */\n stateCookieName?: string;\n\n /** Return URL Cookie 名称 */\n returnUrlCookieName?: string;\n\n /** PKCE Verifier Cookie 名称 */\n verifierCookieName?: string;\n\n /** 回调路径(用于清除 verifier cookie),默认 \"/api/auth/callback\" */\n callbackPath?: string;\n\n /** Logout State Cookie 名称(RP-Initiated Logout CSRF 防护),默认 __Host-nihplod_sso_logout_state */\n logoutStateCookieName?: string;\n\n /**\n * 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除\n * __Host-/__Secure- 前缀;必须与 middleware / callback 的配置保持一致,\n * 否则无法清除它们写入的 Cookie。生产严禁启用——生产环境\n * (NODE_ENV=production 且 ssoBaseUrl 为 https)下该开关会被强制忽略并告警。\n */\n insecureLocalDev?: boolean;\n}\n\ninterface OidcDiscovery {\n issuer: string;\n end_session_endpoint?: string;\n revocation_endpoint?: string;\n [key: string]: unknown;\n}\n\n// ============================================\n// 工具函数\n// ============================================\n\nasync function fetchDiscovery(ssoBaseUrl: string): Promise<OidcDiscovery | null> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 5000);\n try {\n const res = await fetch(\n `${ssoBaseUrl}/api/oauth/.well-known/openid-configuration`,\n { signal: controller.signal }\n );\n if (!res.ok) return null;\n return (await res.json()) as OidcDiscovery;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\n/** 生成安全随机字符串(用于 logout state,Node/Edge Runtime 均支持 Web Crypto) */\nfunction generateRandomString(length: number): string {\n const chars =\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\n const maxValid = Math.floor(256 / chars.length) * chars.length;\n let result = \"\";\n while (result.length < length) {\n const array = new Uint8Array(length * 2);\n crypto.getRandomValues(array);\n for (let i = 0; i < array.length && result.length < length; i++) {\n if (array[i] >= maxValid) continue;\n result += chars[array[i] % chars.length];\n }\n }\n return result;\n}\n\n// ============================================\n// Route Handler 工厂函数\n// ============================================\n\nexport function createLogoutRouteHandler(config: LogoutRouteConfig) {\n const {\n clientId,\n ssoBaseUrl,\n redirectUri,\n clientSecret,\n postLogoutRedirectUri = new URL(redirectUri).origin + \"/\",\n redirectToSso = true,\n callbackPath = \"/api/auth/callback\",\n insecureLocalDev: insecureLocalDevOpt = false,\n } = config;\n\n // 生产守卫:NODE_ENV=production 且 ssoBaseUrl 为 https 时强制忽略该开关(与 middleware/callback 一致)\n const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);\n\n // insecureLocalDev:与 middleware/callback 一致地去前缀 + 关 Secure\n const secureCookies = !insecureLocalDev;\n const pickName = (explicit: string | undefined, fallback: string) =>\n insecureLocalDev ? toInsecureCookieName(explicit ?? fallback) : explicit ?? fallback;\n const accessTokenCookieName = pickName(config.accessTokenCookieName, DEFAULT_ACCESS_TOKEN_COOKIE_NAME);\n const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);\n const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);\n const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);\n const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);\n const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);\n const logoutStateCookieName = pickName(config.logoutStateCookieName, DEFAULT_LOGOUT_STATE_COOKIE_NAME);\n\n const normalizedBase = ssoBaseUrl.replace(/\\/+$/, \"\");\n\n /**\n * 登出 handler:同时适配 GET 与 POST(函数本身不区分 method,\n * 在 route.ts 中 `export const GET = handler; export const POST = handler;` 即可)。\n *\n * ⚠️ CSRF 注意:GET 请求可被跨站触发(如 `<img src=\"/api/auth/logout\">`)。\n * 推荐做法:在 UI 层使用 POST(fetch/form)触发登出;\n * GET 方法保留仅为兼容 <a> 标签跳转与 RP-Initiated Logout 回跳。\n *\n * 当请求携带 state 参数时,视为 RP-Initiated Logout 的回跳(post_logout_redirect_uri\n * 指向本路由的场景),校验 logout state cookie 后放行,防止伪造回跳。\n */\n return async function handler(request: NextRequest) {\n // RP-Initiated Logout 回跳:校验 state(CSRF 防护)\n const returnedState = request.nextUrl.searchParams.get(\"state\");\n if (returnedState) {\n const savedState = request.cookies.get(logoutStateCookieName)?.value;\n if (!savedState || savedState !== returnedState) {\n return NextResponse.json(\n { error: \"invalid_request\", error_description: \"Logout state 不匹配\" },\n { status: 400 }\n );\n }\n const res = NextResponse.redirect(request.nextUrl.origin + \"/\");\n res.cookies.set(logoutStateCookieName, \"\", getHostCookieOptions(0, secureCookies));\n return res;\n }\n\n const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;\n const idTokenHint = request.cookies.get(idTokenCookieName)?.value;\n\n // 1. best-effort 撤销服务端 refresh_token\n if (refreshToken) {\n try {\n const discovery = await fetchDiscovery(normalizedBase);\n const revokeUrl =\n discovery?.revocation_endpoint || `${normalizedBase}/api/oauth/revoke`;\n const body = new URLSearchParams({\n token: refreshToken,\n token_type_hint: \"refresh_token\",\n client_id: clientId,\n });\n if (clientSecret) {\n body.set(\"client_secret\", clientSecret);\n }\n await fetch(revokeUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n } catch {\n // 撤销失败不影响本地登出\n }\n }\n\n // 2. 准备本地清除 SSO cookie 的响应\n const clearCookies = (res: NextResponse) => {\n res.cookies.set(accessTokenCookieName, \"\", getHostCookieOptions(0, secureCookies));\n res.cookies.set(refreshTokenCookieName, \"\", getHostCookieOptions(0, secureCookies));\n res.cookies.set(idTokenCookieName, \"\", getHostCookieOptions(0, secureCookies));\n res.cookies.set(stateCookieName, \"\", getHostCookieOptions(0, secureCookies));\n res.cookies.set(returnUrlCookieName, \"\", getHostCookieOptions(0, secureCookies));\n res.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, \"/\", secureCookies));\n res.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, callbackPath, secureCookies));\n return res;\n };\n\n // 3. 若需要 RP-Initiated Logout,重定向到 SSO 中心,同时必须清除本地 Cookie\n if (redirectToSso) {\n const discovery = await fetchDiscovery(normalizedBase);\n const endSessionEndpoint =\n discovery?.end_session_endpoint || `${normalizedBase}/api/oauth/end-session`;\n const logoutUrl = new URL(endSessionEndpoint);\n logoutUrl.searchParams.set(\"client_id\", clientId);\n logoutUrl.searchParams.set(\n \"post_logout_redirect_uri\",\n postLogoutRedirectUri\n );\n if (idTokenHint) {\n logoutUrl.searchParams.set(\"id_token_hint\", idTokenHint);\n }\n // 携带 state 防登出 CSRF:回跳时校验(见 handler 开头)\n const logoutState = generateRandomString(32);\n logoutUrl.searchParams.set(\"state\", logoutState);\n const res = clearCookies(NextResponse.redirect(logoutUrl.toString()));\n res.cookies.set(logoutStateCookieName, logoutState, getHostCookieOptions(600, secureCookies));\n return res;\n }\n\n return clearCookies(\n NextResponse.redirect(request.nextUrl.origin + \"/\")\n );\n };\n}\n"],"mappings":"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","names":["NextResponse","NextResponse","NextResponse","generateRandomString","NextResponse"]}
|
|
1
|
+
{"version":3,"sources":["../../src/next/middleware.ts","../../src/next/constants.ts","../../src/next/callback.ts","../../src/core/errors.ts","../../src/core/security.ts","../../src/core/id-token.ts","../../src/next/logout.ts"],"sourcesContent":["/**\r\n * Next.js Middleware 辅助函数\r\n *\r\n * 在 Edge Runtime 中运行,自动检测未认证请求并重定向到 SSO 登录页。\r\n *\r\n * ⚠️ Edge Runtime 限制:\r\n * - 不能使用 Node.js crypto 模块\r\n * - PKCE code_challenge 使用 Web Crypto API 的 crypto.subtle.digest(SHA-256)\r\n * (Edge Runtime 18+ 完全支持此 API)\r\n *\r\n * 用法 (src/middleware.ts):\r\n * ```ts\r\n * import { createSsoMiddleware } from \"@nihplod/sso-sdk/next\";\r\n *\r\n * export const middleware = createSsoMiddleware({\r\n * clientId: \"my-app\",\r\n * ssoBaseUrl: \"https://nihplod.cn\",\r\n * redirectUri: \"https://myapp.com/api/auth/callback\",\r\n * publicPaths: [\"/\", \"/public\", \"/api/public\"],\r\n * });\r\n * ```\r\n */\r\n\r\nimport { NextRequest, NextResponse } from \"next/server\";\r\nimport {\r\n DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\r\n DEFAULT_STATE_COOKIE_NAME,\r\n DEFAULT_RETURN_COOKIE_NAME,\r\n DEFAULT_VERIFIER_COOKIE_NAME,\r\n getHostCookieOptions,\r\n getSecureCookieOptions,\r\n resolveInsecureLocalDev,\r\n toInsecureCookieName,\r\n} from \"./constants\";\r\n\r\n// ============================================\r\n// 类型定义\r\n// ============================================\r\n\r\nexport interface SsoMiddlewareConfig {\r\n /** OAuth Client ID */\r\n clientId: string;\r\n\r\n /** SSO 中心地址 */\r\n ssoBaseUrl: string;\r\n\r\n /** 回调 URL(须与注册的 redirect_uri 一致) */\r\n redirectUri: string;\r\n\r\n /** 请求的 scope(空格分隔),默认 \"openid profile\" */\r\n scopes?: string;\r\n\r\n /** 公开路由前缀(不需要认证) */\r\n publicPaths?: string[];\r\n\r\n /** 回调路径(不触发重定向),默认 \"/api/auth/callback\" */\r\n callbackPath?: string;\r\n\r\n /**\r\n * 主站用户会话 Cookie 名称,用于检测是否已有 SSO 会话。\r\n * 默认 \"__Host-user_token\"(与主站 C 端登录 Cookie 一致)。\r\n */\r\n ssoCookieName?: string;\r\n\r\n /**\r\n * OAuth Client Secret(Confidential Client 使用)\r\n * 浏览器端 Public Client 应省略。\r\n */\r\n clientSecret?: string;\r\n\r\n /**\r\n * 是否对 ssoCookieName 对应的主站 Cookie 进行 Introspection 二次验证。\r\n * 默认 true(推荐)。设为 false 时仅检查 Cookie 存在性,延迟最低但可能放行\r\n * 已失效/被撤销的会话 —— 中间件本质上只是 UX 层,敏感数据必须在\r\n * Route Handler / Server Component 中二次校验。\r\n */\r\n validateSsoCookie?: boolean;\r\n\r\n /** Access Token Cookie 名称,默认 __Host-nihplod_sso_at */\r\n accessTokenCookieName?: string;\r\n\r\n /** State Cookie 名称,默认 __Host-nihplod_sso_state */\r\n stateCookieName?: string;\r\n\r\n /** Return URL Cookie 名称,默认 __Host-nihplod_sso_return */\r\n returnUrlCookieName?: string;\r\n\r\n /** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */\r\n verifierCookieName?: string;\r\n\r\n /**\r\n * 本地 HTTP 开发模式(默认 false)。\r\n *\r\n * ⚠️ 仅限 http://localhost 开发:关闭 Cookie 的 Secure 属性并去除\r\n * __Host-/__Secure- 前缀(浏览器拒绝在 HTTP 下写入带这两个前缀的 Cookie,\r\n * 否则会出现「登录后 cookie 写不进去 → middleware 永远判定未登录 →\r\n * 反复跳 SSO」的无限重定向)。开启时启动告警;生产环境严禁启用——\r\n * 生产环境(NODE_ENV=production 且 ssoBaseUrl 为 https)下该开关会被\r\n * 强制忽略并告警,仍走 secure cookie。\r\n * middleware / callback / logout 三处配置需保持一致。\r\n */\r\n insecureLocalDev?: boolean;\r\n}\r\n\r\n// ============================================\r\n// 工具函数\r\n// ============================================\r\n\r\n/**\r\n * 生成安全随机字符串(Edge Runtime 兼容)\r\n * 使用 crypto.getRandomValues(Edge Runtime 支持)\r\n * 拒绝采样法保证 charset 均匀分布\r\n */\r\nfunction generateRandomString(length: number): string {\r\n const chars =\r\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\r\n const maxValid = Math.floor(256 / chars.length) * chars.length;\r\n let result = \"\";\r\n\r\n while (result.length < length) {\r\n const array = new Uint8Array(length * 2);\r\n crypto.getRandomValues(array);\r\n for (let i = 0; i < array.length && result.length < length; i++) {\r\n if (array[i] >= maxValid) continue;\r\n result += chars[array[i] % chars.length];\r\n }\r\n }\r\n return result;\r\n}\r\n\r\n/**\r\n * 计算 PKCE S256 code_challenge(Edge Runtime 兼容)\r\n *\r\n * 使用 Web Crypto API 的 crypto.subtle.digest(\"SHA-256\"),\r\n * Edge Runtime 18+ 完全支持。\r\n */\r\nasync function computeCodeChallenge(verifier: string): Promise<string> {\r\n const encoder = new TextEncoder();\r\n const data = encoder.encode(verifier);\r\n const hash = await crypto.subtle.digest(\"SHA-256\", data);\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 * 路径匹配(支持 /:path* 通配符)\r\n */\r\nfunction matchesPath(pathname: string, paths: string[]): boolean {\r\n return paths.some((path) => {\r\n if (pathname === path) return true;\r\n if (path.endsWith(\"/:path*\") && pathname.startsWith(path.replace(\"/:path*\", \"\"))) return true;\r\n if (pathname.startsWith(path + \"/\")) return true;\r\n return false;\r\n });\r\n}\r\n\r\n/**\r\n * 通过主站 Introspection 端点校验 access_token 是否仍有效。\r\n * Confidential Client(BFF)应配置 clientSecret,防止伪造 Cookie 绕过。\r\n *\r\n * 附带进程级缓存(key = token 的 SHA-256 hex + clientId),\r\n * 减少同一用户 session 在短时间内对 SSO 中心的重复 introspection 调用。\r\n * 缓存带 TTL(30s)与容量上限(LRU 淘汰),避免无界增长。\r\n */\r\nconst introspectionCache = new Map<string, { active: boolean; until: number }>();\r\nconst INTROSPECT_CACHE_TTL_MS = 30_000; // 30 秒\r\nconst INTROSPECT_CACHE_MAX_ENTRIES = 500;\r\n\r\n/** 计算 token 的 SHA-256 hex(Edge Runtime Web Crypto),用作缓存 key 避免跨用户碰撞 */\r\nasync function introspectCacheKey(token: string, clientId: string): Promise<string> {\r\n const hash = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(token));\r\n const hex = Array.from(new Uint8Array(hash))\r\n .map((b) => b.toString(16).padStart(2, \"0\"))\r\n .join(\"\");\r\n return `${hex}|${clientId}`;\r\n}\r\n\r\n/** 读取缓存(命中时刷新插入顺序,实现 LRU) */\r\nfunction introspectCacheGet(key: string): { active: boolean; until: number } | null {\r\n const entry = introspectionCache.get(key);\r\n if (!entry) return null;\r\n if (entry.until <= Date.now()) {\r\n introspectionCache.delete(key);\r\n return null;\r\n }\r\n // LRU:重新插入到 Map 尾部\r\n introspectionCache.delete(key);\r\n introspectionCache.set(key, entry);\r\n return entry;\r\n}\r\n\r\n/** 写入缓存(先清过期项,再按插入顺序淘汰最旧项) */\r\nfunction introspectCacheSet(key: string, active: boolean): void {\r\n const now = Date.now();\r\n for (const [k, v] of introspectionCache) {\r\n if (v.until <= now) introspectionCache.delete(k);\r\n }\r\n while (introspectionCache.size >= INTROSPECT_CACHE_MAX_ENTRIES) {\r\n const oldest = introspectionCache.keys().next();\r\n if (oldest.done) break;\r\n introspectionCache.delete(oldest.value);\r\n }\r\n introspectionCache.set(key, { active, until: now + INTROSPECT_CACHE_TTL_MS });\r\n}\r\n\r\nasync function introspectAccessToken(\r\n token: string,\r\n ssoBaseUrl: string,\r\n clientId: string,\r\n clientSecret?: string\r\n): Promise<boolean> {\r\n const cacheKey = await introspectCacheKey(token, clientId);\r\n const cached = introspectCacheGet(cacheKey);\r\n if (cached) {\r\n return cached.active;\r\n }\r\n\r\n try {\r\n const body = new URLSearchParams({\r\n token,\r\n token_type_hint: \"access_token\",\r\n client_id: clientId,\r\n });\r\n if (clientSecret) {\r\n body.set(\"client_secret\", clientSecret);\r\n }\r\n\r\n const res = await fetch(`${ssoBaseUrl}/api/oauth/introspect`, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\r\n body: body.toString(),\r\n });\r\n\r\n if (!res.ok) {\r\n // 仅缓存确定性的\"token 无效\"结果;网络错误不缓存,下次重试\r\n if (res.status !== 401 && res.status !== 403) {\r\n // 非认证错误(如 500、网络问题):不缓存,让下次请求重试\r\n return false;\r\n }\r\n introspectCacheSet(cacheKey, false);\r\n return false;\r\n }\r\n const data = (await res.json()) as { active?: boolean };\r\n const active = data.active === true;\r\n introspectCacheSet(cacheKey, active);\r\n return active;\r\n } catch {\r\n // 网络异常不缓存:下次请求会重试 introspection\r\n return false;\r\n }\r\n}\r\n\r\n// ============================================\r\n// Middleware 工厂函数\r\n// ============================================\r\n\r\nexport function createSsoMiddleware(config: SsoMiddlewareConfig) {\r\n const {\r\n clientId,\r\n clientSecret,\r\n ssoBaseUrl,\r\n redirectUri,\r\n scopes = \"openid profile\",\r\n publicPaths = [],\r\n callbackPath = \"/api/auth/callback\",\r\n ssoCookieName = \"__Host-user_token\",\r\n validateSsoCookie = true,\r\n insecureLocalDev: insecureLocalDevOpt = false,\r\n } = config;\r\n\r\n // 生产守卫:NODE_ENV=production 且 ssoBaseUrl 为 https 时强制忽略该开关(仍走 secure cookie)\r\n const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);\r\n\r\n // insecureLocalDev:HTTP 本地开发下去除 Cookie 前缀(浏览器拒绝无 Secure 的前缀 Cookie)\r\n const secureCookies = !insecureLocalDev;\r\n const accessTokenCookieName = insecureLocalDev\r\n ? toInsecureCookieName(config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME)\r\n : config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME;\r\n const stateCookieName = insecureLocalDev\r\n ? toInsecureCookieName(config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME)\r\n : config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME;\r\n const returnUrlCookieName = insecureLocalDev\r\n ? toInsecureCookieName(config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME)\r\n : config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME;\r\n const verifierCookieName = insecureLocalDev\r\n ? toInsecureCookieName(config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME)\r\n : config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME;\r\n\r\n // 规范化 ssoBaseUrl\r\n const normalizedBase = ssoBaseUrl.replace(/\\/+$/, \"\");\r\n\r\n if (process.env.NODE_ENV !== \"production\") {\r\n if (!validateSsoCookie) {\r\n console.warn(\r\n \"[SSO SDK] validateSsoCookie=false:中间件仅检查 Cookie 存在性,可能放行已失效的会话。\" +\r\n \"中间件只是 UX 层,敏感数据的鉴权必须在 Route Handler / Server Component 中完成。\"\r\n );\r\n }\r\n if (!clientSecret) {\r\n console.warn(\r\n \"[SSO SDK] 未配置 clientSecret(Public Client 模式):introspection 无客户端认证,\" +\r\n \"中间件判定结果仅作 UX 参考。Confidential Client(BFF)请配置 clientSecret。\"\r\n );\r\n }\r\n }\r\n\r\n return async function ssoMiddleware(request: NextRequest) {\r\n const { pathname } = request.nextUrl;\r\n\r\n // Exclude Next.js internal routes and static assets\r\n if (\r\n pathname.startsWith(\"/_next/\") ||\r\n pathname.startsWith(\"/favicon.ico\") ||\r\n pathname.match(/\\.(ico|png|jpg|jpeg|svg|css|js|woff2?)$/)\r\n ) {\r\n return NextResponse.next();\r\n }\r\n\r\n // Callback path: let the callback handler deal with it\r\n if (pathname === callbackPath) {\r\n return NextResponse.next();\r\n }\r\n\r\n // Public paths: no auth required\r\n const allPublicPaths = [callbackPath, ...publicPaths];\r\n if (matchesPath(pathname, allPublicPaths)) {\r\n return NextResponse.next();\r\n }\r\n\r\n // Check for existing SSO session\r\n const ssoSession = request.cookies.get(ssoCookieName);\r\n if (ssoSession?.value) {\r\n // 可选:对主站 Cookie 进行 Introspection 二次验证\r\n if (validateSsoCookie) {\r\n const tokenActive = await introspectAccessToken(\r\n ssoSession.value,\r\n normalizedBase,\r\n clientId,\r\n clientSecret\r\n );\r\n if (tokenActive) {\r\n return NextResponse.next();\r\n }\r\n // Token 无效:继续到 access_token cookie 检查或重定向\r\n } else {\r\n return NextResponse.next();\r\n }\r\n }\r\n\r\n // Check for access_token in cookie (set by callback handler)\r\n const accessTokenCookie = request.cookies.get(accessTokenCookieName);\r\n if (accessTokenCookie?.value) {\r\n // 调用 Introspection 精确校验 token 是否仍有效。\r\n // Confidential Client 携带 clientSecret;Public Client 仅传 clientId。\r\n const tokenActive = await introspectAccessToken(\r\n accessTokenCookie.value,\r\n normalizedBase,\r\n clientId,\r\n clientSecret\r\n );\r\n\r\n if (tokenActive) {\r\n return NextResponse.next();\r\n }\r\n // Token 无效或已过期:清除 cookie 并继续到 SSO 重定向\r\n }\r\n\r\n // No auth: redirect to SSO\r\n const state = generateRandomString(32);\r\n const verifier = generateRandomString(64);\r\n\r\n // 在 Edge Runtime 中计算 PKCE code_challenge(SHA-256 + base64url)\r\n const challenge = await computeCodeChallenge(verifier);\r\n\r\n const authorizeParams = new URLSearchParams();\r\n authorizeParams.set(\"response_type\", \"code\");\r\n authorizeParams.set(\"client_id\", clientId);\r\n authorizeParams.set(\"redirect_uri\", redirectUri);\r\n authorizeParams.set(\"scope\", scopes);\r\n authorizeParams.set(\"state\", state);\r\n authorizeParams.set(\"code_challenge\", challenge);\r\n authorizeParams.set(\"code_challenge_method\", \"S256\");\r\n\r\n const loginUrl = new URL(\"/api/oauth/authorize\", normalizedBase);\r\n loginUrl.search = authorizeParams.toString();\r\n\r\n const response = NextResponse.redirect(loginUrl);\r\n\r\n // Set state cookie for CSRF verification on callback\r\n response.cookies.set(stateCookieName, state, getHostCookieOptions(600, secureCookies));\r\n\r\n // Set PKCE verifier cookie(httpOnly,供 callback handler 使用)\r\n // 使用 __Secure- 前缀,允许写入 callbackPath\r\n response.cookies.set(verifierCookieName, verifier, getSecureCookieOptions(600, callbackPath, secureCookies));\r\n\r\n // Set return URL cookie(仅存储 pathname + search,截断至安全长度)\r\n const safeReturnUrl = (request.nextUrl.pathname + request.nextUrl.search).slice(0, 2048);\r\n response.cookies.set(returnUrlCookieName, safeReturnUrl, getHostCookieOptions(600, secureCookies));\r\n\r\n // 如果存在过期的 access_token cookie,立即清除\r\n if (accessTokenCookie?.value) {\r\n response.cookies.set(accessTokenCookieName, \"\", getHostCookieOptions(0, secureCookies));\r\n }\r\n\r\n return response;\r\n };\r\n}\r\n","/**\n * Next.js 集成默认 Cookie 名称\n *\n * 除 verifier 外统一使用 __Host- 前缀,要求:\n * - Secure\n * - Path=/\n * - 无 Domain 属性\n *\n * verifier 需要写入回调路径(非 /),因此使用 __Secure- 前缀。\n */\n\nexport const DEFAULT_ACCESS_TOKEN_COOKIE_NAME = \"__Host-nihplod_sso_at\";\nexport const DEFAULT_REFRESH_TOKEN_COOKIE_NAME = \"__Host-nihplod_sso_rt\";\nexport const DEFAULT_ID_TOKEN_COOKIE_NAME = \"__Host-nihplod_sso_id\";\nexport const DEFAULT_STATE_COOKIE_NAME = \"__Host-nihplod_sso_state\";\nexport const DEFAULT_RETURN_COOKIE_NAME = \"__Host-nihplod_sso_return\";\n// Verifier 需要写入回调路径(非 /),因此使用 __Secure- 前缀而非 __Host-\nexport const DEFAULT_VERIFIER_COOKIE_NAME = \"__Secure-nihplod_sso_verifier\";\n// RP-Initiated Logout 的 state cookie(登出 CSRF 防护)\nexport const DEFAULT_LOGOUT_STATE_COOKIE_NAME = \"__Host-nihplod_sso_logout_state\";\n\n/**\n * insecureLocalDev 场景:去除 __Host-/__Secure- 前缀。\n * 浏览器强制要求带这两个前缀的 Cookie 必须设置 Secure,\n * HTTP 本地开发时若保留前缀,即使 secure=false 也会被拒绝写入。\n */\nexport function toInsecureCookieName(name: string): string {\n return name.replace(/^__(Host|Secure)-/, \"\");\n}\n\n/**\n * 解析 insecureLocalDev 开关并输出告警(middleware / callback / logout 三处共用,保证行为一致)。\n *\n * 生产守卫:当 NODE_ENV=production 且 ssoBaseUrl 为 https 时强制忽略该开关,\n * Cookie 仍使用 Secure 属性并保留 __Host-/__Secure- 前缀,\n * 避免误配置把生产环境的 Cookie 降级为 HTTP 可写。\n *\n * 返回值为实际生效的 insecureLocalDev 状态。\n */\nexport function resolveInsecureLocalDev(enabled: boolean, ssoBaseUrl: string): boolean {\n if (!enabled) return false;\n if (process.env.NODE_ENV === \"production\" && /^https:\\/\\//i.test(ssoBaseUrl)) {\n console.warn(\n \"[SSO SDK] insecureLocalDev=true 已被忽略:当前为生产环境(NODE_ENV=production)且 ssoBaseUrl 使用 HTTPS,\" +\n \"Cookie 仍保持 Secure 属性与 __Host-/__Secure- 前缀。请从生产配置中移除该开关。\"\n );\n return false;\n }\n console.warn(\n \"[SSO SDK] insecureLocalDev=true:Cookie 的 Secure 属性已关闭且去除 __Host-/__Secure- 前缀。\" +\n \"仅限 http://localhost 本地开发使用,生产环境必须移除该配置(生产必须用 HTTPS)。\"\n );\n return true;\n}\n\n/** __Host- 前缀 Cookie 的安全选项(Path 必须为 /) */\nexport function getHostCookieOptions(\n maxAge?: number,\n secure: boolean = true\n): {\n httpOnly: true;\n secure: boolean;\n sameSite: \"lax\";\n path: \"/\";\n maxAge?: number;\n} {\n return {\n httpOnly: true,\n secure,\n sameSite: \"lax\",\n path: \"/\",\n ...(maxAge !== undefined ? { maxAge } : {}),\n };\n}\n\n/** __Secure- 前缀 Cookie 的安全选项(允许自定义 Path) */\nexport function getSecureCookieOptions(\n maxAge?: number,\n path = \"/\",\n secure: boolean = true\n): {\n httpOnly: true;\n secure: boolean;\n sameSite: \"lax\";\n path: string;\n maxAge?: number;\n} {\n return {\n httpOnly: true,\n secure,\n sameSite: \"lax\",\n path,\n ...(maxAge !== undefined ? { maxAge } : {}),\n };\n}\n","/**\r\n * App Router 回调 Route Handler\r\n *\r\n * 在 /api/auth/callback 路由中处理 OAuth 回调:\r\n * 1. 验证 state 参数\r\n * 2. 用授权码交换 token\r\n * 3. 设置 session cookie\r\n * 4. 重定向到原始页面\r\n *\r\n * ⚠️ 此 handler 仅运行在 Node.js Runtime(非 Edge),可以使用 crypto.subtle。\r\n *\r\n * 用法 (src/app/api/auth/callback/route.ts):\r\n * ```ts\r\n * import { createCallbackRouteHandler } from \"@nihplod/sso-sdk/next\";\r\n *\r\n * export const GET = createCallbackRouteHandler({\r\n * clientId: \"my-app\",\r\n * ssoBaseUrl: \"https://nihplod.cn\",\r\n * redirectUri: \"https://myapp.com/api/auth/callback\",\r\n * tokenCookieName: \"nihplod_sso_at\",\r\n * });\r\n * ```\r\n */\r\n\r\nimport { NextRequest, NextResponse } from \"next/server\";\r\nimport { validateIdToken } from \"../core/id-token\";\r\nimport { isTrustedReturnUrl } from \"../core/security\";\r\nimport {\r\n DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\r\n DEFAULT_REFRESH_TOKEN_COOKIE_NAME,\r\n DEFAULT_ID_TOKEN_COOKIE_NAME,\r\n DEFAULT_STATE_COOKIE_NAME,\r\n DEFAULT_RETURN_COOKIE_NAME,\r\n DEFAULT_VERIFIER_COOKIE_NAME,\r\n getHostCookieOptions,\r\n getSecureCookieOptions,\r\n resolveInsecureLocalDev,\r\n toInsecureCookieName,\r\n} from \"./constants\";\r\n\r\n// ============================================\r\n// 类型定义\r\n// ============================================\r\n\r\nexport interface CallbackRouteConfig {\r\n /** OAuth Client ID */\r\n clientId: string;\r\n\r\n /** SSO 中心地址 */\r\n ssoBaseUrl: string;\r\n\r\n /** 回调 URL(须与注册的 redirect_uri 一致) */\r\n redirectUri: string;\r\n\r\n /**\r\n * OAuth Client Secret(可选)。\r\n * 对于 Confidential Client(BFF/Next.js),应传入 clientSecret\r\n * 以提供第二因素认证。对于 Public Client(SPA),应省略此字段。\r\n */\r\n clientSecret?: string;\r\n\r\n /** 成功回调后重定向的默认路径,默认 \"/\" */\r\n defaultReturnPath?: string;\r\n\r\n /** Access Token Cookie 名称,默认 __Host-nihplod_sso_at */\r\n accessTokenCookieName?: string;\r\n\r\n /** Refresh Token Cookie 名称,默认 __Host-nihplod_sso_rt */\r\n refreshTokenCookieName?: string;\r\n\r\n /** ID Token Cookie 名称,默认 __Host-nihplod_sso_id */\r\n idTokenCookieName?: string;\r\n\r\n /** State Cookie 名称,默认 __Host-nihplod_sso_state */\r\n stateCookieName?: string;\r\n\r\n /** Return URL Cookie 名称,默认 __Host-nihplod_sso_return */\r\n returnUrlCookieName?: string;\r\n\r\n /** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */\r\n verifierCookieName?: string;\r\n\r\n /**\r\n * 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除\r\n * __Host-/__Secure- 前缀;必须与 createSsoMiddleware 的配置保持一致,\r\n * 否则读不到 middleware 写入的 state/verifier Cookie。生产严禁启用——\r\n * 生产环境(NODE_ENV=production 且 ssoBaseUrl 为 https)下该开关会被\r\n * 强制忽略并告警。\r\n */\r\n insecureLocalDev?: boolean;\r\n}\r\n\r\n// ============================================\r\n// 工具函数\r\n// ============================================\r\n\r\n// returnUrl 开放重定向校验统一使用 ../core/security 的 isTrustedReturnUrl\r\n\r\n// ID Token 预校验逻辑已收敛到 ../core/id-token.ts\r\n\r\n// ============================================\r\n// Route Handler 工厂函数\r\n// ============================================\r\n\r\nexport function createCallbackRouteHandler(config: CallbackRouteConfig) {\r\n const {\r\n clientId,\r\n ssoBaseUrl,\r\n redirectUri,\r\n clientSecret,\r\n defaultReturnPath = \"/\",\r\n insecureLocalDev: insecureLocalDevOpt = false,\r\n } = config;\r\n\r\n // 生产守卫:NODE_ENV=production 且 ssoBaseUrl 为 https 时强制忽略该开关(与 middleware 一致)\r\n const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);\r\n\r\n // insecureLocalDev:与 middleware 一致地去前缀 + 关 Secure,否则读不到 state/verifier\r\n const secureCookies = !insecureLocalDev;\r\n const pickName = (explicit: string | undefined, fallback: string) =>\r\n insecureLocalDev ? toInsecureCookieName(explicit ?? fallback) : explicit ?? fallback;\r\n const accessTokenCookieName = pickName(config.accessTokenCookieName, DEFAULT_ACCESS_TOKEN_COOKIE_NAME);\r\n const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);\r\n const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);\r\n const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);\r\n const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);\r\n const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);\r\n\r\n const normalizedBase = ssoBaseUrl.replace(/\\/+$/, \"\");\r\n\r\n return async function GET(request: NextRequest) {\r\n const { searchParams } = request.nextUrl;\r\n\r\n // 检查错误\r\n const error = searchParams.get(\"error\");\r\n if (error) {\r\n const desc = searchParams.get(\"error_description\") || error;\r\n return NextResponse.json(\r\n { error: \"authorization_failed\", error_description: desc },\r\n { status: 400 }\r\n );\r\n }\r\n\r\n const code = searchParams.get(\"code\");\r\n const returnedState = searchParams.get(\"state\");\r\n\r\n if (!code) {\r\n return NextResponse.json(\r\n {\r\n error: \"invalid_request\",\r\n error_description: \"缺少 authorization code\",\r\n },\r\n { status: 400 }\r\n );\r\n }\r\n\r\n // 验证 state(从 cookie 中读取原始 state,CSRF 必需)\r\n const savedState = request.cookies.get(stateCookieName)?.value;\r\n if (!savedState) {\r\n return NextResponse.json(\r\n {\r\n error: \"invalid_request\",\r\n error_description: \"State 参数缺失,请重新发起授权请求\",\r\n },\r\n { status: 400 }\r\n );\r\n }\r\n if (returnedState !== savedState) {\r\n return NextResponse.json(\r\n {\r\n error: \"invalid_request\",\r\n error_description: \"State 参数不匹配,可能存在 CSRF 攻击\",\r\n },\r\n { status: 400 }\r\n );\r\n }\r\n\r\n // 读取 PKCE code_verifier(middleware 存入的 httpOnly cookie)\r\n const verifier = request.cookies.get(verifierCookieName)?.value;\r\n if (!verifier) {\r\n return NextResponse.json(\r\n {\r\n error: \"invalid_request\",\r\n error_description: \"PKCE verifier 缺失,请重新发起授权请求\",\r\n },\r\n { status: 400 }\r\n );\r\n }\r\n\r\n // 交换 token\r\n const tokenEndpoint = `${normalizedBase}/api/oauth/token`;\r\n const body = new URLSearchParams();\r\n body.set(\"grant_type\", \"authorization_code\");\r\n body.set(\"code\", code);\r\n body.set(\"client_id\", clientId);\r\n body.set(\"redirect_uri\", redirectUri);\r\n body.set(\"code_verifier\", verifier);\r\n if (clientSecret) {\r\n body.set(\"client_secret\", clientSecret);\r\n }\r\n\r\n // 带重试的 token 交换(1 次重试 + 指数退避)\r\n let res: Response | null = null;\r\n let lastError: unknown;\r\n const maxRetries = 1;\r\n\r\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\r\n if (attempt > 0) {\r\n // 指数退避:1s * 2^attempt\r\n await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));\r\n }\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 lastError = null;\r\n break;\r\n } catch (err) {\r\n lastError = err;\r\n if (attempt >= maxRetries) {\r\n return NextResponse.json(\r\n { error: \"server_error\", error_description: \"Token 请求失败,已重试仍不可达\" },\r\n { status: 502 }\r\n );\r\n }\r\n }\r\n }\r\n\r\n if (lastError || !res) {\r\n return NextResponse.json(\r\n { error: \"server_error\", error_description: \"Token 请求失败\" },\r\n { status: 502 }\r\n );\r\n }\r\n\r\n if (!res.ok) {\r\n let errData: Record<string, unknown> = {};\r\n try {\r\n errData = await res.json();\r\n } catch { /* ignore */ }\r\n return NextResponse.json(\r\n {\r\n error: \"token_request_failed\",\r\n error_description:\r\n (errData.error_description as string) || `Token 请求失败: HTTP ${res.status}`,\r\n },\r\n { status: 502 }\r\n );\r\n }\r\n\r\n const tokenData: {\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 } = await res.json();\r\n\r\n // 服务端异常可能省略必要字段;不校验会把字符串 \"undefined\" 写进 cookie\r\n if (!tokenData.access_token || !tokenData.refresh_token) {\r\n return NextResponse.json(\r\n {\r\n error: \"server_error\",\r\n error_description: \"Token 响应缺少 access_token 或 refresh_token\",\r\n },\r\n { status: 502 }\r\n );\r\n }\r\n\r\n // 在设置 Cookie 前预校验 ID Token:防止伪造 token 写入浏览器\r\n if (tokenData.id_token) {\r\n try {\r\n await validateIdToken(\r\n tokenData.id_token,\r\n tokenData.access_token,\r\n normalizedBase,\r\n clientId\r\n );\r\n } catch (err) {\r\n return NextResponse.json(\r\n {\r\n error: \"id_token_invalid\",\r\n error_description: err instanceof Error ? err.message : \"ID Token 验证失败\",\r\n },\r\n { status: 400 }\r\n );\r\n }\r\n }\r\n\r\n // 读取 return URL,并做开放重定向防护。\r\n // 可信 origin 与跳转基准一律取 redirectUri 的 origin(部署时配置的公网地址),\r\n // 而非 request.url / request.nextUrl.origin:Next standalone 部署下后者是进程\r\n // 监听地址(如 http://0.0.0.0:3002),反代场景会把用户重定向到不可达地址。\r\n const callbackOrigin = new URL(redirectUri).origin;\r\n const rawReturnUrl =\r\n request.cookies.get(returnUrlCookieName)?.value || defaultReturnPath;\r\n const returnUrl = isTrustedReturnUrl(rawReturnUrl, callbackOrigin)\r\n ? rawReturnUrl\r\n : \"/\";\r\n\r\n // 重定向并设置 cookie\r\n const response = NextResponse.redirect(new URL(returnUrl, callbackOrigin));\r\n\r\n // 设置 access_token cookie (httpOnly, Secure, SameSite=Lax, Path=/)\r\n response.cookies.set(accessTokenCookieName, tokenData.access_token, {\r\n ...getHostCookieOptions(tokenData.expires_in, secureCookies),\r\n });\r\n\r\n // 设置 refresh_token cookie(使用服务端返回的过期时间动态计算)\r\n // 服务端 refresh_token 通常是 30 天,此处使用 expires_in 映射(若响应包含)\r\n // 回退到 30 天默认值\r\n const refreshMaxAge =\r\n (tokenData as Record<string, unknown>).refresh_expires_in != null\r\n ? (tokenData as Record<string, unknown>).refresh_expires_in as number\r\n : 30 * 24 * 60 * 60;\r\n\r\n response.cookies.set(refreshTokenCookieName, tokenData.refresh_token, {\r\n ...getHostCookieOptions(refreshMaxAge, secureCookies),\r\n });\r\n\r\n // 设置 id_token cookie,用于 RP-Initiated Logout 的 id_token_hint\r\n // 使用 ID Token 自身的过期时间(通常 1 小时),而非 refresh token 的 30 天\r\n if (tokenData.id_token) {\r\n let idTokenMaxAge = 3600; // 默认 1 小时\r\n try {\r\n const parts = tokenData.id_token.split(\".\");\r\n if (parts.length === 3) {\r\n const payload = JSON.parse(\r\n Buffer.from(parts[1].replace(/-/g, \"+\").replace(/_/g, \"/\"), \"base64\").toString(\"utf8\")\r\n ) as { exp?: number };\r\n if (payload.exp && typeof payload.exp === \"number\") {\r\n // 已过期(剩余 <= 0)的 id_token 不写入 cookie,避免携带失效凭证登出\r\n idTokenMaxAge = payload.exp - Math.floor(Date.now() / 1000);\r\n }\r\n }\r\n } catch {\r\n // 解码失败,使用默认值\r\n }\r\n if (idTokenMaxAge > 0) {\r\n response.cookies.set(idTokenCookieName, tokenData.id_token, {\r\n ...getHostCookieOptions(idTokenMaxAge, secureCookies),\r\n });\r\n }\r\n }\r\n\r\n // 清除临时 cookies: state / return URL\r\n response.cookies.set(stateCookieName, \"\", getHostCookieOptions(0, secureCookies));\r\n response.cookies.set(returnUrlCookieName, \"\", getHostCookieOptions(0, secureCookies));\r\n\r\n // 清除 PKCE verifier cookie,必须使用写入时的 path(callbackPath)\r\n // 由于 callback handler 不知道 middleware 的 callbackPath,这里保守地\r\n // 同时清除 path=/ 和 path=当前请求路径两种可能\r\n response.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, \"/\", secureCookies));\r\n response.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, request.nextUrl.pathname, secureCookies));\r\n\r\n return response;\r\n };\r\n}\r\n","/**\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 // 保留:当前 SDK 不主动抛出,预留给服务端错误透传/未来版本使用\r\n | \"authorization_code_used\"\r\n | \"client_disabled\"\r\n | \"user_denied_authorization\"\r\n // 保留:当前 SDK 不主动抛出,预留给服务端错误透传/未来版本使用\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 // 保留:当前 SDK 不主动抛出(HS256 一律拒绝,见 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 区分。当前 SDK 内部不抛出此类型(统一使用 SsoError +\r\n * mapOAuthErrorToSsoCode 映射),作为公共类型导出供子项目自行解析 OAuth 响应时使用。\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","/**\n * 安全相关小工具:returnUrl 开放重定向校验、常量时间字符串比较\n */\n\n/**\n * 校验 returnUrl 是否可信(防开放重定向)。\n * 仅允许:相对路径(且不以 // 开头)或与 currentOrigin 完全同源的绝对 URL。\n * 拒绝一切含反斜杠 \\ 的值:浏览器会把 \"/\\evil.com\" 归一化为 \"//evil.com\",\n * 形成协议相对 URL 开放重定向。\n */\nexport function isTrustedReturnUrl(url: string, currentOrigin: string): boolean {\n if (!url) return false;\n if (url.includes(\"\\\\\")) 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 必需且不得过期(允许 60s clock skew,与 iat 校验对称)\n if (typeof payload.exp !== \"number\") {\n throw new SsoError(\"id_token_invalid\", \"ID Token 缺少 exp 声明\");\n }\n if (Date.now() >= payload.exp * 1000 + 60_000) {\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","/**\n * App Router 登出 Route Handler\n *\n * 在 /api/auth/logout 路由中处理本地登出 + RP-Initiated Logout:\n * 1. 从 cookie 读取 refresh_token 并调用 revocation_endpoint 撤销\n * 2. 清除所有 SSO cookie\n * 3. 可选重定向到 SSO 中心登出页\n *\n * 用法 (src/app/api/auth/logout/route.ts):\n * ```ts\n * import { createLogoutRouteHandler } from \"@nihplod/sso-sdk/next\";\n *\n * const handler = createLogoutRouteHandler({\n * clientId: \"my-app\",\n * clientSecret: \"optional-secret\",\n * ssoBaseUrl: \"https://nihplod.cn\",\n * redirectUri: \"https://myapp.com/api/auth/callback\",\n * postLogoutRedirectUri: \"https://myapp.com/\",\n * });\n *\n * // 推荐使用 POST 触发登出(防登出 CSRF),GET 保留用于兼容 <a> 标签跳转\n * export const GET = handler;\n * export const POST = handler;\n * ```\n */\n\nimport { NextRequest, NextResponse } from \"next/server\";\nimport {\n DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\n DEFAULT_REFRESH_TOKEN_COOKIE_NAME,\n DEFAULT_ID_TOKEN_COOKIE_NAME,\n DEFAULT_STATE_COOKIE_NAME,\n DEFAULT_RETURN_COOKIE_NAME,\n DEFAULT_VERIFIER_COOKIE_NAME,\n DEFAULT_LOGOUT_STATE_COOKIE_NAME,\n getHostCookieOptions,\n getSecureCookieOptions,\n resolveInsecureLocalDev,\n toInsecureCookieName,\n} from \"./constants\";\n\n// ============================================\n// 类型定义\n// ============================================\n\nexport interface LogoutRouteConfig {\n /** OAuth Client ID */\n clientId: string;\n\n /** SSO 中心地址 */\n ssoBaseUrl: string;\n\n /** 回调 URL(与注册 redirect_uri 一致,用于构造 post_logout_redirect_uri) */\n redirectUri: string;\n\n /**\n * OAuth Client Secret(可选)。\n * 撤销 refresh_token 时,Confidential Client 需要认证。\n */\n clientSecret?: string;\n\n /** 登出后跳转回子项目的地址,默认取 redirectUri 的 origin */\n postLogoutRedirectUri?: string;\n\n /** 是否重定向到 SSO 中心登出页,默认 true */\n redirectToSso?: boolean;\n\n /** Access Token Cookie 名称 */\n accessTokenCookieName?: string;\n\n /** Refresh Token Cookie 名称 */\n refreshTokenCookieName?: string;\n\n /** ID Token Cookie 名称,默认 __Host-nihplod_sso_id */\n idTokenCookieName?: string;\n\n /** State Cookie 名称 */\n stateCookieName?: string;\n\n /** Return URL Cookie 名称 */\n returnUrlCookieName?: string;\n\n /** PKCE Verifier Cookie 名称 */\n verifierCookieName?: string;\n\n /** 回调路径(用于清除 verifier cookie),默认 \"/api/auth/callback\" */\n callbackPath?: string;\n\n /** Logout State Cookie 名称(RP-Initiated Logout CSRF 防护),默认 __Host-nihplod_sso_logout_state */\n logoutStateCookieName?: string;\n\n /**\n * 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除\n * __Host-/__Secure- 前缀;必须与 middleware / callback 的配置保持一致,\n * 否则无法清除它们写入的 Cookie。生产严禁启用——生产环境\n * (NODE_ENV=production 且 ssoBaseUrl 为 https)下该开关会被强制忽略并告警。\n */\n insecureLocalDev?: boolean;\n}\n\ninterface OidcDiscovery {\n issuer: string;\n end_session_endpoint?: string;\n revocation_endpoint?: string;\n [key: string]: unknown;\n}\n\n// ============================================\n// 工具函数\n// ============================================\n\nasync function fetchDiscovery(ssoBaseUrl: string): Promise<OidcDiscovery | null> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 5000);\n try {\n const res = await fetch(\n `${ssoBaseUrl}/api/oauth/.well-known/openid-configuration`,\n { signal: controller.signal }\n );\n if (!res.ok) return null;\n return (await res.json()) as OidcDiscovery;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\n/** 生成安全随机字符串(用于 logout state,Node/Edge Runtime 均支持 Web Crypto) */\nfunction generateRandomString(length: number): string {\n const chars =\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\n const maxValid = Math.floor(256 / chars.length) * chars.length;\n let result = \"\";\n while (result.length < length) {\n const array = new Uint8Array(length * 2);\n crypto.getRandomValues(array);\n for (let i = 0; i < array.length && result.length < length; i++) {\n if (array[i] >= maxValid) continue;\n result += chars[array[i] % chars.length];\n }\n }\n return result;\n}\n\n// ============================================\n// Route Handler 工厂函数\n// ============================================\n\nexport function createLogoutRouteHandler(config: LogoutRouteConfig) {\n const {\n clientId,\n ssoBaseUrl,\n redirectUri,\n clientSecret,\n postLogoutRedirectUri = new URL(redirectUri).origin + \"/\",\n redirectToSso = true,\n callbackPath = \"/api/auth/callback\",\n insecureLocalDev: insecureLocalDevOpt = false,\n } = config;\n\n // 生产守卫:NODE_ENV=production 且 ssoBaseUrl 为 https 时强制忽略该开关(与 middleware/callback 一致)\n const insecureLocalDev = resolveInsecureLocalDev(insecureLocalDevOpt, ssoBaseUrl);\n\n // insecureLocalDev:与 middleware/callback 一致地去前缀 + 关 Secure\n const secureCookies = !insecureLocalDev;\n const pickName = (explicit: string | undefined, fallback: string) =>\n insecureLocalDev ? toInsecureCookieName(explicit ?? fallback) : explicit ?? fallback;\n const accessTokenCookieName = pickName(config.accessTokenCookieName, DEFAULT_ACCESS_TOKEN_COOKIE_NAME);\n const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);\n const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);\n const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);\n const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);\n const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);\n const logoutStateCookieName = pickName(config.logoutStateCookieName, DEFAULT_LOGOUT_STATE_COOKIE_NAME);\n\n const normalizedBase = ssoBaseUrl.replace(/\\/+$/, \"\");\n\n // 本地跳转的 origin:取 redirectUri 的 origin 而非 request.nextUrl.origin——\n // standalone 部署下后者是进程监听地址(如 http://0.0.0.0:3002),反代场景不可靠。\n const callbackOrigin = new URL(redirectUri).origin;\n\n /**\n * 登出 handler:同时适配 GET 与 POST(函数本身不区分 method,\n * 在 route.ts 中 `export const GET = handler; export const POST = handler;` 即可)。\n *\n * ⚠️ CSRF 注意:GET 请求可被跨站触发(如 `<img src=\"/api/auth/logout\">`)。\n * 推荐做法:在 UI 层使用 POST(fetch/form)触发登出;\n * GET 方法保留仅为兼容 <a> 标签跳转与 RP-Initiated Logout 回跳。\n *\n * 当请求携带 state 参数时,视为 RP-Initiated Logout 的回跳(post_logout_redirect_uri\n * 指向本路由的场景),校验 logout state cookie 后放行,防止伪造回跳。\n */\n return async function handler(request: NextRequest) {\n // RP-Initiated Logout 回跳:校验 state(CSRF 防护)\n const returnedState = request.nextUrl.searchParams.get(\"state\");\n if (returnedState) {\n const savedState = request.cookies.get(logoutStateCookieName)?.value;\n if (!savedState || savedState !== returnedState) {\n return NextResponse.json(\n { error: \"invalid_request\", error_description: \"Logout state 不匹配\" },\n { status: 400 }\n );\n }\n const res = NextResponse.redirect(callbackOrigin + \"/\");\n res.cookies.set(logoutStateCookieName, \"\", getHostCookieOptions(0, secureCookies));\n return res;\n }\n\n const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;\n const idTokenHint = request.cookies.get(idTokenCookieName)?.value;\n\n // 1. best-effort 撤销服务端 refresh_token\n if (refreshToken) {\n try {\n const discovery = await fetchDiscovery(normalizedBase);\n const revokeUrl =\n discovery?.revocation_endpoint || `${normalizedBase}/api/oauth/revoke`;\n const body = new URLSearchParams({\n token: refreshToken,\n token_type_hint: \"refresh_token\",\n client_id: clientId,\n });\n if (clientSecret) {\n body.set(\"client_secret\", clientSecret);\n }\n await fetch(revokeUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n } catch {\n // 撤销失败不影响本地登出\n }\n }\n\n // 2. 准备本地清除 SSO cookie 的响应\n const clearCookies = (res: NextResponse) => {\n res.cookies.set(accessTokenCookieName, \"\", getHostCookieOptions(0, secureCookies));\n res.cookies.set(refreshTokenCookieName, \"\", getHostCookieOptions(0, secureCookies));\n res.cookies.set(idTokenCookieName, \"\", getHostCookieOptions(0, secureCookies));\n res.cookies.set(stateCookieName, \"\", getHostCookieOptions(0, secureCookies));\n res.cookies.set(returnUrlCookieName, \"\", getHostCookieOptions(0, secureCookies));\n res.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, \"/\", secureCookies));\n res.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, callbackPath, secureCookies));\n return res;\n };\n\n // 3. 若需要 RP-Initiated Logout,重定向到 SSO 中心,同时必须清除本地 Cookie\n if (redirectToSso) {\n const discovery = await fetchDiscovery(normalizedBase);\n const endSessionEndpoint =\n discovery?.end_session_endpoint || `${normalizedBase}/api/oauth/end-session`;\n const logoutUrl = new URL(endSessionEndpoint);\n logoutUrl.searchParams.set(\"client_id\", clientId);\n logoutUrl.searchParams.set(\n \"post_logout_redirect_uri\",\n postLogoutRedirectUri\n );\n if (idTokenHint) {\n logoutUrl.searchParams.set(\"id_token_hint\", idTokenHint);\n }\n // 携带 state 防登出 CSRF:回跳时校验(见 handler 开头)\n const logoutState = generateRandomString(32);\n logoutUrl.searchParams.set(\"state\", logoutState);\n const res = clearCookies(NextResponse.redirect(logoutUrl.toString()));\n res.cookies.set(logoutStateCookieName, logoutState, getHostCookieOptions(600, secureCookies));\n return res;\n }\n\n return clearCookies(\n NextResponse.redirect(callbackOrigin + \"/\")\n );\n };\n}\n"],"mappings":"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","names":["NextResponse","NextResponse","NextResponse","generateRandomString","NextResponse"]}
|
package/dist/react/index.d.mts
CHANGED
|
@@ -68,7 +68,12 @@ interface SsoUser {
|
|
|
68
68
|
sub: string;
|
|
69
69
|
nickname?: string;
|
|
70
70
|
avatar?: string;
|
|
71
|
+
/** 手机号(脱敏,兼容保留的非标准 claim,新代码请使用 phone_number) */
|
|
71
72
|
phone?: string;
|
|
73
|
+
/** 手机号(脱敏,OIDC 标准 claim,与 phone 内容一致) */
|
|
74
|
+
phone_number?: string;
|
|
75
|
+
/** 生日(ISO 8601,需 birthday scope,未设置时为 null) */
|
|
76
|
+
birthday?: string | null;
|
|
72
77
|
membership_level?: string;
|
|
73
78
|
total_points?: number;
|
|
74
79
|
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -68,7 +68,12 @@ interface SsoUser {
|
|
|
68
68
|
sub: string;
|
|
69
69
|
nickname?: string;
|
|
70
70
|
avatar?: string;
|
|
71
|
+
/** 手机号(脱敏,兼容保留的非标准 claim,新代码请使用 phone_number) */
|
|
71
72
|
phone?: string;
|
|
73
|
+
/** 手机号(脱敏,OIDC 标准 claim,与 phone 内容一致) */
|
|
74
|
+
phone_number?: string;
|
|
75
|
+
/** 生日(ISO 8601,需 birthday scope,未设置时为 null) */
|
|
76
|
+
birthday?: string | null;
|
|
72
77
|
membership_level?: string;
|
|
73
78
|
total_points?: number;
|
|
74
79
|
}
|