@nihplod/sso-sdk 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,6 +57,8 @@ interface SsoMiddlewareConfig {
57
57
  accessTokenCookieName?: string;
58
58
  /** State Cookie 名称,默认 __Host-nihplod_sso_state */
59
59
  stateCookieName?: string;
60
+ /** OIDC Nonce Cookie 名称,默认 __Host-nihplod_sso_nonce */
61
+ nonceCookieName?: string;
60
62
  /** Return URL Cookie 名称,默认 __Host-nihplod_sso_return */
61
63
  returnUrlCookieName?: string;
62
64
  /** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */
@@ -73,6 +75,12 @@ interface SsoMiddlewareConfig {
73
75
  * middleware / callback / logout 三处配置需保持一致。
74
76
  */
75
77
  insecureLocalDev?: boolean;
78
+ /**
79
+ * 服务端到服务端调用的内网地址(可选,如 http://127.0.0.1:3000)。
80
+ * 仅用于 introspect 等服务器间请求;浏览器跳转(authorize)仍使用
81
+ * ssoBaseUrl 公网地址。适用于子站与 SSO 中心同机/同内网部署。
82
+ */
83
+ serverBaseUrl?: string;
76
84
  }
77
85
  declare function createSsoMiddleware(config: SsoMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
78
86
 
@@ -123,6 +131,8 @@ interface CallbackRouteConfig {
123
131
  idTokenCookieName?: string;
124
132
  /** State Cookie 名称,默认 __Host-nihplod_sso_state */
125
133
  stateCookieName?: string;
134
+ /** OIDC Nonce Cookie 名称,默认 __Host-nihplod_sso_nonce(须与 createSsoMiddleware 一致) */
135
+ nonceCookieName?: string;
126
136
  /** Return URL Cookie 名称,默认 __Host-nihplod_sso_return */
127
137
  returnUrlCookieName?: string;
128
138
  /** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */
@@ -135,6 +145,11 @@ interface CallbackRouteConfig {
135
145
  * 强制忽略并告警。
136
146
  */
137
147
  insecureLocalDev?: boolean;
148
+ /**
149
+ * 服务端到服务端调用的内网地址(可选,如 http://127.0.0.1:3000)。
150
+ * 仅用于 token 交换等服务器间请求;浏览器跳转仍使用 ssoBaseUrl 公网地址。
151
+ */
152
+ serverBaseUrl?: string;
138
153
  }
139
154
  declare function createCallbackRouteHandler(config: CallbackRouteConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
140
155
 
@@ -158,7 +173,7 @@ declare function createCallbackRouteHandler(config: CallbackRouteConfig): (reque
158
173
  * postLogoutRedirectUri: "https://myapp.com/",
159
174
  * });
160
175
  *
161
- * // 推荐使用 POST 触发登出(防登出 CSRF),GET 保留用于兼容 <a> 标签跳转
176
+ * // 推荐使用 POST 触发登出(防登出 CSRF);GET 不执行登出,仅返回确认页
162
177
  * export const GET = handler;
163
178
  * export const POST = handler;
164
179
  * ```
@@ -188,6 +203,8 @@ interface LogoutRouteConfig {
188
203
  idTokenCookieName?: string;
189
204
  /** State Cookie 名称 */
190
205
  stateCookieName?: string;
206
+ /** OIDC Nonce Cookie 名称,默认 __Host-nihplod_sso_nonce(须与 middleware/callback 一致) */
207
+ nonceCookieName?: string;
191
208
  /** Return URL Cookie 名称 */
192
209
  returnUrlCookieName?: string;
193
210
  /** PKCE Verifier Cookie 名称 */
@@ -196,6 +213,13 @@ interface LogoutRouteConfig {
196
213
  callbackPath?: string;
197
214
  /** Logout State Cookie 名称(RP-Initiated Logout CSRF 防护),默认 __Host-nihplod_sso_logout_state */
198
215
  logoutStateCookieName?: string;
216
+ /**
217
+ * 服务端到服务端调用的内网地址(可选,如 http://127.0.0.1:3000)。
218
+ * 仅用于 discovery / revocation 等服务器间请求;浏览器跳转
219
+ * (end-session 等)始终使用 ssoBaseUrl 公网地址。
220
+ * 适用于子站与 SSO 中心同机/同内网部署:避免经公网代理回源的延迟。
221
+ */
222
+ serverBaseUrl?: string;
199
223
  /**
200
224
  * 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除
201
225
  * __Host-/__Secure- 前缀;必须与 middleware / callback 的配置保持一致,
@@ -220,6 +244,7 @@ declare const DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_at";
220
244
  declare const DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_rt";
221
245
  declare const DEFAULT_ID_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_id";
222
246
  declare const DEFAULT_STATE_COOKIE_NAME = "__Host-nihplod_sso_state";
247
+ declare const DEFAULT_NONCE_COOKIE_NAME = "__Host-nihplod_sso_nonce";
223
248
  declare const DEFAULT_RETURN_COOKIE_NAME = "__Host-nihplod_sso_return";
224
249
  declare const DEFAULT_VERIFIER_COOKIE_NAME = "__Secure-nihplod_sso_verifier";
225
250
  declare const DEFAULT_LOGOUT_STATE_COOKIE_NAME = "__Host-nihplod_sso_logout_state";
@@ -246,4 +271,4 @@ declare function getSecureCookieOptions(maxAge?: number, path?: string, secure?:
246
271
  maxAge?: number;
247
272
  };
248
273
 
249
- export { type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_ID_TOKEN_COOKIE_NAME, DEFAULT_LOGOUT_STATE_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutRouteConfig, type SsoMiddlewareConfig, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions, toInsecureCookieName };
274
+ export { type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_ID_TOKEN_COOKIE_NAME, DEFAULT_LOGOUT_STATE_COOKIE_NAME, DEFAULT_NONCE_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutRouteConfig, type SsoMiddlewareConfig, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions, toInsecureCookieName };
@@ -23,6 +23,7 @@ __export(next_exports, {
23
23
  DEFAULT_ACCESS_TOKEN_COOKIE_NAME: () => DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
24
24
  DEFAULT_ID_TOKEN_COOKIE_NAME: () => DEFAULT_ID_TOKEN_COOKIE_NAME,
25
25
  DEFAULT_LOGOUT_STATE_COOKIE_NAME: () => DEFAULT_LOGOUT_STATE_COOKIE_NAME,
26
+ DEFAULT_NONCE_COOKIE_NAME: () => DEFAULT_NONCE_COOKIE_NAME,
26
27
  DEFAULT_REFRESH_TOKEN_COOKIE_NAME: () => DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
27
28
  DEFAULT_RETURN_COOKIE_NAME: () => DEFAULT_RETURN_COOKIE_NAME,
28
29
  DEFAULT_STATE_COOKIE_NAME: () => DEFAULT_STATE_COOKIE_NAME,
@@ -44,6 +45,7 @@ var DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_at";
44
45
  var DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_rt";
45
46
  var DEFAULT_ID_TOKEN_COOKIE_NAME = "__Host-nihplod_sso_id";
46
47
  var DEFAULT_STATE_COOKIE_NAME = "__Host-nihplod_sso_state";
48
+ var DEFAULT_NONCE_COOKIE_NAME = "__Host-nihplod_sso_nonce";
47
49
  var DEFAULT_RETURN_COOKIE_NAME = "__Host-nihplod_sso_return";
48
50
  var DEFAULT_VERIFIER_COOKIE_NAME = "__Secure-nihplod_sso_verifier";
49
51
  var DEFAULT_LOGOUT_STATE_COOKIE_NAME = "__Host-nihplod_sso_logout_state";
@@ -199,20 +201,20 @@ function createSsoMiddleware(config) {
199
201
  const secureCookies = !insecureLocalDev;
200
202
  const accessTokenCookieName = insecureLocalDev ? toInsecureCookieName(config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME) : config.accessTokenCookieName ?? DEFAULT_ACCESS_TOKEN_COOKIE_NAME;
201
203
  const stateCookieName = insecureLocalDev ? toInsecureCookieName(config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME) : config.stateCookieName ?? DEFAULT_STATE_COOKIE_NAME;
204
+ const nonceCookieName = insecureLocalDev ? toInsecureCookieName(config.nonceCookieName ?? DEFAULT_NONCE_COOKIE_NAME) : config.nonceCookieName ?? DEFAULT_NONCE_COOKIE_NAME;
202
205
  const returnUrlCookieName = insecureLocalDev ? toInsecureCookieName(config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME) : config.returnUrlCookieName ?? DEFAULT_RETURN_COOKIE_NAME;
203
206
  const verifierCookieName = insecureLocalDev ? toInsecureCookieName(config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME) : config.verifierCookieName ?? DEFAULT_VERIFIER_COOKIE_NAME;
204
207
  const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
205
- if (process.env.NODE_ENV !== "production") {
206
- if (!validateSsoCookie) {
207
- console.warn(
208
- "[SSO SDK] validateSsoCookie=false\uFF1A\u4E2D\u95F4\u4EF6\u4EC5\u68C0\u67E5 Cookie \u5B58\u5728\u6027\uFF0C\u53EF\u80FD\u653E\u884C\u5DF2\u5931\u6548\u7684\u4F1A\u8BDD\u3002\u4E2D\u95F4\u4EF6\u53EA\u662F UX \u5C42\uFF0C\u654F\u611F\u6570\u636E\u7684\u9274\u6743\u5FC5\u987B\u5728 Route Handler / Server Component \u4E2D\u5B8C\u6210\u3002"
209
- );
210
- }
211
- if (!clientSecret) {
212
- console.warn(
213
- "[SSO SDK] \u672A\u914D\u7F6E clientSecret\uFF08Public Client \u6A21\u5F0F\uFF09\uFF1Aintrospection \u65E0\u5BA2\u6237\u7AEF\u8BA4\u8BC1\uFF0C\u4E2D\u95F4\u4EF6\u5224\u5B9A\u7ED3\u679C\u4EC5\u4F5C UX \u53C2\u8003\u3002Confidential Client\uFF08BFF\uFF09\u8BF7\u914D\u7F6E clientSecret\u3002"
214
- );
215
- }
208
+ const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
209
+ if (!validateSsoCookie) {
210
+ console.warn(
211
+ "[SSO SDK] validateSsoCookie=false\uFF1A\u4E2D\u95F4\u4EF6\u4EC5\u68C0\u67E5 Cookie \u5B58\u5728\u6027\uFF0C\u53EF\u80FD\u653E\u884C\u5DF2\u5931\u6548\u7684\u4F1A\u8BDD\u3002\u4E2D\u95F4\u4EF6\u53EA\u662F UX \u5C42\uFF0C\u654F\u611F\u6570\u636E\u7684\u9274\u6743\u5FC5\u987B\u5728 Route Handler / Server Component \u4E2D\u5B8C\u6210\u3002"
212
+ );
213
+ }
214
+ if (!clientSecret) {
215
+ console.warn(
216
+ "[SSO SDK] \u672A\u914D\u7F6E clientSecret\uFF08Public Client \u6A21\u5F0F\uFF09\uFF1Aintrospection \u65E0\u5BA2\u6237\u7AEF\u8BA4\u8BC1\uFF0C\u4E2D\u95F4\u4EF6\u5224\u5B9A\u7ED3\u679C\u4EC5\u4F5C UX \u53C2\u8003\u3002Confidential Client\uFF08BFF\uFF09\u8BF7\u914D\u7F6E clientSecret\u3002"
217
+ );
216
218
  }
217
219
  return async function ssoMiddleware(request) {
218
220
  const { pathname } = request.nextUrl;
@@ -231,7 +233,7 @@ function createSsoMiddleware(config) {
231
233
  if (validateSsoCookie) {
232
234
  const tokenActive = await introspectAccessToken(
233
235
  ssoSession.value,
234
- normalizedBase,
236
+ normalizedServerBase,
235
237
  clientId,
236
238
  clientSecret
237
239
  );
@@ -246,7 +248,7 @@ function createSsoMiddleware(config) {
246
248
  if (accessTokenCookie?.value) {
247
249
  const tokenActive = await introspectAccessToken(
248
250
  accessTokenCookie.value,
249
- normalizedBase,
251
+ normalizedServerBase,
250
252
  clientId,
251
253
  clientSecret
252
254
  );
@@ -255,6 +257,7 @@ function createSsoMiddleware(config) {
255
257
  }
256
258
  }
257
259
  const state = generateRandomString(32);
260
+ const nonce = generateRandomString(43);
258
261
  const verifier = generateRandomString(64);
259
262
  const challenge = await computeCodeChallenge(verifier);
260
263
  const authorizeParams = new URLSearchParams();
@@ -263,12 +266,14 @@ function createSsoMiddleware(config) {
263
266
  authorizeParams.set("redirect_uri", redirectUri);
264
267
  authorizeParams.set("scope", scopes);
265
268
  authorizeParams.set("state", state);
269
+ authorizeParams.set("nonce", nonce);
266
270
  authorizeParams.set("code_challenge", challenge);
267
271
  authorizeParams.set("code_challenge_method", "S256");
268
272
  const loginUrl = new URL("/api/oauth/authorize", normalizedBase);
269
273
  loginUrl.search = authorizeParams.toString();
270
274
  const response = import_server.NextResponse.redirect(loginUrl);
271
275
  response.cookies.set(stateCookieName, state, getHostCookieOptions(600, secureCookies));
276
+ response.cookies.set(nonceCookieName, nonce, getHostCookieOptions(600, secureCookies));
272
277
  response.cookies.set(verifierCookieName, verifier, getSecureCookieOptions(600, callbackPath, secureCookies));
273
278
  const safeReturnUrl = (request.nextUrl.pathname + request.nextUrl.search).slice(0, 2048);
274
279
  response.cookies.set(returnUrlCookieName, safeReturnUrl, getHostCookieOptions(600, secureCookies));
@@ -357,18 +362,21 @@ function normalizeIssuer(url) {
357
362
  var cachedJwks = null;
358
363
  var cachedDiscovery = null;
359
364
  var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
365
+ var FETCH_TIMEOUT_MS = 1e4;
360
366
  async function fetchDiscoveryDoc(baseUrl) {
361
367
  const now = Date.now();
362
368
  if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
363
369
  return cachedDiscovery.doc;
364
370
  }
365
371
  try {
366
- const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`);
367
- const doc = res.ok ? await res.json() : null;
372
+ const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
373
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
374
+ });
375
+ if (!res.ok) return null;
376
+ const doc = await res.json();
368
377
  cachedDiscovery = { baseUrl, doc, fetchedAt: now };
