@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.
package/dist/index.d.mts CHANGED
@@ -426,7 +426,7 @@ declare function generateState(): string;
426
426
  * SSO SDK 错误类型
427
427
  */
428
428
  /** SSO 错误码 */
429
- type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch" | "id_token_nonce_mismatch" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
429
+ type SsoErrorCode = "invalid_config" | "state_mismatch" | "issuer_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch" | "id_token_nonce_mismatch" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
430
430
  /**
431
431
  * 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode
432
432
  *
package/dist/index.d.ts CHANGED
@@ -426,7 +426,7 @@ declare function generateState(): string;
426
426
  * SSO SDK 错误类型
427
427
  */
428
428
  /** SSO 错误码 */
429
- type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch" | "id_token_nonce_mismatch" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
429
+ type SsoErrorCode = "invalid_config" | "state_mismatch" | "issuer_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch" | "id_token_nonce_mismatch" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
430
430
  /**
431
431
  * 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode
432
432
  *
package/dist/index.js CHANGED
@@ -143,24 +143,6 @@ var SILENT_PROBE_KEY = "silent_probe";
143
143
  function buildKey(base, clientId) {
144
144
  return clientId ? `${base}:${clientId}` : base;
145
145
  }
146
- function createMemoryStorageAdapter() {
147
- const store = /* @__PURE__ */ new Map();
148
- return {
149
- get(key) {
150
- return store.get(key) ?? null;
151
- },
152
- set(key, value) {
153
- store.set(key, value);
154
- },
155
- remove(key) {
156
- store.delete(key);
157
- },
158
- keys() {
159
- return [...store.keys()];
160
- }
161
- };
162
- }
163
- var memoryStorageAdapter = createMemoryStorageAdapter();
164
146
  var localStorageAdapter = {
165
147
  get(key) {
166
148
  try {
@@ -482,7 +464,19 @@ async function fetchJwks(baseUrl, options = {}) {
482
464
  return cachedJwks.jwks;
483
465
  }
484
466
  const discovery = await fetchDiscoveryDoc(baseUrl);
485
- const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
467
+ let jwksUri = `${baseUrl}/api/oauth/jwks`;
468
+ if (discovery?.jwks_uri) {
469
+ try {
470
+ if (new URL(discovery.jwks_uri).origin === new URL(baseUrl).origin) {
471
+ jwksUri = discovery.jwks_uri;
472
+ } else {
473
+ console.warn(
474
+ "[SSO SDK] Discovery \u7684 jwks_uri \u4E0E ssoBaseUrl \u4E0D\u540C\u6E90\uFF0C\u5DF2\u5FFD\u7565\u5E76\u4F7F\u7528\u9ED8\u8BA4 JWKS \u7AEF\u70B9"
475
+ );
476
+ }
477
+ } catch {
478
+ }
479
+ }
486
480
  try {
487
481
  const res = await fetch(jwksUri, {
488
482
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
@@ -673,11 +667,7 @@ function assertValidTokenResponse(data, requireRefreshToken) {
673
667
  );
674
668
  }
675
669
  }
676
- var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
677
- "login_required",
678
- "consent_required",
679
- "interaction_required"
680
- ]);
670
+ var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set(["login_required", "consent_required", "interaction_required"]);
681
671
  var _SsoClient = class _SsoClient {
682
672
  constructor(config) {
683
673
  this._discovery = null;
@@ -743,6 +733,22 @@ var _SsoClient = class _SsoClient {
743
733
  if (!authorizationOk) bad.push("authorization_endpoint");
744
734
  if (!sameOrigin(d.token_endpoint)) bad.push("token_endpoint");
745
735
  if (!sameOrigin(d.userinfo_endpoint)) bad.push("userinfo_endpoint");
736
+ if (d.jwks_uri && !sameOrigin(d.jwks_uri)) bad.push("jwks_uri");
737
+ if (d.revocation_endpoint && !sameOrigin(d.revocation_endpoint)) {
738
+ bad.push("revocation_endpoint");
739
+ }
740
+ if (d.introspection_endpoint && !sameOrigin(d.introspection_endpoint)) {
741
+ bad.push("introspection_endpoint");
742
+ }
743
+ if (d.end_session_endpoint) {
744
+ try {
745
+ if (new URL(d.end_session_endpoint).origin !== publicOrigin) {
746
+ bad.push("end_session_endpoint");
747
+ }
748
+ } catch {
749
+ bad.push("end_session_endpoint");
750
+ }
751
+ }
746
752
  if (bad.length > 0) {
747
753
  if (!this._discoveryOriginWarned) {
748
754
  this._discoveryOriginWarned = true;
@@ -768,16 +774,11 @@ var _SsoClient = class _SsoClient {
768
774
  const url = `${this._serverBase}/api/oauth/.well-known/openid-configuration`;
769
775
  try {
770
776
  const controller = new AbortController();
771
- const timeoutId = setTimeout(
772
- () => controller.abort(),
773
- _SsoClient.DISCOVERY_TIMEOUT_MS
774
- );
777
+ const timeoutId = setTimeout(() => controller.abort(), _SsoClient.DISCOVERY_TIMEOUT_MS);
775
778
  const res = await fetch(url, { signal: controller.signal });
776
779
  clearTimeout(timeoutId);
777
780
  if (!res.ok) {
778
- console.warn(
779
- `[SSO SDK] OIDC Discovery \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}, \u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9`
780
- );
781
+ console.warn(`[SSO SDK] OIDC Discovery \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}, \u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9`);
781
782
  return null;
782
783
  }
783
784
  const doc = await res.json();
@@ -992,10 +993,7 @@ var _SsoClient = class _SsoClient {
992
993
  removeOAuthState(this.config.clientId);
993
994
  removeOAuthNonce(this.config.clientId);
994
995
  removePkceVerifier(`${this.config.clientId}_popup_nonce`);
995
- throw new SsoError(
996
- "popup_blocked",
997
- "\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5"
998
- );
996
+ throw new SsoError("popup_blocked", "\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5");
999
997
  }
1000
998
  const redirectUriOrigin = new URL(this.config.redirectUri).origin;
1001
999
  try {
@@ -1024,9 +1022,7 @@ var _SsoClient = class _SsoClient {
1024
1022
  }
1025
1023
  this.handleCallback(event.data.callbackUrl).then((tokenData) => {
1026
1024
  if (tokenData === null) {
1027
- reject(
1028
- new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09")
1029
- );
1025
+ reject(new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09"));
1030
1026
  return;
1031
1027
  }
1032
1028
  resolve(tokenData);
@@ -1037,12 +1033,7 @@ var _SsoClient = class _SsoClient {
1037
1033
  if (!completed) {
1038
1034
  completed = true;
1039
1035
  cleanup();
1040
- reject(
1041
- new SsoError(
1042
- "popup_closed",
1043
- "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED"
1044
- )
1045
- );
1036
+ reject(new SsoError("popup_closed", "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED"));
1046
1037
  }
1047
1038
  }
1048
1039
  }, 500);
@@ -1109,18 +1100,24 @@ var _SsoClient = class _SsoClient {
1109
1100
  removeOAuthNonce(this.config.clientId);
1110
1101
  removePkceVerifier(this.config.clientId);
1111
1102
  removeSilentProbe(this.config.clientId);
1112
- throw new SsoError(
1113
- "state_mismatch",
1114
- "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
1115
- );
1103
+ throw new SsoError("state_mismatch", "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB");
1104
+ }
1105
+ const returnedIss = params.get("iss");
1106
+ if (returnedIss) {
1107
+ const discovery = await this._getDiscovery();
1108
+ const expectedIssuer = (discovery?.issuer || this.config.ssoBaseUrl).replace(/\/+$/, "");
1109
+ if (returnedIss.replace(/\/+$/, "") !== expectedIssuer) {
1110
+ removeOAuthState(this.config.clientId);
1111
+ removeOAuthNonce(this.config.clientId);
1112
+ removePkceVerifier(this.config.clientId);
1113
+ removeSilentProbe(this.config.clientId);
1114
+ throw new SsoError("issuer_mismatch", "\u6388\u6743\u54CD\u5E94 issuer \u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 IdP \u6DF7\u53E0\u653B\u51FB");
1115
+ }
1116
1116
  }
1117
1117
  const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
1118
1118
  const verifier = getPkceVerifier(this.config.clientId);
1119
1119
  if (!verifier) {
1120
- throw new SsoError(
1121
- "pkce_required",
1122
- "code_verifier \u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u6765\u81EA\u5176\u4ED6\u6807\u7B7E\u9875\uFF09"
1123
- );
1120
+ 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");
1124
1121
  }
1125
1122
  const tokenEndpoint = await this._getTokenEndpoint();
1126
1123
  const body = new URLSearchParams();
@@ -1167,10 +1164,7 @@ var _SsoClient = class _SsoClient {
1167
1164
  removePkceVerifier(this.config.clientId);
1168
1165
  removeSilentProbe(this.config.clientId);
1169
1166
  removeTokenData(this.config.clientId);
1170
- throw new SsoError(
1171
- "id_token_invalid",
1172
- "Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09"
1173
- );
1167
+ throw new SsoError("id_token_invalid", "Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09");
1174
1168
  }
1175
1169
  if (data.id_token) {
1176
1170
  try {
@@ -1235,19 +1229,14 @@ var _SsoClient = class _SsoClient {
1235
1229
  }
1236
1230
  let res = null;
1237
1231
  let lastErr;
1238
- for (let attempt = 0; attempt < 2; attempt++) {
1239
- try {
1240
- res = await fetchWithTimeout(tokenEndpoint, {
1241
- method: "POST",
1242
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1243
- body: body.toString()
1244
- });
1245
- lastErr = null;
1246
- break;
1247
- } catch (err) {
1248
- lastErr = err;
1249
- if (attempt < 1) await new Promise((r) => setTimeout(r, 1e3));
1250
- }
1232
+ try {
1233
+ res = await fetchWithTimeout(tokenEndpoint, {
1234
+ method: "POST",
1235
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1236
+ body: body.toString()
1237
+ });
1238
+ } catch (err) {
1239
+ lastErr = err;
1251
1240
  }
1252
1241
  if (lastErr || !res) {
1253
1242
  throw new SsoError("network_error", "\u5237\u65B0 Token \u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", lastErr);
@@ -1329,10 +1318,7 @@ var _SsoClient = class _SsoClient {
1329
1318
  removeTokenData(this.config.clientId);
1330
1319
  throw new SsoError("not_authenticated", "Token \u5DF2\u5931\u6548");
1331
1320
  }
1332
- throw new SsoError(
1333
- "userinfo_failed",
1334
- `\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: HTTP ${res.status}`
1335
- );
1321
+ throw new SsoError("userinfo_failed", `\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: HTTP ${res.status}`);
1336
1322
  }
1337
1323
  return await res.json();
1338
1324
  }
@@ -1392,7 +1378,7 @@ var _SsoClient = class _SsoClient {
1392
1378
  if (this.config.clientSecret) {
1393
1379
  revokeBody.set("client_secret", this.config.clientSecret);
1394
1380
  }
1395
- await fetchWithTimeout(
1381
+ const doRevoke = () => fetchWithTimeout(
1396
1382
  revokeUrl,
1397
1383
  {
1398
1384
  method: "POST",
@@ -1401,6 +1387,14 @@ var _SsoClient = class _SsoClient {
1401
1387
  },
1402
1388
  REVOKE_TIMEOUT_MS
1403
1389
  );
1390
+ let res = await doRevoke();
1391
+ if (!res.ok && (res.status >= 500 || res.status === 429)) {
1392
+ await new Promise((resolve) => setTimeout(resolve, 300));
1393
+ res = await doRevoke();
1394
+ }
1395
+ if (!res.ok) {
1396
+ this.debugLog("logout: \u670D\u52A1\u7AEF\u64A4\u9500 refresh_token \u5931\u8D25", { status: res.status });
1397
+ }
1404
1398
  } catch {
1405
1399
  }
1406
1400
  }
@@ -1411,10 +1405,7 @@ var _SsoClient = class _SsoClient {
1411
1405
  logoutUrl.searchParams.set("client_id", this.config.clientId);
1412
1406
  const postLogoutUri = this.config.postLogoutRedirectUri || this.config.redirectUri;
1413
1407
  if (postLogoutUri) {
1414
- logoutUrl.searchParams.set(
1415
- "post_logout_redirect_uri",
1416
- postLogoutUri
1417
- );
1408
+ logoutUrl.searchParams.set("post_logout_redirect_uri", postLogoutUri);
1418
1409
  }
1419
1410
  if (idTokenHint) {
1420
1411
  logoutUrl.searchParams.set("id_token_hint", idTokenHint);