@nihplod/sso-sdk 1.3.0 → 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.
@@ -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,21 +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
208
  const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
206
- if (process.env.NODE_ENV !== "production") {
207
- if (!validateSsoCookie) {
208
- console.warn(
209
- "[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"
210
- );
211
- }
212
- if (!clientSecret) {
213
- console.warn(
214
- "[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"
215
- );
216
- }
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
+ );
217
218
  }
218
219
  return async function ssoMiddleware(request) {
219
220
  const { pathname } = request.nextUrl;
@@ -256,6 +257,7 @@ function createSsoMiddleware(config) {
256
257
  }
257
258
  }
258
259
  const state = generateRandomString(32);
260
+ const nonce = generateRandomString(43);
259
261
  const verifier = generateRandomString(64);
260
262
  const challenge = await computeCodeChallenge(verifier);
261
263
  const authorizeParams = new URLSearchParams();
@@ -264,12 +266,14 @@ function createSsoMiddleware(config) {
264
266
  authorizeParams.set("redirect_uri", redirectUri);
265
267
  authorizeParams.set("scope", scopes);
266
268
  authorizeParams.set("state", state);
269
+ authorizeParams.set("nonce", nonce);
267
270
  authorizeParams.set("code_challenge", challenge);
268
271
  authorizeParams.set("code_challenge_method", "S256");
269
272
  const loginUrl = new URL("/api/oauth/authorize", normalizedBase);
270
273
  loginUrl.search = authorizeParams.toString();
271
274
  const response = import_server.NextResponse.redirect(loginUrl);
272
275
  response.cookies.set(stateCookieName, state, getHostCookieOptions(600, secureCookies));
276
+ response.cookies.set(nonceCookieName, nonce, getHostCookieOptions(600, secureCookies));
273
277
  response.cookies.set(verifierCookieName, verifier, getSecureCookieOptions(600, callbackPath, secureCookies));
274
278
  const safeReturnUrl = (request.nextUrl.pathname + request.nextUrl.search).slice(0, 2048);
275
279
  response.cookies.set(returnUrlCookieName, safeReturnUrl, getHostCookieOptions(600, secureCookies));
@@ -358,18 +362,21 @@ function normalizeIssuer(url) {
358
362
  var cachedJwks = null;
359
363
  var cachedDiscovery = null;
360
364
  var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
365
+ var FETCH_TIMEOUT_MS = 1e4;
361
366
  async function fetchDiscoveryDoc(baseUrl) {
362
367
  const now = Date.now();
363
368
  if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
364
369
  return cachedDiscovery.doc;
365
370
  }
366
371
  try {
367
- const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`);
368
- 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();
369
377
  cachedDiscovery = { baseUrl, doc, fetchedAt: now };
370
378
  return doc;
371
379
  } catch {
372
- cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
373
380
  return null;
374
381
  }
375
382
  }
@@ -381,7 +388,10 @@ async function fetchJwks(baseUrl, options = {}) {
381
388
  const discovery = await fetchDiscoveryDoc(baseUrl);
382
389
  const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
383
390
  try {
384
- 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
+ });
385
395
  if (!res.ok) return null;
386
396
  const jwks = await res.json();
387
397
  cachedJwks = { baseUrl, jwks, fetchedAt: now };
@@ -428,7 +438,7 @@ async function computeAtHash(accessToken) {
428
438
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
429
439
  }
430
440
  async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
431
- const { rejectHs256WhenRs256Available = true } = options;
441
+ const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
432
442
  const header = decodeJwtHeader(idToken);
433
443
  if (!header) {
434
444
  throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
@@ -518,6 +528,12 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
518
528
  throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
519
529
  }
520
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
+ }
521
537
  return { sub: payload.sub };
522
538
  }
523
539
 
@@ -538,11 +554,19 @@ function createCallbackRouteHandler(config) {
538
554
  const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
539
555
  const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
540
556
  const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
557
+ const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
541
558
  const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
542
559
  const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
543
560
  const normalizedBase = ssoBaseUrl.replace(/\/+$/, "");
544
561
  const normalizedServerBase = (config.serverBaseUrl ?? ssoBaseUrl).replace(/\/+$/, "");
545
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) {
546
570
  const { searchParams } = request.nextUrl;
547
571
  const error = searchParams.get("error");
548
572
  if (error) {
@@ -573,7 +597,7 @@ function createCallbackRouteHandler(config) {
573
597
  { status: 400 }
574
598
  );
575
599
  }
576
- if (returnedState !== savedState) {
600
+ if (!timingSafeEqualString(returnedState ?? "", savedState)) {
577
601
  return import_server2.NextResponse.json(
578
602
  {
579
603
  error: "invalid_request",
@@ -582,6 +606,7 @@ function createCallbackRouteHandler(config) {
582
606
  { status: 400 }
583
607
  );
584
608
  }
609
+ const expectedNonce = request.cookies.get(nonceCookieName)?.value;
585
610
  const verifier = request.cookies.get(verifierCookieName)?.value;
586
611
  if (!verifier) {
587
612
  return import_server2.NextResponse.json(
@@ -663,7 +688,8 @@ function createCallbackRouteHandler(config) {
663
688
  tokenData.id_token,
664
689
  tokenData.access_token,
665
690
  normalizedBase,
666
- clientId
691
+ clientId,
692
+ { expectedNonce }
667
693
  );
668
694
  } catch (err) {
669
695
  return import_server2.NextResponse.json(
@@ -707,11 +733,12 @@ function createCallbackRouteHandler(config) {
707
733
  }
708
734
  }
709
735
  response.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
736
+ response.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
710
737
  response.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
711
738
  response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
712
739
  response.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, request.nextUrl.pathname, secureCookies));
713
740
  return response;
714
- };
741
+ }
715
742
  }
716
743
 
717
744
  // src/next/logout.ts
@@ -719,7 +746,7 @@ var import_server3 = require("next/server");
719
746
 
720
747
  // src/core/discovery.ts
721
748
  var DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1e3;
722
- var FETCH_TIMEOUT_MS = 5e3;
749
+ var FETCH_TIMEOUT_MS2 = 5e3;
723
750
  var cache = /* @__PURE__ */ new Map();
724
751
  var inflight = /* @__PURE__ */ new Map();
725
752
  function fetchDiscoveryCached(baseUrl) {
@@ -731,7 +758,7 @@ function fetchDiscoveryCached(baseUrl) {
731
758
  if (existing) return existing;
732
759
  const promise = (async () => {
733
760
  const controller = new AbortController();
734
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
761
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
735
762
  try {
736
763
  const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
737
764
  signal: controller.signal
@@ -752,6 +779,23 @@ function fetchDiscoveryCached(baseUrl) {
752
779
  }
753
780
 
754
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>`;
798
+ }
755
799
  function generateRandomString2(length) {
756
800
  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
757
801
  const maxValid = Math.floor(256 / chars.length) * chars.length;
@@ -784,6 +828,7 @@ function createLogoutRouteHandler(config) {
784
828
  const refreshTokenCookieName = pickName(config.refreshTokenCookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
785
829
  const idTokenCookieName = pickName(config.idTokenCookieName, DEFAULT_ID_TOKEN_COOKIE_NAME);
786
830
  const stateCookieName = pickName(config.stateCookieName, DEFAULT_STATE_COOKIE_NAME);
831
+ const nonceCookieName = pickName(config.nonceCookieName, DEFAULT_NONCE_COOKIE_NAME);
787
832
  const returnUrlCookieName = pickName(config.returnUrlCookieName, DEFAULT_RETURN_COOKIE_NAME);
788
833
  const verifierCookieName = pickName(config.verifierCookieName, DEFAULT_VERIFIER_COOKIE_NAME);
789
834
  const logoutStateCookieName = pickName(config.logoutStateCookieName, DEFAULT_LOGOUT_STATE_COOKIE_NAME);
@@ -804,6 +849,12 @@ function createLogoutRouteHandler(config) {
804
849
  res.cookies.set(logoutStateCookieName, "", getHostCookieOptions(0, secureCookies));
805
850
  return res;
806
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
+ }
807
858
  const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;
808
859
  const idTokenHint = request.cookies.get(idTokenCookieName)?.value;
809
860
  if (refreshToken) {
@@ -832,6 +883,7 @@ function createLogoutRouteHandler(config) {
832
883
  res.cookies.set(refreshTokenCookieName, "", getHostCookieOptions(0, secureCookies));
833
884
  res.cookies.set(idTokenCookieName, "", getHostCookieOptions(0, secureCookies));
834
885
  res.cookies.set(stateCookieName, "", getHostCookieOptions(0, secureCookies));
886
+ res.cookies.set(nonceCookieName, "", getHostCookieOptions(0, secureCookies));
835
887
  res.cookies.set(returnUrlCookieName, "", getHostCookieOptions(0, secureCookies));
836
888
  res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, "/", secureCookies));
837
889
  res.cookies.set(verifierCookieName, "", getSecureCookieOptions(0, callbackPath, secureCookies));
@@ -865,6 +917,7 @@ function createLogoutRouteHandler(config) {
865
917
  DEFAULT_ACCESS_TOKEN_COOKIE_NAME,
866
918
  DEFAULT_ID_TOKEN_COOKIE_NAME,
867
919
  DEFAULT_LOGOUT_STATE_COOKIE_NAME,
920
+ DEFAULT_NONCE_COOKIE_NAME,
868
921
  DEFAULT_REFRESH_TOKEN_COOKIE_NAME,
869
922
  DEFAULT_RETURN_COOKIE_NAME,
870
923
  DEFAULT_STATE_COOKIE_NAME,