@nihplod/sso-sdk 1.3.1 → 1.4.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.mjs CHANGED
@@ -85,6 +85,7 @@ var STATE_KEY = "oauth_state";
85
85
  var NONCE_KEY = "oidc_nonce";
86
86
  var RETURN_URL_KEY = "return_url";
87
87
  var LOGOUT_STATE_KEY = "logout_state";
88
+ var SILENT_PROBE_KEY = "silent_probe";
88
89
  function buildKey(base, clientId) {
89
90
  return clientId ? `${base}:${clientId}` : base;
90
91
  }
@@ -221,6 +222,15 @@ function getReturnUrl(clientId) {
221
222
  function removeReturnUrl(clientId) {
222
223
  _transient.remove(buildKey(RETURN_URL_KEY, clientId));
223
224
  }
225
+ function saveSilentProbe(state, clientId) {
226
+ _transient.set(buildKey(SILENT_PROBE_KEY, clientId), state);
227
+ }
228
+ function getSilentProbe(clientId) {
229
+ return _transient.get(buildKey(SILENT_PROBE_KEY, clientId));
230
+ }
231
+ function removeSilentProbe(clientId) {
232
+ _transient.remove(buildKey(SILENT_PROBE_KEY, clientId));
233
+ }
224
234
  function clearAllSsoData(clientId) {
225
235
  if (clientId) {
226
236
  removeTokenData(clientId);
@@ -228,6 +238,7 @@ function clearAllSsoData(clientId) {
228
238
  removeOAuthNonce(clientId);
229
239
  removeReturnUrl(clientId);
230
240
  removeLogoutState(clientId);
241
+ removeSilentProbe(clientId);
231
242
  removePkceVerifier(clientId);
232
243
  removePkceVerifier(`${clientId}_popup_nonce`);
233
244
  return;
@@ -237,6 +248,7 @@ function clearAllSsoData(clientId) {
237
248
  removeOAuthNonce();
238
249
  removeReturnUrl();
239
250
  removeLogoutState();
251
+ removeSilentProbe();
240
252
  const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;
241
253
  const stores = [
242
254
  typeof sessionStorage !== "undefined" ? sessionStorage : null,
@@ -501,6 +513,11 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
501
513
  }
502
514
 
503
515
  // src/core/SsoClient.ts
516
+ var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
517
+ "login_required",
518
+ "consent_required",
519
+ "interaction_required"
520
+ ]);
504
521
  var _SsoClient = class _SsoClient {
505
522
  constructor(config) {
506
523
  this._discovery = null;
@@ -594,12 +611,15 @@ var _SsoClient = class _SsoClient {
594
611
  *
595
612
  * @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;
596
613
  * 仅允许相对路径或同源绝对 URL,否则忽略并告警)
614
+ * @param options.prompt - OIDC prompt 参数。传 "none" 为静默探测:
615
+ * 额外在存储中记录探测标记(该次请求的 state),回调无会话时
616
+ * handleCallback() 返回 null 而不抛错。
597
617
  *
598
618
  * ⚠️ 不要与 getLoginUrl() 混用:两者都会重新生成并覆盖 sessionStorage 中的
599
619
  * state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
600
620
  * 同一次登录只使用其中一个入口。
601
621
  */
602
- async login(returnUrl) {
622
+ async login(returnUrl, options = {}) {
603
623
  const verifier = generateCodeVerifier();
604
624
  const challenge = await generateCodeChallenge(verifier);
605
625
  const state = generateState();
@@ -607,6 +627,11 @@ var _SsoClient = class _SsoClient {
607
627
  savePkceVerifier(this.config.clientId, verifier);
608
628
  saveOAuthState(state, this.config.clientId);
609
629
  saveOAuthNonce(nonce, this.config.clientId);
630
+ if (options.prompt === "none") {
631
+ saveSilentProbe(state, this.config.clientId);
632
+ } else {
633
+ removeSilentProbe(this.config.clientId);
634
+ }
610
635
  if (returnUrl) {
611
636
  this._saveReturnUrlIfTrusted(returnUrl);
612
637
  }
@@ -620,6 +645,9 @@ var _SsoClient = class _SsoClient {
620
645
  params.set("nonce", nonce);
621
646
  params.set("code_challenge", challenge);
622
647
  params.set("code_challenge_method", "S256");
648
+ if (options.prompt) {
649
+ params.set("prompt", options.prompt);
650
+ }
623
651
  window.location.href = `${authorizeEndpoint}?${params.toString()}`;
624
652
  }
625
653
  /**
@@ -631,7 +659,7 @@ var _SsoClient = class _SsoClient {
631
659
  * state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
632
660
  * 同一次登录只使用其中一个入口。
633
661
  */
634
- async getLoginUrl(returnUrl) {
662
+ async getLoginUrl(returnUrl, options = {}) {
635
663
  const verifier = generateCodeVerifier();
636
664
  const challenge = await generateCodeChallenge(verifier);
637
665
  const state = generateState();
@@ -639,6 +667,11 @@ var _SsoClient = class _SsoClient {
639
667
  savePkceVerifier(this.config.clientId, verifier);
640
668
  saveOAuthState(state, this.config.clientId);
641
669
  saveOAuthNonce(nonce, this.config.clientId);
670
+ if (options.prompt === "none") {
671
+ saveSilentProbe(state, this.config.clientId);
672
+ } else {
673
+ removeSilentProbe(this.config.clientId);
674
+ }
642
675
  if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);
643
676
  const authorizeEndpoint = await this._getAuthorizeEndpoint();
644
677
  const params = new URLSearchParams();
@@ -650,6 +683,9 @@ var _SsoClient = class _SsoClient {
650
683
  params.set("nonce", nonce);
651
684
  params.set("code_challenge", challenge);
652
685
  params.set("code_challenge_method", "S256");
686
+ if (options.prompt) {
687
+ params.set("prompt", options.prompt);
688
+ }
653
689
  return `${authorizeEndpoint}?${params.toString()}`;
654
690
  }
655
691
  /**
@@ -762,7 +798,15 @@ var _SsoClient = class _SsoClient {
762
798
  if (popup && !popup.closed) {
763
799
  popup.close();
764
800
  }
765
- this.handleCallback(event.data.callbackUrl).then(resolve).catch(reject);
801
+ this.handleCallback(event.data.callbackUrl).then((tokenData) => {
802
+ if (tokenData === null) {
803
+ reject(
804
+ new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09")
805
+ );
806
+ return;
807
+ }
808
+ resolve(tokenData);
809
+ }).catch(reject);
766
810
  };
767
811
  const pollTimer = setInterval(() => {
768
812
  if (popup.closed) {
@@ -800,16 +844,29 @@ var _SsoClient = class _SsoClient {
800
844
  * 成功后 token 自动保存到 token 存储(默认 sessionStorage,可通过 setTokenStorage 定制)。
801
845
  *
802
846
  * @param callbackUrl - 完整的回调 URL(window.location.href)
803
- * @returns TokenData 或 null
847
+ * @returns TokenData;prompt=none 静默探测且无 SSO 会话时返回 null
848
+ * (IdP 回跳 login_required / consent_required / interaction_required
849
+ * 且回调 state 与探测标记匹配),此时临时数据已清理,调用方按"未登录"处理即可
804
850
  */
805
851
  async handleCallback(callbackUrl) {
806
852
  const url = new URL(callbackUrl);
807
853
  const params = url.searchParams;
808
854
  const error = params.get("error");
809
855
  if (error) {
856
+ const returnedErrorState = params.get("state");
857
+ const probeState = getSilentProbe(this.config.clientId);
858
+ const savedStateForProbe = getOAuthState(this.config.clientId);
859
+ if (SILENT_PROBE_ERRORS.has(error) && probeState && savedStateForProbe && timingSafeEqualString(savedStateForProbe, returnedErrorState ?? "") && timingSafeEqualString(probeState, returnedErrorState ?? "")) {
860
+ removeOAuthState(this.config.clientId);
861
+ removeOAuthNonce(this.config.clientId);
862
+ removePkceVerifier(this.config.clientId);
863
+ removeSilentProbe(this.config.clientId);
864
+ return null;
865
+ }
810
866
  removeOAuthState(this.config.clientId);
811
867
  removeOAuthNonce(this.config.clientId);
812
868
  removePkceVerifier(this.config.clientId);
869
+ removeSilentProbe(this.config.clientId);
813
870
  const desc = params.get("error_description") || error;
814
871
  throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
815
872
  }
@@ -819,6 +876,7 @@ var _SsoClient = class _SsoClient {
819
876
  removeOAuthState(this.config.clientId);
820
877
  removeOAuthNonce(this.config.clientId);
821
878
  removePkceVerifier(this.config.clientId);
879
+ removeSilentProbe(this.config.clientId);
822
880
  throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
823
881
  }
824
882
  const savedState = getOAuthState(this.config.clientId);
@@ -826,6 +884,7 @@ var _SsoClient = class _SsoClient {
826
884
  removeOAuthState(this.config.clientId);
827
885
  removeOAuthNonce(this.config.clientId);
828
886
  removePkceVerifier(this.config.clientId);
887
+ removeSilentProbe(this.config.clientId);
829
888
  throw new SsoError(
830
889
  "state_mismatch",
831
890
  "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
@@ -889,6 +948,7 @@ var _SsoClient = class _SsoClient {
889
948
  removeOAuthState(this.config.clientId);
890
949
  removeOAuthNonce(this.config.clientId);
891
950
  removePkceVerifier(this.config.clientId);
951
+ removeSilentProbe(this.config.clientId);
892
952
  const now = Date.now();
893
953
  const tokenData = {
894
954
  access_token: data.access_token,