369
378
  return doc;
370
379
  } catch {
371
- cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
372
380
  return null;
373
381
  }
374
382
  }
@@ -380,7 +388,10 @@ async function fetchJwks(baseUrl, options = {}) {
380
388
  const discovery = await fetchDiscoveryDoc(baseUrl);
381
389
  const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
382
390
  try {
383
- const res = await fetch(jwksUri, options.forceRefresh ? { cache: "no-cache" } : void 0);
391
+ const res = await fetch(jwksUri, {
392
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
393
+ ...options.forceRefresh ? { cache: "no-cache" } : {}
394
+ });
384
395
  if (!res.ok) return null;
385
396
  const jwks = await res.json();
386
397
  cachedJwks = { baseUrl, jwks, fetchedAt: now };
@@ -427,7 +438,7 @@ async function computeAtHash(accessToken) {
427
438
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
428
439
  }
429
440
  async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
430
- const { rejectHs256WhenRs256Available = true } = options;
441
+ const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
431
442
  const header = decodeJwtHeader(idToken);
432
443
  if (!header) {
433
444
  throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
@@ -517,6 +528,12 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
517
528
  throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
518
529
  }
519
530
  }
531
+ if (expectedNonce !== void 0) {
532
+ const tokenNonce = typeof payload.nonce === "string" ? payload.nonce : "";
533
+ if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
534
+ throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
535
+ }
536
+ }
520
537
  return { sub: payload.sub };
