@nihplod/sso-sdk 1.3.1 → 1.4.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.
@@ -86,6 +86,7 @@ var STATE_KEY = "oauth_state";
86
86
  var NONCE_KEY = "oidc_nonce";
87
87
  var RETURN_URL_KEY = "return_url";
88
88
  var LOGOUT_STATE_KEY = "logout_state";
89
+ var SILENT_PROBE_KEY = "silent_probe";
89
90
  function buildKey(base, clientId) {
90
91
  return clientId ? `${base}:${clientId}` : base;
91
92
  }
@@ -193,6 +194,15 @@ function getReturnUrl(clientId) {
193
194
  function removeReturnUrl(clientId) {
194
195
  _transient.remove(buildKey(RETURN_URL_KEY, clientId));
195
196
  }
197
+ function saveSilentProbe(state, clientId) {
198
+ _transient.set(buildKey(SILENT_PROBE_KEY, clientId), state);
199
+ }
200
+ function getSilentProbe(clientId) {
201
+ return _transient.get(buildKey(SILENT_PROBE_KEY, clientId));
202
+ }
203
+ function removeSilentProbe(clientId) {
204
+ _transient.remove(buildKey(SILENT_PROBE_KEY, clientId));
205
+ }
196
206
  function clearAllSsoData(clientId) {
197
207
  if (clientId) {
198
208
  removeTokenData(clientId);
@@ -200,6 +210,7 @@ function clearAllSsoData(clientId) {
200
210
  removeOAuthNonce(clientId);
201
211
  removeReturnUrl(clientId);
202
212
  removeLogoutState(clientId);
213
+ removeSilentProbe(clientId);
203
214
  removePkceVerifier(clientId);
204
215
  removePkceVerifier(`${clientId}_popup_nonce`);
205
216
  return;
@@ -209,6 +220,7 @@ function clearAllSsoData(clientId) {
209
220
  removeOAuthNonce();
210
221
  removeReturnUrl();
211
222
  removeLogoutState();
223
+ removeSilentProbe();
212
224
  const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;
213
225
  const stores = [
214
226
  typeof sessionStorage !== "undefined" ? sessionStorage : null,
@@ -468,6 +480,11 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
468
480
  }
469
481
 
470
482
  // src/core/SsoClient.ts
483
+ var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
484
+ "login_required",
485
+ "consent_required",
486
+ "interaction_required"
487
+ ]);
471
488
  var _SsoClient = class _SsoClient {
472
489
  constructor(config) {
473
490
  this._discovery = null;
@@ -561,12 +578,15 @@ var _SsoClient = class _SsoClient {
561
578
  *
562
579
  * @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;
563
580
  * 仅允许相对路径或同源绝对 URL,否则忽略并告警)
581
+ * @param options.prompt - OIDC prompt 参数。传 "none" 为静默探测:
582
+ * 额外在存储中记录探测标记(该次请求的 state),回调无会话时
583
+ * handleCallback() 返回 null 而不抛错。
564
584
  *
565
585
  * ⚠️ 不要与 getLoginUrl() 混用:两者都会重新生成并覆盖 sessionStorage 中的
566
586
  * state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
567
587
  * 同一次登录只使用其中一个入口。
568
588
  */
569
- async login(returnUrl) {
589
+ async login(returnUrl, options = {}) {
570
590
  const verifier = generateCodeVerifier();
571
591
  const challenge = await generateCodeChallenge(verifier);
572
592
  const state = generateState();
@@ -574,6 +594,11 @@ var _SsoClient = class _SsoClient {
574
594
  savePkceVerifier(this.config.clientId, verifier);
575
595
  saveOAuthState(state, this.config.clientId);
576
596
  saveOAuthNonce(nonce, this.config.clientId);
597
+ if (options.prompt === "none") {
598
+ saveSilentProbe(state, this.config.clientId);
599
+ } else {
600
+ removeSilentProbe(this.config.clientId);
601
+ }
577
602
  if (returnUrl) {
578
603
  this._saveReturnUrlIfTrusted(returnUrl);
579
604
  }
@@ -587,6 +612,9 @@ var _SsoClient = class _SsoClient {
587
612
  params.set("nonce", nonce);
588
613
  params.set("code_challenge", challenge);
589
614
  params.set("code_challenge_method", "S256");
615
+ if (options.prompt) {
616
+ params.set("prompt", options.prompt);
617
+ }
590
618
  window.location.href = `${authorizeEndpoint}?${params.toString()}`;
591
619
  }
592
620
  /**
@@ -598,7 +626,7 @@ var _SsoClient = class _SsoClient {
598
626
  * state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
599
627
  * 同一次登录只使用其中一个入口。
600
628
  */
601
- async getLoginUrl(returnUrl) {
629
+ async getLoginUrl(returnUrl, options = {}) {
602
630
  const verifier = generateCodeVerifier();
603
631
  const challenge = await generateCodeChallenge(verifier);
604
632
  const state = generateState();
@@ -606,6 +634,11 @@ var _SsoClient = class _SsoClient {
606
634
  savePkceVerifier(this.config.clientId, verifier);
607
635
  saveOAuthState(state, this.config.clientId);
608
636
  saveOAuthNonce(nonce, this.config.clientId);
637
+ if (options.prompt === "none") {
638
+ saveSilentProbe(state, this.config.clientId);
639
+ } else {
640
+ removeSilentProbe(this.config.clientId);
641
+ }
609
642
  if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);
610
643
  const authorizeEndpoint = await this._getAuthorizeEndpoint();
611
644
  const params = new URLSearchParams();
@@ -617,6 +650,9 @@ var _SsoClient = class _SsoClient {
617
650
  params.set("nonce", nonce);
618
651
  params.set("code_challenge", challenge);
619
652
  params.set("code_challenge_method", "S256");
653
+ if (options.prompt) {
654
+ params.set("prompt", options.prompt);
655
+ }
620
656
  return `${authorizeEndpoint}?${params.toString()}`;
621
657
  }
622
658
  /**
@@ -729,7 +765,15 @@ var _SsoClient = class _SsoClient {
729
765
  if (popup && !popup.closed) {
730
766
  popup.close();
731
767
  }
732
- this.handleCallback(event.data.callbackUrl).then(resolve).catch(reject);
768
+ this.handleCallback(event.data.callbackUrl).then((tokenData) => {
769
+ if (tokenData === null) {
770
+ reject(
771
+ new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09")
772
+ );
773
+ return;
774
+ }
775
+ resolve(tokenData);
776
+ }).catch(reject);
733
777
  };
734
778
  const pollTimer = setInterval(() => {
735
779
  if (popup.closed) {
@@ -767,16 +811,29 @@ var _SsoClient = class _SsoClient {
767
811
  * 成功后 token 自动保存到 token 存储(默认 sessionStorage,可通过 setTokenStorage 定制)。
768
812
  *
769
813
  * @param callbackUrl - 完整的回调 URL(window.location.href)
770
- * @returns TokenData 或 null
814
+ * @returns TokenData;prompt=none 静默探测且无 SSO 会话时返回 null
815
+ * (IdP 回跳 login_required / consent_required / interaction_required
816
+ * 且回调 state 与探测标记匹配),此时临时数据已清理,调用方按"未登录"处理即可
771
817
  */
772
818
  async handleCallback(callbackUrl) {
773
819
  const url = new URL(callbackUrl);
774
820
  const params = url.searchParams;
775
821
  const error = params.get("error");
776
822
  if (error) {
823
+ const returnedErrorState = params.get("state");
824
+ const probeState = getSilentProbe(this.config.clientId);
825
+ const savedStateForProbe = getOAuthState(this.config.clientId);
826
+ if (SILENT_PROBE_ERRORS.has(error) && probeState && savedStateForProbe && timingSafeEqualString(savedStateForProbe, returnedErrorState ?? "") && timingSafeEqualString(probeState, returnedErrorState ?? "")) {
827
+ removeOAuthState(this.config.clientId);
828
+ removeOAuthNonce(this.config.clientId);
829
+ removePkceVerifier(this.config.clientId);
830
+ removeSilentProbe(this.config.clientId);
831
+ return null;
832
+ }
777
833
  removeOAuthState(this.config.clientId);
778
834
  removeOAuthNonce(this.config.clientId);
779
835
  removePkceVerifier(this.config.clientId);
836
+ removeSilentProbe(this.config.clientId);
780
837
  const desc = params.get("error_description") || error;
781
838
  throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
782
839
  }
@@ -786,6 +843,7 @@ var _SsoClient = class _SsoClient {
786
843
  removeOAuthState(this.config.clientId);
787
844
  removeOAuthNonce(this.config.clientId);
788
845
  removePkceVerifier(this.config.clientId);
846
+ removeSilentProbe(this.config.clientId);
789
847
  throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
790
848
  }
791
849
  const savedState = getOAuthState(this.config.clientId);
@@ -793,6 +851,7 @@ var _SsoClient = class _SsoClient {
793
851
  removeOAuthState(this.config.clientId);
794
852
  removeOAuthNonce(this.config.clientId);
795
853
  removePkceVerifier(this.config.clientId);
854
+ removeSilentProbe(this.config.clientId);
796
855
  throw new SsoError(
797
856
  "state_mismatch",
798
857
  "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
@@ -856,6 +915,7 @@ var _SsoClient = class _SsoClient {
856
915
  removeOAuthState(this.config.clientId);
857
916
  removeOAuthNonce(this.config.clientId);
858
917
  removePkceVerifier(this.config.clientId);
918
+ removeSilentProbe(this.config.clientId);
859
919
  const now = Date.now();
860
920
  const tokenData = {
861
921
  access_token: data.access_token,
@@ -1498,6 +1558,14 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
1498
1558
  try {
1499
1559
  const tokenData = await client.handleCallback(window.location.href);
1500
1560
  if (cancelled) return;
1561
+ if (tokenData === null) {
1562
+ const clientId2 = client.config.clientId;
1563
+ const probeReturnUrl = getReturnUrl(clientId2);
1564
+ removeReturnUrl(clientId2);
1565
+ const target = probeReturnUrl && isTrustedReturnUrl(probeReturnUrl, window.location.origin) ? probeReturnUrl : "/";
1566
+ window.location.href = target + (target.includes("?") ? "&" : "?") + "sso_probe=no_session";
1567
+ return;
1568
+ }
1501
1569
  await refreshUser();
1502
1570
  if (cancelled) return;
1503
1571
  const clientId = client.config.clientId;