@nihplod/sso-sdk 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/index.ts","../../src/next/middleware.ts","../../src/next/constants.ts","../../src/next/callback.ts","../../src/next/logout.ts"],"sourcesContent":["/**\n * @nihplod/sso-sdk/next\n *\n * Next.js App Router 绑定:\n * - createSsoMiddleware: Next.js Middleware 辅助函数\n * - createCallbackRouteHandler: App Router 回调 Route Handler\n * - createLogoutRouteHandler: App Router 登出 Route Handler\n */\n\nexport { createSsoMiddleware } from \"./middleware\";\nexport type { SsoMiddlewareConfig } from \"./middleware\";\n\nexport { createCallbackRouteHandler } from \"./callback\";\nexport type { CallbackRouteConfig } from \"./callback\";\n\nexport { createLogoutRouteHandler } from \"./logout\";\nexport type { LogoutRouteConfig } from \"./logout\";\n\nexport {\n DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\n DEFAULT_REFRESH_TOKEN_COOKIE_NAME,\n DEFAULT_STATE_COOKIE_NAME,\n DEFAULT_RETURN_COOKIE_NAME,\n DEFAULT_VERIFIER_COOKIE_NAME,\n getHostCookieOptions,\n getSecureCookieOptions,\n} from \"./constants\";\n","/**\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} 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 /** 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// ============================================\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 * 从 JWT 中解码 payload(不验证签名,仅用于 Edge Runtime 快速检查 exp)\r\n */\r\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\r\n try {\r\n const parts = token.split(\".\");\r\n if (parts.length !== 3) return null;\r\n const payload = parts[1];\r\n // base64url decode (Edge Runtime 兼容)\r\n const base64 = payload.replace(/-/g, \"+\").replace(/_/g, \"/\");\r\n const decoded = atob(base64);\r\n return JSON.parse(decoded) as Record<string, unknown>;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * 通过主站 Introspection 端点校验 access_token 是否仍有效。\r\n * Confidential Client(BFF)应配置 clientSecret,防止伪造 Cookie 绕过。\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 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) return false;\r\n const data = (await res.json()) as { active?: boolean };\r\n return data.active === true;\r\n } catch {\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 accessTokenCookieName = DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\r\n stateCookieName = DEFAULT_STATE_COOKIE_NAME,\r\n returnUrlCookieName = DEFAULT_RETURN_COOKIE_NAME,\r\n verifierCookieName = DEFAULT_VERIFIER_COOKIE_NAME,\r\n } = config;\r\n\r\n // 规范化 ssoBaseUrl\r\n const normalizedBase = ssoBaseUrl.replace(/\\/+$/, \"\");\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 return NextResponse.next();\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));\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));\r\n\r\n // Set return URL cookie\r\n response.cookies.set(returnUrlCookieName, request.url, getHostCookieOptions(600));\r\n\r\n // 如果存在过期的 access_token cookie,立即清除\r\n if (accessTokenCookie?.value) {\r\n response.cookies.set(accessTokenCookieName, \"\", getHostCookieOptions(0));\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\n/** __Host- 前缀 Cookie 的安全选项(Path 必须为 /) */\nexport function getHostCookieOptions(maxAge?: number): {\n httpOnly: true;\n secure: true;\n sameSite: \"lax\";\n path: \"/\";\n maxAge?: number;\n} {\n return {\n httpOnly: true,\n secure: true,\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): {\n httpOnly: true;\n secure: true;\n sameSite: \"lax\";\n path: string;\n maxAge?: number;\n} {\n return {\n httpOnly: true,\n secure: true,\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 {\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} 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// ============================================\r\n// 工具函数\r\n// ============================================\r\n\r\n/**\r\n * 校验回调后的 returnUrl 是否可信。\r\n * 仅允许:相对路径(且不以 // 开头)或与当前 origin 完全一致。\r\n */\r\nfunction isTrustedReturnUrl(url: string, currentOrigin: string): boolean {\r\n if (!url) return false;\r\n if (url.startsWith(\"/\") && !url.startsWith(\"//\")) return true;\r\n try {\r\n return new URL(url).origin === currentOrigin;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n// ============================================\r\n// ID Token 预校验(写 Cookie 之前)\r\n// ============================================\r\n\r\ninterface JwksKey {\r\n kty: string;\r\n kid?: string;\r\n alg?: string;\r\n use?: string;\r\n n?: string;\r\n e?: string;\r\n}\r\n\r\ninterface Jwks {\r\n keys: JwksKey[];\r\n}\r\n\r\nfunction base64UrlDecode(input: string): Uint8Array {\r\n const base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\r\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\r\n const binary = atob(padded);\r\n const bytes = new Uint8Array(binary.length);\r\n for (let i = 0; i < binary.length; i++) {\r\n bytes[i] = binary.charCodeAt(i);\r\n }\r\n return bytes;\r\n}\r\n\r\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\r\n try {\r\n const parts = token.split(\".\");\r\n if (parts.length !== 3) return null;\r\n const json = new TextDecoder().decode(base64UrlDecode(parts[1]));\r\n return JSON.parse(json) as Record<string, unknown>;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nfunction decodeJwtHeader(token: string): Record<string, unknown> | null {\r\n try {\r\n const parts = token.split(\".\");\r\n if (parts.length !== 3) return null;\r\n const json = new TextDecoder().decode(base64UrlDecode(parts[0]));\r\n return JSON.parse(json) as Record<string, unknown>;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nasync function fetchJwks(baseUrl: string): Promise<Jwks | null> {\r\n try {\r\n const res = await fetch(`${baseUrl}/api/oauth/jwks`);\r\n if (!res.ok) return null;\r\n return (await res.json()) as Jwks;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nasync function verifyRs256Signature(token: string, jwk: JwksKey): Promise<boolean> {\r\n try {\r\n const [headerB64, payloadB64, signature] = token.split(\".\");\r\n if (!signature || !jwk.n || !jwk.e) return false;\r\n\r\n const cryptoKey = await crypto.subtle.importKey(\r\n \"jwk\",\r\n { kty: \"RSA\", n: jwk.n, e: jwk.e, alg: \"RS256\", ext: false },\r\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\r\n false,\r\n [\"verify\"]\r\n );\r\n\r\n const signatureBytes = base64UrlDecode(signature);\r\n const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);\r\n return await crypto.subtle.verify(\r\n \"RSASSA-PKCS1-v1_5\",\r\n cryptoKey,\r\n signatureBytes as unknown as BufferSource,\r\n data as unknown as BufferSource\r\n );\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nasync function computeAtHash(accessToken: string): Promise<string> {\r\n const hash = await crypto.subtle.digest(\r\n \"SHA-256\",\r\n new TextEncoder().encode(accessToken)\r\n );\r\n const bytes = new Uint8Array(hash);\r\n const half = bytes.slice(0, bytes.length / 2);\r\n let binary = \"\";\r\n for (const b of half) {\r\n binary += String.fromCharCode(b);\r\n }\r\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\r\n}\r\n\r\n/**\r\n * 在设置 Cookie 前校验 ID Token 基本声明与签名。\r\n * HS256 场景下无法验证签名(Confidential/BFF 应配置 RS256),仅校验声明与 at_hash。\r\n */\r\nasync function validateIdToken(\r\n idToken: string,\r\n accessToken: string,\r\n expectedIssuer: string,\r\n expectedClientId: string\r\n): Promise<void> {\r\n const header = decodeJwtHeader(idToken);\r\n if (!header) throw new Error(\"ID Token 格式错误\");\r\n\r\n const alg = header.alg;\r\n if (typeof alg !== \"string\" || (alg !== \"RS256\" && alg !== \"HS256\")) {\r\n throw new Error(`不支持的 ID Token 签名算法: ${alg}`);\r\n }\r\n\r\n if (alg === \"RS256\") {\r\n const jwks = await fetchJwks(expectedIssuer);\r\n if (!jwks) throw new Error(\"无法获取 JWKS\");\r\n const kid = typeof header.kid === \"string\" ? header.kid : undefined;\r\n const key = jwks.keys.find(\r\n (k) =>\r\n k.kty === \"RSA\" &&\r\n k.alg === \"RS256\" &&\r\n k.use === \"sig\" &&\r\n (kid ? k.kid === kid : true)\r\n );\r\n if (!key) throw new Error(\"JWKS 中未找到匹配公钥\");\r\n const valid = await verifyRs256Signature(idToken, key);\r\n if (!valid) throw new Error(\"ID Token 签名验证失败\");\r\n }\r\n\r\n // HS256:对称密钥不应通过 JWKS 暴露给 BFF,Confidential Client 应迁移到 RS256\r\n if (alg === \"HS256\") {\r\n // eslint-disable-next-line no-console\r\n console.warn(\r\n \"[SSO SDK/Next] ID Token 使用 HS256 签名。建议主站启用 RS256 以获得完整签名验证。\"\r\n );\r\n }\r\n\r\n const payload = decodeJwtPayload(idToken);\r\n if (!payload) throw new Error(\"ID Token payload 解析失败\");\r\n\r\n const normalizedIssuer = expectedIssuer.replace(/\\/+$/, \"\");\r\n const tokenIssuer =\r\n typeof payload.iss === \"string\" ? payload.iss.replace(/\\/+$/, \"\") : \"\";\r\n if (tokenIssuer !== normalizedIssuer) {\r\n throw new Error(\"ID Token issuer 不匹配\");\r\n }\r\n\r\n const aud = payload.aud;\r\n const audArr = Array.isArray(aud) ? aud : [aud];\r\n if (!audArr.includes(expectedClientId)) {\r\n throw new Error(\"ID Token audience 不匹配\");\r\n }\r\n\r\n if (typeof payload.exp === \"number\" && Date.now() >= payload.exp * 1000) {\r\n throw new Error(\"ID Token 已过期\");\r\n }\r\n if (typeof payload.sub !== \"string\" || !payload.sub) {\r\n throw new Error(\"ID Token 缺少 sub\");\r\n }\r\n\r\n if (typeof payload.at_hash === \"string\" && payload.at_hash) {\r\n const actual = await computeAtHash(accessToken);\r\n if (actual !== payload.at_hash) {\r\n throw new Error(\"ID Token at_hash 不匹配\");\r\n }\r\n }\r\n}\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 accessTokenCookieName = DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\r\n refreshTokenCookieName = DEFAULT_REFRESH_TOKEN_COOKIE_NAME,\r\n idTokenCookieName = DEFAULT_ID_TOKEN_COOKIE_NAME,\r\n stateCookieName = DEFAULT_STATE_COOKIE_NAME,\r\n returnUrlCookieName = DEFAULT_RETURN_COOKIE_NAME,\r\n verifierCookieName = DEFAULT_VERIFIER_COOKIE_NAME,\r\n } = config;\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;\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 // 在设置 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),\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),\r\n });\r\n\r\n // 设置 id_token cookie,用于 RP-Initiated Logout 的 id_token_hint\r\n if (tokenData.id_token) {\r\n response.cookies.set(idTokenCookieName, tokenData.id_token, {\r\n ...getHostCookieOptions(refreshMaxAge),\r\n });\r\n }\r\n\r\n // 清除临时 cookies: state / return URL\r\n response.cookies.set(stateCookieName, \"\", getHostCookieOptions(0));\r\n response.cookies.set(returnUrlCookieName, \"\", getHostCookieOptions(0));\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, \"/\"));\r\n response.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, request.nextUrl.pathname));\r\n\r\n return response;\r\n };\r\n}\r\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 * export const GET = 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 */\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 getHostCookieOptions,\n getSecureCookieOptions,\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\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 try {\n const controller = new AbortController();\n setTimeout(() => controller.abort(), 5000);\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 }\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 accessTokenCookieName = DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\n refreshTokenCookieName = DEFAULT_REFRESH_TOKEN_COOKIE_NAME,\n idTokenCookieName = DEFAULT_ID_TOKEN_COOKIE_NAME,\n stateCookieName = DEFAULT_STATE_COOKIE_NAME,\n returnUrlCookieName = DEFAULT_RETURN_COOKIE_NAME,\n verifierCookieName = DEFAULT_VERIFIER_COOKIE_NAME,\n callbackPath = \"/api/auth/callback\",\n } = config;\n\n const normalizedBase = ssoBaseUrl.replace(/\\/+$/, \"\");\n\n return async function GET(request: NextRequest) {\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));\n res.cookies.set(refreshTokenCookieName, \"\", getHostCookieOptions(0));\n res.cookies.set(idTokenCookieName, \"\", getHostCookieOptions(0));\n res.cookies.set(stateCookieName, \"\", getHostCookieOptions(0));\n res.cookies.set(returnUrlCookieName, \"\", getHostCookieOptions(0));\n res.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, \"/\"));\n res.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, callbackPath));\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}/logout`;\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 return clearCookies(NextResponse.redirect(logoutUrl.toString()));\n }\n\n return clearCookies(\n NextResponse.redirect(request.nextUrl.origin + \"/\")\n );\n };\n}\n"],"mappings":"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","names":["import_server","import_server"]}
1
+ {"version":3,"sources":["../../src/next/index.ts","../../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":["/**\n * @nihplod/sso-sdk/next\n *\n * Next.js App Router 绑定:\n * - createSsoMiddleware: Next.js Middleware 辅助函数\n * - createCallbackRouteHandler: App Router 回调 Route Handler\n * - createLogoutRouteHandler: App Router 登出 Route Handler\n */\n\nexport { createSsoMiddleware } from \"./middleware\";\nexport type { SsoMiddlewareConfig } from \"./middleware\";\n\nexport { createCallbackRouteHandler } from \"./callback\";\nexport type { CallbackRouteConfig } from \"./callback\";\n\nexport { createLogoutRouteHandler } from \"./logout\";\nexport type { LogoutRouteConfig } from \"./logout\";\n\nexport {\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} from \"./constants\";\n","/**\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} 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// ============================================\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 accessTokenCookieName = DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\r\n stateCookieName = DEFAULT_STATE_COOKIE_NAME,\r\n returnUrlCookieName = DEFAULT_RETURN_COOKIE_NAME,\r\n verifierCookieName = DEFAULT_VERIFIER_COOKIE_NAME,\r\n } = config;\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));\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));\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));\r\n\r\n // 如果存在过期的 access_token cookie,立即清除\r\n if (accessTokenCookie?.value) {\r\n response.cookies.set(accessTokenCookieName, \"\", getHostCookieOptions(0));\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/** __Host- 前缀 Cookie 的安全选项(Path 必须为 /) */\nexport function getHostCookieOptions(maxAge?: number): {\n httpOnly: true;\n secure: true;\n sameSite: \"lax\";\n path: \"/\";\n maxAge?: number;\n} {\n return {\n httpOnly: true,\n secure: true,\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): {\n httpOnly: true;\n secure: true;\n sameSite: \"lax\";\n path: string;\n maxAge?: number;\n} {\n return {\n httpOnly: true,\n secure: true,\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} 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// ============================================\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 accessTokenCookieName = DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\r\n refreshTokenCookieName = DEFAULT_REFRESH_TOKEN_COOKIE_NAME,\r\n idTokenCookieName = DEFAULT_ID_TOKEN_COOKIE_NAME,\r\n stateCookieName = DEFAULT_STATE_COOKIE_NAME,\r\n returnUrlCookieName = DEFAULT_RETURN_COOKIE_NAME,\r\n verifierCookieName = DEFAULT_VERIFIER_COOKIE_NAME,\r\n } = config;\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 // 在设置 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),\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),\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),\r\n });\r\n }\r\n }\r\n\r\n // 清除临时 cookies: state / return URL\r\n response.cookies.set(stateCookieName, \"\", getHostCookieOptions(0));\r\n response.cookies.set(returnUrlCookieName, \"\", getHostCookieOptions(0));\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, \"/\"));\r\n response.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, request.nextUrl.pathname));\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 | \"authorization_code_used\"\r\n | \"client_disabled\"\r\n | \"user_denied_authorization\"\r\n | \"account_disabled\"\r\n | \"sso_server_error\"\r\n | \"rate_limited\"\r\n | \"network_error\"\r\n | \"popup_blocked\"\r\n | \"popup_closed\"\r\n | \"id_token_invalid\"\r\n | \"id_token_unsupported_alg\"\r\n | \"id_token_hs256_unsupported\"\r\n | \"id_token_missing_secret\"\r\n | \"id_token_invalid_signature\"\r\n | \"id_token_issuer_mismatch\"\r\n | \"id_token_audience_mismatch\"\r\n | \"id_token_expired\"\r\n | \"id_token_missing_sub\"\r\n | \"id_token_at_hash_mismatch\";\r\n\r\n/**\r\n * 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode\r\n *\r\n * @param oauthError 服务端返回的 error 字段\r\n * @param context 调用上下文:\"token_exchange\"(授权码换 token)或 \"refresh\"(刷新)。\r\n * invalid_grant 在换 token 场景多为授权码过期/已用,在刷新场景多为会话失效,\r\n * 需按上下文细化映射。\r\n */\r\nexport function mapOAuthErrorToSsoCode(\r\n oauthError: string,\r\n context: \"token_exchange\" | \"refresh\" = \"token_exchange\"\r\n): SsoErrorCode {\r\n switch (oauthError) {\r\n case \"invalid_grant\":\r\n // 刷新场景的 invalid_grant 表示 refresh_token 已撤销/过期(调用方另会清除本地登录态)\r\n return context === \"refresh\" ? \"session_expired\" : \"authorization_code_expired\";\r\n case \"invalid_client\":\r\n return \"client_disabled\";\r\n case \"access_denied\":\r\n return \"user_denied_authorization\";\r\n case \"server_error\":\r\n return \"sso_server_error\";\r\n case \"rate_limited\":\r\n // 限流是独立的可重试错误,不应误报为 network_error\r\n return \"rate_limited\";\r\n case \"invalid_scope\":\r\n case \"invalid_request\":\r\n case \"unauthorized_client\":\r\n case \"unsupported_grant_type\":\r\n default:\r\n return \"token_request_failed\";\r\n }\r\n}\r\n\r\n/**\r\n * SSO SDK 自定义错误\r\n */\r\nexport class SsoError extends Error {\r\n public readonly code: SsoErrorCode;\r\n public readonly description: string;\r\n public readonly cause?: unknown;\r\n\r\n constructor(code: SsoErrorCode, description: string, cause?: unknown) {\r\n super(`[SSO SDK] ${code}: ${description}`);\r\n this.name = \"SsoError\";\r\n this.code = code;\r\n this.description = description;\r\n this.cause = cause;\r\n }\r\n}\r\n\r\n/**\r\n * OAuth 2.0 协议层错误(RFC 6749 §5.2)\r\n *\r\n * 用于表示服务端原样返回的 OAuth 错误(error / error_description / error_uri),\r\n * 与 SDK 语义的 SsoError 区分。\r\n */\r\nexport class OAuthError extends Error {\r\n public readonly code: string;\r\n public readonly description: string;\r\n public readonly uri?: string;\r\n\r\n constructor(code: string, description: string, uri?: string) {\r\n super(`[OAuth] ${code}: ${description}`);\r\n this.name = \"OAuthError\";\r\n this.code = code;\r\n this.description = description;\r\n this.uri = uri;\r\n }\r\n}\r\n","/**\n * 安全相关小工具:returnUrl 开放重定向校验、常量时间字符串比较\n */\n\n/**\n * 校验 returnUrl 是否可信(防开放重定向)。\n * 仅允许:相对路径(且不以 // 开头)或与 currentOrigin 完全同源的绝对 URL。\n */\nexport function isTrustedReturnUrl(url: string, currentOrigin: string): boolean {\n if (!url) return false;\n if (url.startsWith(\"/\") && !url.startsWith(\"//\")) return true;\n try {\n return new URL(url).origin === currentOrigin;\n } catch {\n return false;\n }\n}\n\n/**\n * 常量时间字符串比较(避免 state / at_hash / nonce 等机密值的时序侧信道)。\n * 长度不同也执行完整循环,不提前返回。\n */\nexport function timingSafeEqualString(a: string, b: string): boolean {\n const ba = new TextEncoder().encode(a);\n const bb = new TextEncoder().encode(b);\n if (ba.length === 0 || bb.length === 0) return ba.length === bb.length;\n const len = Math.max(ba.length, bb.length);\n let diff = ba.length ^ bb.length;\n for (let i = 0; i < len; i++) {\n diff |= ba[i % ba.length] ^ bb[i % bb.length];\n }\n return diff === 0;\n}\n","/**\n * ID Token 验证工具(浏览器 / Edge Runtime 通用)\n *\n * 提取 SsoClient 与 Next.js callback 中重复的 ID Token 校验逻辑,\n * 包括 base64url 解码、JWKS 获取、RS256 签名验证、at_hash 计算等。\n */\n\nimport { SsoError } from \"./errors\";\nimport { timingSafeEqualString } from \"./security\";\n\n// 运行时 crypto.subtle 检测:现代浏览器 / Edge Runtime / Node 20+ 均提供全局 crypto\nconst _crypto: Pick<Crypto, \"subtle\"> | null =\n typeof crypto !== \"undefined\" && (crypto as Crypto).subtle ? (crypto as Crypto) : null;\n\nfunction getCrypto(): Crypto | null {\n return (_crypto as Crypto | null) ?? null;\n}\n\nexport interface JwksKey {\n kty: string;\n kid?: string;\n alg?: string;\n use?: string;\n n?: string;\n e?: string;\n x5c?: string[];\n}\n\nexport interface Jwks {\n keys: JwksKey[];\n}\n\nexport interface ValidateIdTokenResult {\n sub: string;\n}\n\nexport interface ValidateIdTokenOptions {\n /**\n * 发现 SSO 中心已启用 RS256 时,是否拒绝 HS256 ID Token。\n * 默认 true:只要 JWKS 中存在 RS256 签名密钥,就拒绝 HS256(安全推荐)。\n */\n rejectHs256WhenRs256Available?: boolean;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\n/** 解码 JWT payload(不验证签名) */\nexport function decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n const json = new TextDecoder().decode(base64UrlDecode(parts[1]));\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/** 解码 JWT header */\nexport function decodeJwtHeader(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n const json = new TextDecoder().decode(base64UrlDecode(parts[0]));\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/** 标准化 issuer/base URL,去除末尾斜杠 */\nfunction normalizeIssuer(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\n/** OIDC Discovery 文档(仅取本模块关心的字段) */\ninterface OidcDiscoveryDoc {\n issuer?: string;\n jwks_uri?: string;\n}\n\nlet cachedJwks: { baseUrl: string; jwks: Jwks; fetchedAt: number } | null = null;\nlet cachedDiscovery: { baseUrl: string; doc: OidcDiscoveryDoc | null; fetchedAt: number } | null = null;\nconst JWKS_CACHE_TTL_MS = 5 * 60 * 1000;\n\n/** 拉取 OIDC Discovery 文档(带内存缓存;失败返回 null 不抛错) */\nasync function fetchDiscoveryDoc(baseUrl: string): Promise<OidcDiscoveryDoc | null> {\n const now = Date.now();\n if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {\n return cachedDiscovery.doc;\n }\n\n try {\n const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`);\n const doc = res.ok ? ((await res.json()) as OidcDiscoveryDoc) : null;\n cachedDiscovery = { baseUrl, doc, fetchedAt: now };\n return doc;\n } catch {\n cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };\n return null;\n }\n}\n\nexport interface FetchJwksOptions {\n /**\n * 绕过内存缓存与浏览器 HTTP 缓存(cache: \"no-cache\")强制重取 JWKS,\n * 并同步刷新模块级缓存。用于密钥轮换后 kid 失配的自愈重试。\n */\n forceRefresh?: boolean;\n}\n\n/** 从 SSO 中心拉取 JWKS(带内存缓存;优先 Discovery 的 jwks_uri,回退硬编码路径) */\nexport async function fetchJwks(baseUrl: string, options: FetchJwksOptions = {}): Promise<Jwks | null> {\n const now = Date.now();\n if (!options.forceRefresh && cachedJwks && cachedJwks.baseUrl === baseUrl && now - cachedJwks.fetchedAt < JWKS_CACHE_TTL_MS) {\n return cachedJwks.jwks;\n }\n\n const discovery = await fetchDiscoveryDoc(baseUrl);\n const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;\n\n try {\n const res = await fetch(jwksUri, options.forceRefresh ? { cache: \"no-cache\" } : undefined);\n if (!res.ok) return null;\n const jwks = (await res.json()) as Jwks;\n cachedJwks = { baseUrl, jwks, fetchedAt: now };\n return jwks;\n } catch {\n return null;\n }\n}\n\n/** 清空 JWKS / Discovery 内存缓存(测试与密钥轮换场景使用) */\nexport function clearIdTokenCaches(): void {\n cachedJwks = null;\n cachedDiscovery = null;\n}\n\n/** 使用 RS256 公钥验证 ID Token 签名 */\nexport async function verifyRs256Signature(token: string, jwk: JwksKey): Promise<boolean> {\n try {\n const [headerB64, payloadB64, signature] = token.split(\".\");\n if (!signature || !jwk.n || !jwk.e) return false;\n\n const c = getCrypto();\n if (!c) return false;\n\n const cryptoKey = await c.subtle.importKey(\n \"jwk\",\n { kty: \"RSA\", n: jwk.n, e: jwk.e, alg: \"RS256\", ext: false },\n { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\n false,\n [\"verify\"]\n );\n\n const signatureBytes = base64UrlDecode(signature);\n const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);\n return await c.subtle.verify(\n \"RSASSA-PKCS1-v1_5\",\n cryptoKey,\n signatureBytes as unknown as BufferSource,\n data as unknown as BufferSource\n );\n } catch {\n return false;\n }\n}\n\n/** 计算 OIDC at_hash(access_token SHA-256 前 128bit base64url) */\nexport async function computeAtHash(accessToken: string): Promise<string> {\n const c = getCrypto();\n if (!c) return \"\";\n const hash = await c.subtle.digest(\"SHA-256\", new TextEncoder().encode(accessToken));\n const bytes = new Uint8Array(hash);\n const half = bytes.slice(0, bytes.length / 2);\n let binary = \"\";\n for (const b of half) {\n binary += String.fromCharCode(b);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** 验证 ID Token(iss、aud、exp、iat、azp、sub、签名、at_hash) */\nexport async function validateIdToken(\n idToken: string,\n accessToken: string,\n expectedIssuer: string,\n expectedClientId: string,\n options: ValidateIdTokenOptions = {}\n): Promise<ValidateIdTokenResult> {\n const { rejectHs256WhenRs256Available = true } = options;\n\n const header = decodeJwtHeader(idToken);\n if (!header) {\n throw new SsoError(\"id_token_invalid\", \"ID Token 格式错误\");\n }\n\n const alg = header.alg;\n if (typeof alg !== \"string\" || (alg !== \"RS256\" && alg !== \"HS256\")) {\n throw new SsoError(\"id_token_unsupported_alg\", `不支持的 ID Token 签名算法: ${alg}`);\n }\n\n const baseUrl = normalizeIssuer(expectedIssuer);\n // issuer 以 Discovery 文档的 issuer 为准(防伪站伪造 iss 通过校验),回退调用方传入值\n const discovery = await fetchDiscoveryDoc(baseUrl);\n const normalizedIssuer = normalizeIssuer(discovery?.issuer || baseUrl);\n\n // RS256:通过 JWKS 验证签名\n if (alg === \"RS256\") {\n const jwks = await fetchJwks(baseUrl);\n if (!jwks) {\n throw new SsoError(\"id_token_invalid_signature\", \"无法获取 JWKS 验证 ID Token 签名\");\n }\n const kid = typeof header.kid === \"string\" ? header.kid : undefined;\n // 有 kid 时精确匹配;无 kid 时逐个尝试所有 RS256 签名公钥\n const matchCandidates = (set: Jwks) =>\n set.keys.filter(\n (k) =>\n k.kty === \"RSA\" &&\n k.alg === \"RS256\" &&\n k.use === \"sig\" &&\n (kid ? k.kid === kid : true)\n );\n const verifyAny = async (keys: JwksKey[]): Promise<boolean> => {\n for (const key of keys) {\n if (await verifyRs256Signature(idToken, key)) return true;\n }\n return false;\n };\n\n let candidates = matchCandidates(jwks);\n let validSig = candidates.length > 0 ? await verifyAny(candidates) : false;\n\n // 密钥轮换自愈:按 kid 过滤无候选,或所有候选验签失败时,\n // 可能是本地 JWKS 缓存陈旧 —— 强制重取一次 JWKS 后重试,仍失败才抛错\n if (candidates.length === 0 || !validSig) {\n const freshJwks = await fetchJwks(baseUrl, { forceRefresh: true });\n if (freshJwks) {\n candidates = matchCandidates(freshJwks);\n validSig = candidates.length > 0 ? await verifyAny(candidates) : false;\n }\n }\n\n if (candidates.length === 0) {\n throw new SsoError(\"id_token_invalid_signature\", \"JWKS 中未找到匹配的 RS256 公钥\");\n }\n if (!validSig) {\n throw new SsoError(\"id_token_invalid_signature\", \"ID Token 签名验证失败\");\n }\n }\n\n // HS256:对称密钥无法安全分发给 Public Client / BFF。\n // SDK 拒绝 HS256 ID Token:主站需配置 RS256 密钥对以启用安全验证。\n if (alg === \"HS256\") {\n if (rejectHs256WhenRs256Available) {\n const jwks = await fetchJwks(baseUrl);\n const hasRs256 = jwks?.keys?.some((k) => k.alg === \"RS256\" && k.use === \"sig\");\n if (hasRs256) {\n throw new SsoError(\"id_token_unsupported_alg\", \"SSO 中心已配置 RS256,拒绝 HS256 ID Token\");\n }\n }\n throw new SsoError(\n \"id_token_hs256_unsupported\",\n \"ID Token 使用 HS256 对称签名,SDK 无法安全验证。\" +\n \"请联系管理员在 SSO 主站配置 RS256 密钥对(JWT_ID_TOKEN_PRIVATE_KEY / JWT_ID_TOKEN_PUBLIC_KEY)后重新签发。\"\n );\n }\n\n const payload = decodeJwtPayload(idToken);\n if (!payload) {\n throw new SsoError(\"id_token_invalid\", \"ID Token payload 解析失败\");\n }\n\n const tokenIssuer = typeof payload.iss === \"string\" ? normalizeIssuer(payload.iss) : \"\";\n if (tokenIssuer !== normalizedIssuer) {\n throw new SsoError(\"id_token_issuer_mismatch\", \"ID Token issuer 不匹配\");\n }\n\n // aud 校验:单值或数组包含 clientId;多 aud 时按 OIDC Core §3.1.3.7 要求 azp 必须等于 clientId\n const aud = payload.aud;\n const audList = Array.isArray(aud) ? aud : typeof aud === \"string\" ? [aud] : [];\n if (!audList.includes(expectedClientId)) {\n throw new SsoError(\"id_token_audience_mismatch\", \"ID Token audience 不匹配\");\n }\n if (audList.length > 1 && payload.azp !== expectedClientId) {\n throw new SsoError(\"id_token_audience_mismatch\", \"ID Token 多 audience 时 azp 必须为当前 client\");\n }\n\n // exp 必需且不得过期\n if (typeof payload.exp !== \"number\") {\n throw new SsoError(\"id_token_invalid\", \"ID Token 缺少 exp 声明\");\n }\n if (Date.now() >= payload.exp * 1000) {\n throw new SsoError(\"id_token_expired\", \"ID Token 已过期\");\n }\n\n // iat 不得在未来(允许 60s clock skew)\n if (typeof payload.iat === \"number\" && payload.iat * 1000 > Date.now() + 60_000) {\n throw new SsoError(\"id_token_invalid\", \"ID Token iat 在未来,疑似伪造或时钟异常\");\n }\n\n if (typeof payload.sub !== \"string\" || !payload.sub) {\n throw new SsoError(\"id_token_missing_sub\", \"ID Token 缺少 sub\");\n }\n\n // 校验 at_hash:确保 ID Token 与当前 access_token 绑定(常量时间比较)\n if (typeof payload.at_hash === \"string\" && payload.at_hash) {\n const actual = await computeAtHash(accessToken);\n if (!timingSafeEqualString(actual, payload.at_hash)) {\n throw new SsoError(\"id_token_at_hash_mismatch\", \"ID Token at_hash 不匹配\");\n }\n }\n\n return { sub: payload.sub };\n}\n","/**\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} 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\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 accessTokenCookieName = DEFAULT_ACCESS_TOKEN_COOKIE_NAME,\n refreshTokenCookieName = DEFAULT_REFRESH_TOKEN_COOKIE_NAME,\n idTokenCookieName = DEFAULT_ID_TOKEN_COOKIE_NAME,\n stateCookieName = DEFAULT_STATE_COOKIE_NAME,\n returnUrlCookieName = DEFAULT_RETURN_COOKIE_NAME,\n verifierCookieName = DEFAULT_VERIFIER_COOKIE_NAME,\n callbackPath = \"/api/auth/callback\",\n logoutStateCookieName = DEFAULT_LOGOUT_STATE_COOKIE_NAME,\n } = config;\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));\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));\n res.cookies.set(refreshTokenCookieName, \"\", getHostCookieOptions(0));\n res.cookies.set(idTokenCookieName, \"\", getHostCookieOptions(0));\n res.cookies.set(stateCookieName, \"\", getHostCookieOptions(0));\n res.cookies.set(returnUrlCookieName, \"\", getHostCookieOptions(0));\n res.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, \"/\"));\n res.cookies.set(verifierCookieName, \"\", getSecureCookieOptions(0, callbackPath));\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}/logout`;\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));\n return res;\n }\n\n return clearCookies(\n NextResponse.redirect(request.nextUrl.origin + \"/\")\n );\n };\n}\n"],"mappings":"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","names":["import_server","import_server","generateRandomString"]}