521
538
  }
522
539
 
@@ -537,10 +554,19 @@ function createCallbackRouteHandler(config) {
537
554
  const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
538
555
  const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
539
556
  const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
557
+ const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
540
558
  const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
541
559
  const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
542
560
  const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
561
+ const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
543
562
  return async function GET(request) {
563
+ const response = await handleCallback(request);
564
+ if (response.status >= 400) {
565
+ response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
566
+ }
567
+ return response;
568
+ };
569
+ async function handleCallback(request) {
544
570
  const { searchParams } = request.nextUrl;
545
571
  const error = searchParams.get("error");
546
572
  if (error) {
@@ -571,7 +597,7 @@ function createCallbackRouteHandler(config) {
571
597
  { status: 400 }
572
598
  );
573
599
  }
574
- if (returnedState !== savedState) {
600
+ if (!timingSafeEqualString(returnedState ?? "", savedState)) {
575
601
  return import_server2.NextResponse.json(
576
602
  {
577
603
  error: "invalid_request",
@@ -580,6 +606,7 @@ function createCallbackRouteHandler(config) {
580
606
  { status: 400 }
581
607
  );
582
608
  }
609
+ const expectedNonce = request.cookies.get(nonceCookieName)?.value;
583
610
  const verifier = request.cookies.get(verifierCookieName)?.value;
584
611
  if (!verifier) {
585
612
  return import_server2.NextResponse.json(
@@ -590,7 +617,7 @@ function createCallbackRouteHandler(config) {
590
617
  { status: 400 }
591
618
  );
592
619
  }
593
- const tokenEndpoint = `${normalizedBase}/api/oauth/token`;
620
+ const tokenEndpoint = `${normalizedServerBase}/api/oauth/token`;
594
621
  const body = new URLSearchParams();
595
622
  body.set("grant_type", "authorization_code");
596
623
  body.set("code", code);
@@ -661,7 +688,8 @@ function createCallbackRouteHandler(config) {
661
688
  tokenData.id_token,
662
689
  tokenData.access_token,
663
690
  normalizedBase,
664
- clientId
691
+ clientId,
692
+ { expectedNonce }
665
693
  );
666
694
  } catch (err) {
667
695
  return import_server2.NextResponse.json(
@@ -705,30 +733,68 @@ function createCallbackRouteHandler(config) {
705
733
  }
706
734
  }
707
735
  response.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
736
+ response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
708
737
  response.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
709
738
  response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
710
739
  response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, request.nextUrl.pathname, secureCookies));
711
740
  return response;
712
- };
741
+ }
713
742
  }
714
743
 
715
744
  // src/next/logout.ts
716
745
  var import_server3 = require("next/server");
717
- async function fetchDiscovery(ssoBaseUrl) {
718
- const controller = new AbortController();
719
- const timeoutId = setTimeout(() => controller.abort(), 5e3);
720
- try {
721
- const res = await fetch(
722
- `${ssoBaseUrl}/api/oauth/.well-known/openid-configuration`,
723
- { signal: controller.signal }
724
- );
725
- if (!res.ok) return null;
726
- return await res.json();
727
- } catch {
728
- return null;
729
- } finally {
730
- clearTimeout(timeoutId);
731
- }
746
+
747
+ // src/core/discovery.ts
748
+ var DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1e3;
749
+ var FETCH_TIMEOUT_MS2 = 5e3;
750
+ var cache = /* @__PURE__ */ new Map();
751
+ var inflight = /* @__PURE__ */ new Map();
752
+ function fetchDiscoveryCached(baseUrl) {
753
+ const hit = cache.get(baseUrl);
754
+ if (hit && hit.expiresAt > Date.now()) {
755
+ return Promise.resolve(hit.doc);
756
+ }
757
+ const existing = inflight.get(baseUrl);
758
+ if (existing) return existing;
759
+ const promise = (async () => {
760
+ const controller = new AbortController();
761
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
762
+ try {
763
+ const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
764
+ signal: controller.signal
765
+ });
766
+ if (!res.ok) return null;
767
+ const doc = await res.json();
768
+ cache.set(baseUrl, { doc, expiresAt: Date.now() + DISCOVERY_CACHE_TTL_MS });
769
+ return doc;
770
+ } catch {
771
+ return null;
772
+ } finally {
773
+ clearTimeout(timeoutId);
774
+ inflight.delete(baseUrl);
775
+ }
776
+ })();
777
+ inflight.set(baseUrl, promise);
778
+ return promise;
779
+ }
780
+
781
+ // src/next/logout.ts
782
+ function buildLogoutConfirmHtml() {
783
+ return `<!doctype html>
784
+ <html lang="zh-CN">
785
+ <head>
786
+ <meta charset="utf-8">
787
+ <meta name="viewport" content="width=device-width, initial-scale=1">
788
+ <title>\u786E\u8BA4\u9000\u51FA\u767B\u5F55</title>
789
+ </head>
790
+ <body style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui,sans-serif;">
791
+ <form method="post" style="text-align:center;">
792
+ <p>\u786E\u5B9A\u8981\u9000\u51FA\u767B\u5F55\u5417\uFF1F</p>
793
+ <button type="submit" style="padding:10px 20px;background-color:#ef4444;color:white;border:none;border-radius:8px;cursor:pointer;font-size:14px;">\u9000\u51FA\u767B\u5F55</button>
794
+ <p><a href="/" style="color:#2563eb;text-decoration:underline;">\u53D6\u6D88\u5E76\u8FD4\u56DE\u9996\u9875</a></p>
795
+ </form>
796
+ </body>
797
+ </html>`;
732
798
  }
