@nihplod/sso-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +360 -0
- package/dist/index.d.mts +272 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +788 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +740 -0
- package/dist/index.mjs.map +1 -0
- package/dist/next/index.d.mts +201 -0
- package/dist/next/index.d.ts +201 -0
- package/dist/next/index.js +583 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/index.mjs +547 -0
- package/dist/next/index.mjs.map +1 -0
- package/dist/react/index.d.mts +301 -0
- package/dist/react/index.d.ts +301 -0
- package/dist/react/index.js +1037 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +1003 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
// src/next/middleware.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
|
|
4
|
+
// src/next/constants.ts
|
|
5
|
+
var DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_at";
|
|
6
|
+
var DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_rt";
|
|
7
|
+
var DEFAULT_ID_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_id";
|
|
8
|
+
var DEFAULT_STATE_COOKIE_NAME = "__Host-nihplod_sso_state";
|
|
9
|
+
var DEFAULT_RETURN_COOKIE_NAME = "__Host-nihplod_sso_return";
|
|
10
|
+
var DEFAULT_VERIFIER_COOKIE_NAME = "__Secure-nihplod_sso_verifier";
|
|
11
|
+
function getHostCookieOptions(maxAge) {
|
|
12
|
+
return {
|
|
13
|
+
httpOnly: true,
|
|
14
|
+
secure: true,
|
|
15
|
+
sameSite: "lax",
|
|
16
|
+
path: "/",
|
|
17
|
+
...maxAge !== void 0 ? { maxAge } : {}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function getSecureCookieOptions(maxAge, path = "/") {
|
|
21
|
+
return {
|
|
22
|
+
httpOnly: true,
|
|
23
|
+
secure: true,
|
|
24
|
+
sameSite: "lax",
|
|
25
|
+
path,
|
|
26
|
+
...maxAge !== void 0 ? { maxAge } : {}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/next/middleware.ts
|
|
31
|
+
function generateRandomString(length) {
|
|
32
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
33
|
+
const maxValid = Math.floor(256 / chars.length) * chars.length;
|
|
34
|
+
let result = "";
|
|
35
|
+
while (result.length < length) {
|
|
36
|
+
const array = new Uint8Array(length * 2);
|
|
37
|
+
crypto.getRandomValues(array);
|
|
38
|
+
for (let i = 0; i < array.length && result.length < length; i++) {
|
|
39
|
+
if (array[i] >= maxValid) continue;
|
|
40
|
+
result += chars[array[i] % chars.length];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
async function computeCodeChallenge(verifier) {
|
|
46
|
+
const encoder = new TextEncoder();
|
|
47
|
+
const data = encoder.encode(verifier);
|
|
48
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
49
|
+
const bytes = new Uint8Array(hash);
|
|
50
|
+
let binary = "";
|
|
51
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
52
|
+
binary += String.fromCharCode(bytes[i]);
|
|
53
|
+
}
|
|
54
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
55
|
+
}
|
|
56
|
+
function matchesPath(pathname, paths) {
|
|
57
|
+
return paths.some((path) => {
|
|
58
|
+
if (pathname === path) return true;
|
|
59
|
+
if (path.endsWith("/:path*") && pathname.startsWith(path.replace("/:path*", ""))) return true;
|
|
60
|
+
if (pathname.startsWith(path + "/")) return true;
|
|
61
|
+
return false;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function introspectAccessToken(token, ssoBaseUrl, clientId, clientSecret) {
|
|
65
|
+
try {
|
|
66
|
+
const body = new URLSearchParams({
|
|
67
|
+
token,
|
|
68
|
+
token_type_hint: "access_token",
|
|
69
|
+
client_id: clientId
|
|
70
|
+
});
|
|
71
|
+
if (clientSecret) {
|
|
72
|
+
body.set("client_secret", clientSecret);
|
|
73
|
+
}
|
|
74
|
+
const res = await fetch(`${ssoBaseUrl}/api/oauth/introspect`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
77
|
+
body: body.toString()
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) return false;
|
|
80
|
+
const data = await res.json();
|
|
81
|
+
return data.active === true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function createSsoMiddleware(config) {
|
|
87
|
+
const {
|
|
88
|
+
clientId,
|
|
89
|
+
clientSecret,
|
|
90
|
+
ssoBaseUrl,
|
|
91
|
+
redirectUri,
|
|
92
|
+
scopes = "openid profile",
|
|
93
|
+
publicPaths = [],
|
|
94
|
+
callbackPath = "/api/auth/callback",
|
|
95
|
+
ssoCookieName = "__Host-user_token",
|
|
96
|
+
accessTokenCookieName = DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
|
|
97
|
+
stateCookieName = DEFAULT_STATE_COOKIE_NAME,
|
|
98
|
+
returnUrlCookieName = DEFAULT_RETURN_COOKIE_NAME,
|
|
99
|
+
verifierCookieName = DEFAULT_VERIFIER_COOKIE_NAME
|
|
100
|
+
} = config;
|
|
101
|
+
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
102
|
+
return async function ssoMiddleware(request) {
|
|
103
|
+
const { pathname } = request.nextUrl;
|
|
104
|
+
if (pathname.startsWith("/_next/") || pathname.startsWith("/favicon.ico") || pathname.match(/\.(ico|png|jpg|jpeg|svg|css|js|woff2?)$/)) {
|
|
105
|
+
return NextResponse.next();
|
|
106
|
+
}
|
|
107
|
+
if (pathname === callbackPath) {
|
|
108
|
+
return NextResponse.next();
|
|
109
|
+
}
|
|
110
|
+
const allPublicPaths = [callbackPath, ...publicPaths];
|
|
111
|
+
if (matchesPath(pathname, allPublicPaths)) {
|
|
112
|
+
return NextResponse.next();
|
|
113
|
+
}
|
|
114
|
+
const ssoSession = request.cookies.get(ssoCookieName);
|
|
115
|
+
if (ssoSession?.value) {
|
|
116
|
+
return NextResponse.next();
|
|
117
|
+
}
|
|
118
|
+
const accessTokenCookie = request.cookies.get(accessTokenCookieName);
|
|
119
|
+
if (accessTokenCookie?.value) {
|
|
120
|
+
const tokenActive = await introspectAccessToken(
|
|
121
|
+
accessTokenCookie.value,
|
|
122
|
+
normalizedBase,
|
|
123
|
+
clientId,
|
|
124
|
+
clientSecret
|
|
125
|
+
);
|
|
126
|
+
if (tokenActive) {
|
|
127
|
+
return NextResponse.next();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const state = generateRandomString(32);
|
|
131
|
+
const verifier = generateRandomString(64);
|
|
132
|
+
const challenge = await computeCodeChallenge(verifier);
|
|
133
|
+
const authorizeParams = new URLSearchParams();
|
|
134
|
+
authorizeParams.set("response_type", "code");
|
|
135
|
+
authorizeParams.set("client_id", clientId);
|
|
136
|
+
authorizeParams.set("redirect_uri", redirectUri);
|
|
137
|
+
authorizeParams.set("scope", scopes);
|
|
138
|
+
authorizeParams.set("state", state);
|
|
139
|
+
authorizeParams.set("code_challenge", challenge);
|
|
140
|
+
authorizeParams.set("code_challenge_method", "S256");
|
|
141
|
+
const loginUrl = new URL("/api/oauth/authorize", normalizedBase);
|
|
142
|
+
loginUrl.search = authorizeParams.toString();
|
|
143
|
+
const response = NextResponse.redirect(loginUrl);
|
|
144
|
+
response.cookies.set(stateCookieName, state, getHostCookieOptions(600));
|
|
145
|
+
response.cookies.set(verifierCookieName, verifier, getSecureCookieOptions(600, callbackPath));
|
|
146
|
+
response.cookies.set(returnUrlCookieName, request.url, getHostCookieOptions(600));
|
|
147
|
+
if (accessTokenCookie?.value) {
|
|
148
|
+
response.cookies.set(accessTokenCookieName, "", getHostCookieOptions(0));
|
|
149
|
+
}
|
|
150
|
+
return response;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/next/callback.ts
|
|
155
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
156
|
+
function isTrustedReturnUrl(url, currentOrigin) {
|
|
157
|
+
if (!url) return false;
|
|
158
|
+
if (url.startsWith("/") && !url.startsWith("//")) return true;
|
|
159
|
+
try {
|
|
160
|
+
return new URL(url).origin === currentOrigin;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function base64UrlDecode(input) {
|
|
166
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
167
|
+
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
168
|
+
const binary = atob(padded);
|
|
169
|
+
const bytes = new Uint8Array(binary.length);
|
|
170
|
+
for (let i = 0; i < binary.length; i++) {
|
|
171
|
+
bytes[i] = binary.charCodeAt(i);
|
|
172
|
+
}
|
|
173
|
+
return bytes;
|
|
174
|
+
}
|
|
175
|
+
function decodeJwtPayload(token) {
|
|
176
|
+
try {
|
|
177
|
+
const parts = token.split(".");
|
|
178
|
+
if (parts.length !== 3) return null;
|
|
179
|
+
const json = new TextDecoder().decode(base64UrlDecode(parts[1]));
|
|
180
|
+
return JSON.parse(json);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function decodeJwtHeader(token) {
|
|
186
|
+
try {
|
|
187
|
+
const parts = token.split(".");
|
|
188
|
+
if (parts.length !== 3) return null;
|
|
189
|
+
const json = new TextDecoder().decode(base64UrlDecode(parts[0]));
|
|
190
|
+
return JSON.parse(json);
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function fetchJwks(baseUrl) {
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch(`${baseUrl}/api/oauth/jwks`);
|
|
198
|
+
if (!res.ok) return null;
|
|
199
|
+
return await res.json();
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function verifyRs256Signature(token, jwk) {
|
|
205
|
+
try {
|
|
206
|
+
const [headerB64, payloadB64, signature] = token.split(".");
|
|
207
|
+
if (!signature || !jwk.n || !jwk.e) return false;
|
|
208
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
209
|
+
"jwk",
|
|
210
|
+
{ kty: "RSA", n: jwk.n, e: jwk.e, alg: "RS256", ext: false },
|
|
211
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
212
|
+
false,
|
|
213
|
+
["verify"]
|
|
214
|
+
);
|
|
215
|
+
const signatureBytes = base64UrlDecode(signature);
|
|
216
|
+
const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
217
|
+
return await crypto.subtle.verify(
|
|
218
|
+
"RSASSA-PKCS1-v1_5",
|
|
219
|
+
cryptoKey,
|
|
220
|
+
signatureBytes,
|
|
221
|
+
data
|
|
222
|
+
);
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function computeAtHash(accessToken) {
|
|
228
|
+
const hash = await crypto.subtle.digest(
|
|
229
|
+
"SHA-256",
|
|
230
|
+
new TextEncoder().encode(accessToken)
|
|
231
|
+
);
|
|
232
|
+
const bytes = new Uint8Array(hash);
|
|
233
|
+
const half = bytes.slice(0, bytes.length / 2);
|
|
234
|
+
let binary = "";
|
|
235
|
+
for (const b of half) {
|
|
236
|
+
binary += String.fromCharCode(b);
|
|
237
|
+
}
|
|
238
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
239
|
+
}
|
|
240
|
+
async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId) {
|
|
241
|
+
const header = decodeJwtHeader(idToken);
|
|
242
|
+
if (!header) throw new Error("ID Token \u683C\u5F0F\u9519\u8BEF");
|
|
243
|
+
const alg = header.alg;
|
|
244
|
+
if (typeof alg !== "string" || alg !== "RS256" && alg !== "HS256") {
|
|
245
|
+
throw new Error(`\u4E0D\u652F\u6301\u7684 ID Token \u7B7E\u540D\u7B97\u6CD5: ${alg}`);
|
|
246
|
+
}
|
|
247
|
+
if (alg === "RS256") {
|
|
248
|
+
const jwks = await fetchJwks(expectedIssuer);
|
|
249
|
+
if (!jwks) throw new Error("\u65E0\u6CD5\u83B7\u53D6 JWKS");
|
|
250
|
+
const kid = typeof header.kid === "string" ? header.kid : void 0;
|
|
251
|
+
const key = jwks.keys.find(
|
|
252
|
+
(k) => k.kty === "RSA" && k.alg === "RS256" && k.use === "sig" && (kid ? k.kid === kid : true)
|
|
253
|
+
);
|
|
254
|
+
if (!key) throw new Error("JWKS \u4E2D\u672A\u627E\u5230\u5339\u914D\u516C\u94A5");
|
|
255
|
+
const valid = await verifyRs256Signature(idToken, key);
|
|
256
|
+
if (!valid) throw new Error("ID Token \u7B7E\u540D\u9A8C\u8BC1\u5931\u8D25");
|
|
257
|
+
}
|
|
258
|
+
if (alg === "HS256") {
|
|
259
|
+
console.warn(
|
|
260
|
+
"[SSO SDK/Next] ID Token \u4F7F\u7528 HS256 \u7B7E\u540D\u3002\u5EFA\u8BAE\u4E3B\u7AD9\u542F\u7528 RS256 \u4EE5\u83B7\u5F97\u5B8C\u6574\u7B7E\u540D\u9A8C\u8BC1\u3002"
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const payload = decodeJwtPayload(idToken);
|
|
264
|
+
if (!payload) throw new Error("ID Token payload \u89E3\u6790\u5931\u8D25");
|
|
265
|
+
const normalizedIssuer = expectedIssuer.replace(/\/+$/, "");
|
|
266
|
+
const tokenIssuer = typeof payload.iss === "string" ? payload.iss.replace(/\/+$/, "") : "";
|
|
267
|
+
if (tokenIssuer !== normalizedIssuer) {
|
|
268
|
+
throw new Error("ID Token issuer \u4E0D\u5339\u914D");
|
|
269
|
+
}
|
|
270
|
+
const aud = payload.aud;
|
|
271
|
+
const audArr = Array.isArray(aud) ? aud : [aud];
|
|
272
|
+
if (!audArr.includes(expectedClientId)) {
|
|
273
|
+
throw new Error("ID Token audience \u4E0D\u5339\u914D");
|
|
274
|
+
}
|
|
275
|
+
if (typeof payload.exp === "number" && Date.now() >= payload.exp * 1e3) {
|
|
276
|
+
throw new Error("ID Token \u5DF2\u8FC7\u671F");
|
|
277
|
+
}
|
|
278
|
+
if (typeof payload.sub !== "string" || !payload.sub) {
|
|
279
|
+
throw new Error("ID Token \u7F3A\u5C11 sub");
|
|
280
|
+
}
|
|
281
|
+
if (typeof payload.at_hash === "string" && payload.at_hash) {
|
|
282
|
+
const actual = await computeAtHash(accessToken);
|
|
283
|
+
if (actual !== payload.at_hash) {
|
|
284
|
+
throw new Error("ID Token at_hash \u4E0D\u5339\u914D");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function createCallbackRouteHandler(config) {
|
|
289
|
+
const {
|
|
290
|
+
clientId,
|
|
291
|
+
ssoBaseUrl,
|
|
292
|
+
redirectUri,
|
|
293
|
+
clientSecret,
|
|
294
|
+
defaultReturnPath = "/",
|
|
295
|
+
accessTokenCookieName = DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
|
|
296
|
+
refreshTokenCookieName = DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
|
|
297
|
+
idTokenCookieName = DEFAULT_ID_TOKEN_COOKIE_NAME,
|
|
298
|
+
stateCookieName = DEFAULT_STATE_COOKIE_NAME,
|
|
299
|
+
returnUrlCookieName = DEFAULT_RETURN_COOKIE_NAME,
|
|
300
|
+
verifierCookieName = DEFAULT_VERIFIER_COOKIE_NAME
|
|
301
|
+
} = config;
|
|
302
|
+
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
303
|
+
return async function GET(request) {
|
|
304
|
+
const { searchParams } = request.nextUrl;
|
|
305
|
+
const error = searchParams.get("error");
|
|
306
|
+
if (error) {
|
|
307
|
+
const desc = searchParams.get("error_description") || error;
|
|
308
|
+
return NextResponse2.json(
|
|
309
|
+
{ error: "authorization_failed", error_description: desc },
|
|
310
|
+
{ status: 400 }
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const code = searchParams.get("code");
|
|
314
|
+
const returnedState = searchParams.get("state");
|
|
315
|
+
if (!code) {
|
|
316
|
+
return NextResponse2.json(
|
|
317
|
+
{
|
|
318
|
+
error: "invalid_request",
|
|
319
|
+
error_description: "\u7F3A\u5C11 authorization code"
|
|
320
|
+
},
|
|
321
|
+
{ status: 400 }
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
const savedState = request.cookies.get(stateCookieName)?.value;
|
|
325
|
+
if (!savedState) {
|
|
326
|
+
return NextResponse2.json(
|
|
327
|
+
{
|
|
328
|
+
error: "invalid_request",
|
|
329
|
+
error_description: "State \u53C2\u6570\u7F3A\u5931\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743\u8BF7\u6C42"
|
|
330
|
+
},
|
|
331
|
+
{ status: 400 }
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (returnedState !== savedState) {
|
|
335
|
+
return NextResponse2.json(
|
|
336
|
+
{
|
|
337
|
+
error: "invalid_request",
|
|
338
|
+
error_description: "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
|
|
339
|
+
},
|
|
340
|
+
{ status: 400 }
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const verifier = request.cookies.get(verifierCookieName)?.value;
|
|
344
|
+
if (!verifier) {
|
|
345
|
+
return NextResponse2.json(
|
|
346
|
+
{
|
|
347
|
+
error: "invalid_request",
|
|
348
|
+
error_description: "PKCE verifier \u7F3A\u5931\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743\u8BF7\u6C42"
|
|
349
|
+
},
|
|
350
|
+
{ status: 400 }
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
const tokenEndpoint = `${normalizedBase}/api/oauth/token`;
|
|
354
|
+
const body = new URLSearchParams();
|
|
355
|
+
body.set("grant_type", "authorization_code");
|
|
356
|
+
body.set("code", code);
|
|
357
|
+
body.set("client_id", clientId);
|
|
358
|
+
body.set("redirect_uri", redirectUri);
|
|
359
|
+
body.set("code_verifier", verifier);
|
|
360
|
+
if (clientSecret) {
|
|
361
|
+
body.set("client_secret", clientSecret);
|
|
362
|
+
}
|
|
363
|
+
let res;
|
|
364
|
+
let lastError;
|
|
365
|
+
const maxRetries = 1;
|
|
366
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
367
|
+
if (attempt > 0) {
|
|
368
|
+
await new Promise((r) => setTimeout(r, 1e3 * Math.pow(2, attempt - 1)));
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
res = await fetch(tokenEndpoint, {
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
374
|
+
body: body.toString()
|
|
375
|
+
});
|
|
376
|
+
lastError = null;
|
|
377
|
+
break;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
lastError = err;
|
|
380
|
+
if (attempt >= maxRetries) {
|
|
381
|
+
return NextResponse2.json(
|
|
382
|
+
{ error: "server_error", error_description: "Token \u8BF7\u6C42\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5\u4ECD\u4E0D\u53EF\u8FBE" },
|
|
383
|
+
{ status: 502 }
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (lastError || !res) {
|
|
389
|
+
return NextResponse2.json(
|
|
390
|
+
{ error: "server_error", error_description: "Token \u8BF7\u6C42\u5931\u8D25" },
|
|
391
|
+
{ status: 502 }
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (!res.ok) {
|
|
395
|
+
let errData = {};
|
|
396
|
+
try {
|
|
397
|
+
errData = await res.json();
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
return NextResponse2.json(
|
|
401
|
+
{
|
|
402
|
+
error: "token_request_failed",
|
|
403
|
+
error_description: errData.error_description || `Token \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}`
|
|
404
|
+
},
|
|
405
|
+
{ status: 502 }
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const tokenData = await res.json();
|
|
409
|
+
if (tokenData.id_token) {
|
|
410
|
+
try {
|
|
411
|
+
await validateIdToken(
|
|
412
|
+
tokenData.id_token,
|
|
413
|
+
tokenData.access_token,
|
|
414
|
+
normalizedBase,
|
|
415
|
+
clientId
|
|
416
|
+
);
|
|
417
|
+
} catch (err) {
|
|
418
|
+
return NextResponse2.json(
|
|
419
|
+
{
|
|
420
|
+
error: "id_token_invalid",
|
|
421
|
+
error_description: err instanceof Error ? err.message : "ID Token \u9A8C\u8BC1\u5931\u8D25"
|
|
422
|
+
},
|
|
423
|
+
{ status: 400 }
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const rawReturnUrl = request.cookies.get(returnUrlCookieName)?.value || defaultReturnPath;
|
|
428
|
+
const returnUrl = isTrustedReturnUrl(rawReturnUrl, request.nextUrl.origin) ? rawReturnUrl : "/";
|
|
429
|
+
const response = NextResponse2.redirect(new URL(returnUrl, request.url));
|
|
430
|
+
response.cookies.set(accessTokenCookieName, tokenData.access_token, {
|
|
431
|
+
...getHostCookieOptions(tokenData.expires_in)
|
|
432
|
+
});
|
|
433
|
+
const refreshMaxAge = tokenData.refresh_expires_in != null ? tokenData.refresh_expires_in : 30 * 24 * 60 * 60;
|
|
434
|
+
response.cookies.set(refreshTokenCookieName, tokenData.refresh_token, {
|
|
435
|
+
...getHostCookieOptions(refreshMaxAge)
|
|
436
|
+
});
|
|
437
|
+
if (tokenData.id_token) {
|
|
438
|
+
response.cookies.set(idTokenCookieName, tokenData.id_token, {
|
|
439
|
+
...getHostCookieOptions(refreshMaxAge)
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
response.cookies.set(stateCookieName, "", getHostCookieOptions(0));
|
|
443
|
+
response.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0));
|
|
444
|
+
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/"));
|
|
445
|
+
response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, request.nextUrl.pathname));
|
|
446
|
+
return response;
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/next/logout.ts
|
|
451
|
+
import { NextResponse as NextResponse3 } from "next/server";
|
|
452
|
+
async function fetchDiscovery(ssoBaseUrl) {
|
|
453
|
+
try {
|
|
454
|
+
const controller = new AbortController();
|
|
455
|
+
setTimeout(() => controller.abort(), 5e3);
|
|
456
|
+
const res = await fetch(
|
|
457
|
+
`${ssoBaseUrl}/api/oauth/.well-known/openid-configuration`,
|
|
458
|
+
{ signal: controller.signal }
|
|
459
|
+
);
|
|
460
|
+
if (!res.ok) return null;
|
|
461
|
+
return await res.json();
|
|
462
|
+
} catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function createLogoutRouteHandler(config) {
|
|
467
|
+
const {
|
|
468
|
+
clientId,
|
|
469
|
+
ssoBaseUrl,
|
|
470
|
+
redirectUri,
|
|
471
|
+
clientSecret,
|
|
472
|
+
postLogoutRedirectUri = new URL(redirectUri).origin + "/",
|
|
473
|
+
redirectToSso = true,
|
|
474
|
+
accessTokenCookieName = DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
|
|
475
|
+
refreshTokenCookieName = DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
|
|
476
|
+
idTokenCookieName = DEFAULT_ID_TOKEN_COOKIE_NAME,
|
|
477
|
+
stateCookieName = DEFAULT_STATE_COOKIE_NAME,
|
|
478
|
+
returnUrlCookieName = DEFAULT_RETURN_COOKIE_NAME,
|
|
479
|
+
verifierCookieName = DEFAULT_VERIFIER_COOKIE_NAME,
|
|
480
|
+
callbackPath = "/api/auth/callback"
|
|
481
|
+
} = config;
|
|
482
|
+
const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
|
|
483
|
+
return async function GET(request) {
|
|
484
|
+
const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;
|
|
485
|
+
const idTokenHint = request.cookies.get(idTokenCookieName)?.value;
|
|
486
|
+
if (refreshToken) {
|
|
487
|
+
try {
|
|
488
|
+
const discovery = await fetchDiscovery(normalizedBase);
|
|
489
|
+
const revokeUrl = discovery?.revocation_endpoint || `${normalizedBase}/api/oauth/revoke`;
|
|
490
|
+
const body = new URLSearchParams({
|
|
491
|
+
token: refreshToken,
|
|
492
|
+
token_type_hint: "refresh_token",
|
|
493
|
+
client_id: clientId
|
|
494
|
+
});
|
|
495
|
+
if (clientSecret) {
|
|
496
|
+
body.set("client_secret", clientSecret);
|
|
497
|
+
}
|
|
498
|
+
await fetch(revokeUrl, {
|
|
499
|
+
method: "POST",
|
|
500
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
501
|
+
body: body.toString()
|
|
502
|
+
});
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const clearCookies = (res) => {
|
|
507
|
+
res.cookies.set(accessTokenCookieName, "", getHostCookieOptions(0));
|
|
508
|
+
res.cookies.set(refreshTokenCookieName, "", getHostCookieOptions(0));
|
|
509
|
+
res.cookies.set(idTokenCookieName, "", getHostCookieOptions(0));
|
|
510
|
+
res.cookies.set(stateCookieName, "", getHostCookieOptions(0));
|
|
511
|
+
res.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0));
|
|
512
|
+
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/"));
|
|
513
|
+
res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, callbackPath));
|
|
514
|
+
return res;
|
|
515
|
+
};
|
|
516
|
+
if (redirectToSso) {
|
|
517
|
+
const discovery = await fetchDiscovery(normalizedBase);
|
|
518
|
+
const endSessionEndpoint = discovery?.end_session_endpoint || `${normalizedBase}/logout`;
|
|
519
|
+
const logoutUrl = new URL(endSessionEndpoint);
|
|
520
|
+
logoutUrl.searchParams.set("client_id", clientId);
|
|
521
|
+
logoutUrl.searchParams.set(
|
|
522
|
+
"post_logout_redirect_uri",
|
|
523
|
+
postLogoutRedirectUri
|
|
524
|
+
);
|
|
525
|
+
if (idTokenHint) {
|
|
526
|
+
logoutUrl.searchParams.set("id_token_hint", idTokenHint);
|
|
527
|
+
}
|
|
528
|
+
return clearCookies(NextResponse3.redirect(logoutUrl.toString()));
|
|
529
|
+
}
|
|
530
|
+
return clearCookies(
|
|
531
|
+
NextResponse3.redirect(request.nextUrl.origin + "/")
|
|
532
|
+
);
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
export {
|
|
536
|
+
DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
|
|
537
|
+
DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
|
|
538
|
+
DEFAULT_RETURN_COOKIE_NAME,
|
|
539
|
+
DEFAULT_STATE_COOKIE_NAME,
|
|
540
|
+
DEFAULT_VERIFIER_COOKIE_NAME,
|
|
541
|
+
createCallbackRouteHandler,
|
|
542
|
+
createLogoutRouteHandler,
|
|
543
|
+
createSsoMiddleware,
|
|
544
|
+
getHostCookieOptions,
|
|
545
|
+
getSecureCookieOptions
|
|
546
|
+
};
|
|
547
|
+
//# sourceMappingURL=index.mjs.map
|