@nihplod/sso-sdk 1.6.0 → 1.7.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.
@@ -90,24 +90,6 @@ var SILENT_PROBE_KEY = "silent_probe";
90
90
  function buildKey(base, clientId) {
91
91
  return clientId ? `${base}:${clientId}` : base;
92
92
  }
93
- function createMemoryStorageAdapter() {
94
- const store = /* @__PURE__ */ new Map();
95
- return {
96
- get(key) {
97
- return store.get(key) ?? null;
98
- },
99
- set(key, value) {
100
- store.set(key, value);
101
- },
102
- remove(key) {
103
- store.delete(key);
104
- },
105
- keys() {
106
- return [...store.keys()];
107
- }
108
- };
109
- }
110
- var memoryStorageAdapter = createMemoryStorageAdapter();
111
93
  function createSessionStorageAdapter() {
112
94
  const fallback = /* @__PURE__ */ new Map();
113
95
  return {
@@ -375,7 +357,19 @@ async function fetchJwks(baseUrl, options = {}) {
375
357
  return cachedJwks.jwks;
376
358
  }
377
359
  const discovery = await fetchDiscoveryDoc(baseUrl);
378
- const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
360
+ let jwksUri = `${baseUrl}/api/oauth/jwks`;
361
+ if (discovery?.jwks_uri) {
362
+ try {
363
+ if (new URL(discovery.jwks_uri).origin === new URL(baseUrl).origin) {
364
+ jwksUri = discovery.jwks_uri;
365
+ } else {
366
+ console.warn(
367
+ "[SSO SDK] Discovery \u7684 jwks_uri \u4E0E ssoBaseUrl \u4E0D\u540C\u6E90\uFF0C\u5DF2\u5FFD\u7565\u5E76\u4F7F\u7528\u9ED8\u8BA4 JWKS \u7AEF\u70B9"
368
+ );
369
+ }
370
+ } catch {
371
+ }
372
+ }
379
373
  try {
380
374
  const res = await fetch(jwksUri, {
381
375
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
@@ -566,11 +560,7 @@ function assertValidTokenResponse(data, requireRefreshToken) {
566
560
  );
567
561
  }
568
562
  }
569
- var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
570
- "login_required",
571
- "consent_required",
572
- "interaction_required"
573
- ]);
563
+ var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set(["login_required", "consent_required", "interaction_required"]);
574
564
  var _SsoClient = class _SsoClient {
575
565
  constructor(config) {
576
566
  this._discovery = null;
@@ -636,6 +626,22 @@ var _SsoClient = class _SsoClient {
636
626
  if (!authorizationOk) bad.push("authorization_endpoint");
637
627
  if (!sameOrigin(d.token_endpoint)) bad.push("token_endpoint");
638
628
  if (!sameOrigin(d.userinfo_endpoint)) bad.push("userinfo_endpoint");
629
+ if (d.jwks_uri && !sameOrigin(d.jwks_uri)) bad.push("jwks_uri");
630
+ if (d.revocation_endpoint && !sameOrigin(d.revocation_endpoint)) {
631
+ bad.push("revocation_endpoint");
632
+ }
633
+ if (d.introspection_endpoint && !sameOrigin(d.introspection_endpoint)) {
634
+ bad.push("introspection_endpoint");
635
+ }
636
+ if (d.end_session_endpoint) {
637
+ try {
638
+ if (new URL(d.end_session_endpoint).origin !== publicOrigin) {
639
+ bad.push("end_session_endpoint");
640
+ }
641
+ } catch {
642
+ bad.push("end_session_endpoint");
643
+ }
644
+ }
639
645
  if (bad.length > 0) {
640
646
  if (!this._discoveryOriginWarned) {
641
647
  this._discoveryOriginWarned = true;
@@ -661,16 +667,11 @@ var _SsoClient = class _SsoClient {
661
667
  const url = `${this._serverBase}/api/oauth/.well-known/openid-configuration`;
662
668
  try {
663
669
  const controller = new AbortController();
664
- const timeoutId = setTimeout(
665
- () => controller.abort(),
666
- _SsoClient.DISCOVERY_TIMEOUT_MS
667
- );
670
+ const timeoutId = setTimeout(() => controller.abort(), _SsoClient.DISCOVERY_TIMEOUT_MS);
668
671
  const res = await fetch(url, { signal: controller.signal });
669
672
  clearTimeout(timeoutId);
670
673
  if (!res.ok) {
671
- console.warn(
672
- `[SSO SDK] OIDC Discovery \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}, \u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9`
673
- );
674
+ console.warn(`[SSO SDK] OIDC Discovery \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}, \u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9`);
674
675
  return null;
675
676
  }
676
677
  const doc = await res.json();
@@ -885,10 +886,7 @@ var _SsoClient = class _SsoClient {
885
886
  removeOAuthState(this.config.clientId);
886
887
  removeOAuthNonce(this.config.clientId);
887
888
  removePkceVerifier(`${this.config.clientId}_popup_nonce`);
888
- throw new SsoError(
889
- "popup_blocked",
890
- "\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5"
891
- );
889
+ throw new SsoError("popup_blocked", "\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5");
892
890
  }
893
891
  const redirectUriOrigin = new URL(this.config.redirectUri).origin;
894
892
  try {
@@ -917,9 +915,7 @@ var _SsoClient = class _SsoClient {
917
915
  }
918
916
  this.handleCallback(event.data.callbackUrl).then((tokenData) => {
919
917
  if (tokenData === null) {
920
- reject(
921
- new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09")
922
- );
918
+ reject(new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09"));
923
919
  return;
924
920
  }
925
921
  resolve(tokenData);
@@ -930,12 +926,7 @@ var _SsoClient = class _SsoClient {
930
926
  if (!completed) {
931
927
  completed = true;
932
928
  cleanup();
933
- reject(
934
- new SsoError(
935
- "popup_closed",
936
- "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED"
937
- )
938
- );
929
+ reject(new SsoError("popup_closed", "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED"));
939
930
  }
940
931
  }
941
932
  }, 500);
@@ -1002,18 +993,24 @@ var _SsoClient = class _SsoClient {
1002
993
  removeOAuthNonce(this.config.clientId);
1003
994
  removePkceVerifier(this.config.clientId);
1004
995
  removeSilentProbe(this.config.clientId);
1005
- throw new SsoError(
1006
- "state_mismatch",
1007
- "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
1008
- );
996
+ throw new SsoError("state_mismatch", "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB");
997
+ }
998
+ const returnedIss = params.get("iss");
999
+ if (returnedIss) {
1000
+ const discovery = await this._getDiscovery();
1001
+ const expectedIssuer = (discovery?.issuer || this.config.ssoBaseUrl).replace(/\/+$/, "");
1002
+ if (returnedIss.replace(/\/+$/, "") !== expectedIssuer) {
1003
+ removeOAuthState(this.config.clientId);
1004
+ removeOAuthNonce(this.config.clientId);
1005
+ removePkceVerifier(this.config.clientId);
1006
+ removeSilentProbe(this.config.clientId);
1007
+ throw new SsoError("issuer_mismatch", "\u6388\u6743\u54CD\u5E94 issuer \u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 IdP \u6DF7\u53E0\u653B\u51FB");
1008
+ }
1009
1009
  }
1010
1010
  const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
1011
1011
  const verifier = getPkceVerifier(this.config.clientId);
1012
1012
  if (!verifier) {
1013
- throw new SsoError(
1014
- "pkce_required",
1015
- "code_verifier \u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u6765\u81EA\u5176\u4ED6\u6807\u7B7E\u9875\uFF09"
1016
- );
1013
+ throw new SsoError("pkce_required", "code_verifier \u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u6765\u81EA\u5176\u4ED6\u6807\u7B7E\u9875\uFF09");
1017
1014
  }
1018
1015
  const tokenEndpoint = await this._getTokenEndpoint();
1019
1016
  const body = new URLSearchParams();
@@ -1060,10 +1057,7 @@ var _SsoClient = class _SsoClient {
1060
1057
  removePkceVerifier(this.config.clientId);
1061
1058
  removeSilentProbe(this.config.clientId);
1062
1059
  removeTokenData(this.config.clientId);
1063
- throw new SsoError(
1064
- "id_token_invalid",
1065
- "Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09"
1066
- );
1060
+ throw new SsoError("id_token_invalid", "Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09");
1067
1061
  }
1068
1062
  if (data.id_token) {
1069
1063
  try {
@@ -1128,19 +1122,14 @@ var _SsoClient = class _SsoClient {
1128
1122
  }
1129
1123
  let res = null;
1130
1124
  let lastErr;
1131
- for (let attempt = 0; attempt < 2; attempt++) {
1132
- try {
1133
- res = await fetchWithTimeout(tokenEndpoint, {
1134
- method: "POST",
1135
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1136
- body: body.toString()
1137
- });
1138
- lastErr = null;
1139
- break;
1140
- } catch (err) {
1141
- lastErr = err;
1142
- if (attempt < 1) await new Promise((r) => setTimeout(r, 1e3));
1143
- }
1125
+ try {
1126
+ res = await fetchWithTimeout(tokenEndpoint, {
1127
+ method: "POST",
1128
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1129
+ body: body.toString()
1130
+ });
1131
+ } catch (err) {
1132
+ lastErr = err;
1144
1133
  }
1145
1134
  if (lastErr || !res) {
1146
1135
  throw new SsoError("network_error", "\u5237\u65B0 Token \u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", lastErr);
@@ -1222,10 +1211,7 @@ var _SsoClient = class _SsoClient {
1222
1211
  removeTokenData(this.config.clientId);
1223
1212
  throw new SsoError("not_authenticated", "Token \u5DF2\u5931\u6548");
1224
1213
  }
1225
- throw new SsoError(
1226
- "userinfo_failed",
1227
- `\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: HTTP ${res.status}`
1228
- );
1214
+ throw new SsoError("userinfo_failed", `\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: HTTP ${res.status}`);
1229
1215
  }
1230
1216
  return await res.json();
1231
1217
  }
@@ -1285,7 +1271,7 @@ var _SsoClient = class _SsoClient {
1285
1271
  if (this.config.clientSecret) {
1286
1272
  revokeBody.set("client_secret", this.config.clientSecret);
1287
1273
  }
1288
- await fetchWithTimeout(
1274
+ const doRevoke = () => fetchWithTimeout(
1289
1275
  revokeUrl,
1290
1276
  {
1291
1277
  method: "POST",
@@ -1294,6 +1280,14 @@ var _SsoClient = class _SsoClient {
1294
1280
  },
1295
1281
  REVOKE_TIMEOUT_MS
1296
1282
  );
1283
+ let res = await doRevoke();
1284
+ if (!res.ok && (res.status >= 500 || res.status === 429)) {
1285
+ await new Promise((resolve) => setTimeout(resolve, 300));
1286
+ res = await doRevoke();
1287
+ }
1288
+ if (!res.ok) {
1289
+ this.debugLog("logout: \u670D\u52A1\u7AEF\u64A4\u9500 refresh_token \u5931\u8D25", { status: res.status });
1290
+ }
1297
1291
  } catch {
1298
1292
  }
1299
1293
  }
@@ -1304,10 +1298,7 @@ var _SsoClient = class _SsoClient {
1304
1298
  logoutUrl.searchParams.set("client_id", this.config.clientId);
1305
1299
  const postLogoutUri = this.config.postLogoutRedirectUri || this.config.redirectUri;
1306
1300
  if (postLogoutUri) {
1307
- logoutUrl.searchParams.set(
1308
- "post_logout_redirect_uri",
1309
- postLogoutUri
1310
- );
1301
+ logoutUrl.searchParams.set("post_logout_redirect_uri", postLogoutUri);
1311
1302
  }
1312
1303
  if (idTokenHint) {
1313
1304
  logoutUrl.searchParams.set("id_token_hint", idTokenHint);
@@ -1537,7 +1528,15 @@ function SsoProvider({
1537
1528
  const tokenData = getTokenData(client.config.clientId);
1538
1529
  if (!tokenData) return;
1539
1530
  const remainingSec = (tokenData.expires_at - Date.now()) / 1e3;
1540
- if (remainingSec <= 0 || remainingSec <= refreshThreshold) {
1531
+ const lifetimeSec = Math.max(
1532
+ 1,
1533
+ (tokenData.expires_at - tokenData.issued_at) / 1e3
1534
+ );
1535
+ const effectiveThreshold = Math.max(
1536
+ 1,
1537
+ Math.min(refreshThreshold, Math.floor(lifetimeSec / 3))
1538
+ );
1539
+ if (remainingSec <= 0 || remainingSec <= effectiveThreshold) {
1541
1540
  refreshTimerRef.current = setTimeout(
1542
1541
  () => {
1543
1542
  if (active) void attemptRefresh();
@@ -1546,13 +1545,13 @@ function SsoProvider({
1546
1545
  );
1547
1546
  return;
1548
1547
  }
1549
- const delayMs = Math.max((remainingSec - refreshThreshold) * 1e3, minDelayMs, 1e3);
1548
+ const delayMs = Math.max((remainingSec - effectiveThreshold) * 1e3, minDelayMs, 1e3);
1550
1549
  refreshTimerRef.current = setTimeout(() => {
1551
1550
  if (!active) return;
1552
1551
  const td = getTokenData(client.config.clientId);
1553
1552
  if (!td) return;
1554
1553
  const secLeft = (td.expires_at - Date.now()) / 1e3;
1555
- if (secLeft <= refreshThreshold) {
1554
+ if (secLeft <= effectiveThreshold) {
1556
1555
  void attemptRefresh();
1557
1556
  } else {
1558
1557
  scheduleNextRefresh();
@@ -1567,7 +1566,7 @@ function SsoProvider({
1567
1566
  refreshTimerRef.current = null;
1568
1567
  }
1569
1568
  };
1570
- }, [client, loadUser, refreshThreshold, onTokenRefreshed]);
1569
+ }, [client, loadUser, refreshThreshold, onTokenRefreshed, markSessionExpired]);
1571
1570
  useEffect(() => {
1572
1571
  const handleStorageChange = (e) => {
1573
1572
  if (e.key && e.key.startsWith("nihplod_sso_") && !e.key.startsWith(REFRESH_LOCK_PREFIX)) {
@@ -1662,6 +1661,26 @@ function useSso() {
1662
1661
 
1663
1662
  // src/react/RequireAuth.tsx
1664
1663
  import React2, { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
1664
+ function LoginErrorContent({
1665
+ loginError,
1666
+ retry,
1667
+ renderLoginError
1668
+ }) {
1669
+ if (renderLoginError) {
1670
+ return React2.createElement(
1671
+ React2.Fragment,
1672
+ null,
1673
+ renderLoginError(loginError, retry)
1674
+ );
1675
+ }
1676
+ const message = loginError instanceof Error ? loginError.message : "\u767B\u5F55\u5931\u8D25";
1677
+ return React2.createElement(
1678
+ "div",
1679
+ null,
1680
+ React2.createElement("p", null, `\u767B\u5F55\u672A\u5B8C\u6210\uFF1A${message}`),
1681
+ React2.createElement("button", { onClick: retry }, "\u91CD\u8BD5\u767B\u5F55")
1682
+ );
1683
+ }
1665
1684
  function RequireAuth({
1666
1685
  children,
1667
1686
  fallback,
@@ -1707,20 +1726,11 @@ function RequireAuth({
1707
1726
  }
1708
1727
  if (!isAuthenticated) {
1709
1728
  if (loginError) {
1710
- if (renderLoginError) {
1711
- return React2.createElement(
1712
- React2.Fragment,
1713
- null,
1714
- renderLoginError(loginError, retry)
1715
- );
1716
- }
1717
- const message = loginError instanceof Error ? loginError.message : "\u767B\u5F55\u5931\u8D25";
1718
- return React2.createElement(
1719
- "div",
1720
- null,
1721
- React2.createElement("p", null, `\u767B\u5F55\u672A\u5B8C\u6210\uFF1A${message}`),
1722
- React2.createElement("button", { onClick: retry }, "\u91CD\u8BD5\u767B\u5F55")
1723
- );
1729
+ return React2.createElement(LoginErrorContent, {
1730
+ loginError,
1731
+ retry,
1732
+ renderLoginError
1733
+ });
1724
1734
  }
1725
1735
  if (fallback) return React2.createElement(React2.Fragment, null, fallback);
1726
1736
  return React2.createElement("div", null, "\u8BF7\u5148\u767B\u5F55");