733
799
  function generateRandomString2(length) {
734
800
  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
@@ -762,10 +828,12 @@ function createLogoutRouteHandler(config) {
762
828
  const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
763
829
  const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
764
830
  const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
831
+ const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
765
832
  const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
766
833
  const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
767
834
  const logoutStateCookieName = pickName(config.logoutStateCookieName, DEFAULT_LOGOUT_STATE_COOKIE_NAME);
768
835
  const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
836
+ const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
769
837
  const callbackOrigin = new URL(redirectUri).origin;
770
838
  return async function handler(request) {
771
839
  const returnedState = request.nextUrl.searchParams.get("state");
@@ -781,12 +849,18 @@ function createLogoutRouteHandler(config) {
781
849
  res.cookies.set(logoutStateCookieName, "", getHostCookieOptions(0, secureCookies));
782
850
  return res;
783
851
  }
852
+ if (request.method === "GET") {
853
+ return new import_server3.NextResponse(buildLogoutConfirmHtml(), {
854
+ status: 200,
855
+ headers: { "content-type": "text/html; charset=utf-8" }
856
+ });
857
+ }
784
858
  const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;
785
859
  const idTokenHint = request.cookies.get(idTokenCookieName)?.value;
786
860
  if (refreshToken) {
787
861
  try {
788
- const discovery = await fetchDiscovery(normalizedBase);
789
- const revokeUrl = discovery?.revocation_endpoint || `${normalizedBase}/api/oauth/revoke`;
862
+ const discovery = config.serverBaseUrl ? null : await fetchDiscoveryCached(normalizedServerBase);
863
+ const revokeUrl = config.serverBaseUrl ? `${normalizedServerBase}/api/oauth/revoke` : discovery?.revocation_endpoint || `${normalizedBase}/api/oauth/revoke`;
790
864
  const body = new URLSearchParams({
791
865
  token: refreshToken,
792
866
  token_type_hint: "refresh_token",
@@ -798,7 +872,8 @@ function createLogoutRouteHandler(config) {
798
872
  await fetch(revokeUrl, {
799
873
  method: "POST",
800
874
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
801
- body: body.toString()
875
+ body: body.toString(),
876
+ signal: AbortSignal.timeout(3e3)
802
877
  });
803
878
  } catch {
804
879
  }
@@ -808,13 +883,14 @@ function createLogoutRouteHandler(config) {
808
883
  res.cookies.set(refreshTokenCookieName, "", getHostCookieOptions(0, secureCookies));
809
884
  res.cookies.set(idTokenCookieName, "", getHostCookieOptions(0, secureCookies));
810
885
  res.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
886
+ res.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
811
887
  res.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
812
888
  res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
813
889
  res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, callbackPath, secureCookies));
814
890
  return res;
815
891
  };
816
892
  if (redirectToSso) {
817
- const discovery = await fetchDiscovery(normalizedBase);
893
+ const discovery = await fetchDiscoveryCached(normalizedServerBase);
818
894
  const endSessionEndpoint = discovery?.end_session_endpoint || `${normalizedBase}/api/oauth/end-session`;
819
895
  const logoutUrl = new URL(endSessionEndpoint);
820
896
  logoutUrl.searchParams.set("client_id", clientId);
@@ -841,6 +917,7 @@ function createLogoutRouteHandler(config) {
841
917
  DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
842
918
  DEFAULT_ID_TOKEN_COOKIE_NAME,
843
919
  DEFAULT_LOGOUT_STATE_COOKIE_NAME,
920
+ DEFAULT_NONCE_COOKIE_NAME,
844
921
  DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
845
922
  DEFAULT_RETURN_COOKIE_NAME,
846
923
  DEFAULT_STATE_COOKIE_NAME,