@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/README.md +46 -5
- package/dist/index.d.mts +25 -5
- package/dist/index.d.ts +25 -5
- package/dist/index.js +64 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -4
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +73 -3
- package/dist/next/index.d.ts +73 -3
- package/dist/next/index.js +213 -2
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +212 -2
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +25 -5
- package/dist/react/index.d.ts +25 -5
- package/dist/react/index.js +72 -4
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +72 -4
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react/index.mjs
CHANGED
|
@@ -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(
|
|
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
|
|
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;
|