@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.d.ts
CHANGED
|
@@ -86,6 +86,21 @@ interface SsoUser {
|
|
|
86
86
|
total_spent?: number;
|
|
87
87
|
total_points?: number;
|
|
88
88
|
}
|
|
89
|
+
/** login() / getLoginUrl() 选项 */
|
|
90
|
+
interface LoginOptions {
|
|
91
|
+
/**
|
|
92
|
+
* OIDC prompt 参数:
|
|
93
|
+
* - "none":静默探测 SSO 会话。用户在 IdP 有会话时直接回跳完成登录;
|
|
94
|
+
* 无会话时 IdP 回跳 error=login_required(或 consent_required /
|
|
95
|
+
* interaction_required),handleCallback() 识别后返回 null 而不抛错
|
|
96
|
+
* (CallbackPage 默认跳回 returnUrl 并附 sso_probe=no_session)。
|
|
97
|
+
* ⚠️ 不要在页面加载时自动探测(会打扰无会话用户并消耗请求),
|
|
98
|
+
* 应由用户点击等明确动作触发。
|
|
99
|
+
* - "login":强制重新认证;
|
|
100
|
+
* - "consent":强制重新授权确认。
|
|
101
|
+
*/
|
|
102
|
+
prompt?: "none" | "login" | "consent";
|
|
103
|
+
}
|
|
89
104
|
/** OIDC Discovery 文档 */
|
|
90
105
|
interface OidcDiscovery {
|
|
91
106
|
issuer: string;
|
|
@@ -139,12 +154,15 @@ declare class SsoClient {
|
|
|
139
154
|
*
|
|
140
155
|
* @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;
|
|
141
156
|
* 仅允许相对路径或同源绝对 URL,否则忽略并告警)
|
|
157
|
+
* @param options.prompt - OIDC prompt 参数。传 "none" 为静默探测:
|
|
158
|
+
* 额外在存储中记录探测标记(该次请求的 state),回调无会话时
|
|
159
|
+
* handleCallback() 返回 null 而不抛错。
|
|
142
160
|
*
|
|
143
161
|
* ⚠️ 不要与 getLoginUrl() 混用:两者都会重新生成并覆盖 sessionStorage 中的
|
|
144
162
|
* state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
|
|
145
163
|
* 同一次登录只使用其中一个入口。
|
|
146
164
|
*/
|
|
147
|
-
login(returnUrl?: string): Promise<void>;
|
|
165
|
+
login(returnUrl?: string, options?: LoginOptions): Promise<void>;
|
|
148
166
|
/**
|
|
149
167
|
* 构建登录 URL(不跳转,返回 URL 字符串)
|
|
150
168
|
*
|
|
@@ -154,7 +172,7 @@ declare class SsoClient {
|
|
|
154
172
|
* state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
|
|
155
173
|
* 同一次登录只使用其中一个入口。
|
|
156
174
|
*/
|
|
157
|
-
getLoginUrl(returnUrl?: string): Promise<string>;
|
|
175
|
+
getLoginUrl(returnUrl?: string, options?: LoginOptions): Promise<string>;
|
|
158
176
|
/**
|
|
159
177
|
* 弹窗模式 SSO 登录
|
|
160
178
|
*
|
|
@@ -200,9 +218,11 @@ declare class SsoClient {
|
|
|
200
218
|
* 成功后 token 自动保存到 token 存储(默认 sessionStorage,可通过 setTokenStorage 定制)。
|
|
201
219
|
*
|
|
202
220
|
* @param callbackUrl - 完整的回调 URL(window.location.href)
|
|
203
|
-
* @returns TokenData
|
|
221
|
+
* @returns TokenData;prompt=none 静默探测且无 SSO 会话时返回 null
|
|
222
|
+
* (IdP 回跳 login_required / consent_required / interaction_required
|
|
223
|
+
* 且回调 state 与探测标记匹配),此时临时数据已清理,调用方按"未登录"处理即可
|
|
204
224
|
*/
|
|
205
|
-
handleCallback(callbackUrl: string): Promise<TokenData>;
|
|
225
|
+
handleCallback(callbackUrl: string): Promise<TokenData | null>;
|
|
206
226
|
/**
|
|
207
227
|
* 刷新 Access Token
|
|
208
228
|
*
|
|
@@ -270,7 +290,7 @@ declare class SsoClient {
|
|
|
270
290
|
* SSO SDK 错误类型
|
|
271
291
|
*/
|
|
272
292
|
/** SSO 错误码 */
|
|
273
|
-
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";
|
|
293
|
+
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";
|
|
274
294
|
/**
|
|
275
295
|
* SSO SDK 自定义错误
|
|
276
296
|
*/
|
package/dist/react/index.js
CHANGED
|
@@ -120,6 +120,7 @@ var STATE_KEY = "oauth_state";
|
|
|
120
120
|
var NONCE_KEY = "oidc_nonce";
|
|
121
121
|
var RETURN_URL_KEY = "return_url";
|
|
122
122
|
var LOGOUT_STATE_KEY = "logout_state";
|
|
123
|
+
var SILENT_PROBE_KEY = "silent_probe";
|
|
123
124
|
function buildKey(base, clientId) {
|
|
124
125
|
return clientId ? `${base}:${clientId}` : base;
|
|
125
126
|
}
|
|
@@ -227,6 +228,15 @@ function getReturnUrl(clientId) {
|
|
|
227
228
|
function removeReturnUrl(clientId) {
|
|
228
229
|
_transient.remove(buildKey(RETURN_URL_KEY, clientId));
|
|
229
230
|
}
|
|
231
|
+
function saveSilentProbe(state, clientId) {
|
|
232
|
+
_transient.set(buildKey(SILENT_PROBE_KEY, clientId), state);
|
|
233
|
+
}
|
|
234
|
+
function getSilentProbe(clientId) {
|
|
235
|
+
return _transient.get(buildKey(SILENT_PROBE_KEY, clientId));
|
|
236
|
+
}
|
|
237
|
+
function removeSilentProbe(clientId) {
|
|
238
|
+
_transient.remove(buildKey(SILENT_PROBE_KEY, clientId));
|
|
239
|
+
}
|
|
230
240
|
function clearAllSsoData(clientId) {
|
|
231
241
|
if (clientId) {
|
|
232
242
|
removeTokenData(clientId);
|
|
@@ -234,6 +244,7 @@ function clearAllSsoData(clientId) {
|
|
|
234
244
|
removeOAuthNonce(clientId);
|
|
235
245
|
removeReturnUrl(clientId);
|
|
236
246
|
removeLogoutState(clientId);
|
|
247
|
+
removeSilentProbe(clientId);
|
|
237
248
|
removePkceVerifier(clientId);
|
|
238
249
|
removePkceVerifier(`${clientId}_popup_nonce`);
|
|
239
250
|
return;
|
|
@@ -243,6 +254,7 @@ function clearAllSsoData(clientId) {
|
|
|
243
254
|
removeOAuthNonce();
|
|
244
255
|
removeReturnUrl();
|
|
245
256
|
removeLogoutState();
|
|
257
|
+
removeSilentProbe();
|
|
246
258
|
const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;
|
|
247
259
|
const stores = [
|
|
248
260
|
typeof sessionStorage !== "undefined" ? sessionStorage : null,
|
|
@@ -502,6 +514,11 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
502
514
|
}
|
|
503
515
|
|
|
504
516
|
// src/core/SsoClient.ts
|
|
517
|
+
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
|
|
518
|
+
"login_required",
|
|
519
|
+
"consent_required",
|
|
520
|
+
"interaction_required"
|
|
521
|
+
]);
|
|
505
522
|
var _SsoClient = class _SsoClient {
|
|
506
523
|
constructor(config) {
|
|
507
524
|
this._discovery = null;
|
|
@@ -595,12 +612,15 @@ var _SsoClient = class _SsoClient {
|
|
|
595
612
|
*
|
|
596
613
|
* @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;
|
|
597
614
|
* 仅允许相对路径或同源绝对 URL,否则忽略并告警)
|
|
615
|
+
* @param options.prompt - OIDC prompt 参数。传 "none" 为静默探测:
|
|
616
|
+
* 额外在存储中记录探测标记(该次请求的 state),回调无会话时
|
|
617
|
+
* handleCallback() 返回 null 而不抛错。
|
|
598
618
|
*
|
|
599
619
|
* ⚠️ 不要与 getLoginUrl() 混用:两者都会重新生成并覆盖 sessionStorage 中的
|
|
600
620
|
* state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
|
|
601
621
|
* 同一次登录只使用其中一个入口。
|
|
602
622
|
*/
|
|
603
|
-
async login(returnUrl) {
|
|
623
|
+
async login(returnUrl, options = {}) {
|
|
604
624
|
const verifier = generateCodeVerifier();
|
|
605
625
|
const challenge = await generateCodeChallenge(verifier);
|
|
606
626
|
const state = generateState();
|
|
@@ -608,6 +628,11 @@ var _SsoClient = class _SsoClient {
|
|
|
608
628
|
savePkceVerifier(this.config.clientId, verifier);
|
|
609
629
|
saveOAuthState(state, this.config.clientId);
|
|
610
630
|
saveOAuthNonce(nonce, this.config.clientId);
|
|
631
|
+
if (options.prompt === "none") {
|
|
632
|
+
saveSilentProbe(state, this.config.clientId);
|
|
633
|
+
} else {
|
|
634
|
+
removeSilentProbe(this.config.clientId);
|
|
635
|
+
}
|
|
611
636
|
if (returnUrl) {
|
|
612
637
|
this._saveReturnUrlIfTrusted(returnUrl);
|
|
613
638
|
}
|
|
@@ -621,6 +646,9 @@ var _SsoClient = class _SsoClient {
|
|
|
621
646
|
params.set("nonce", nonce);
|
|
622
647
|
params.set("code_challenge", challenge);
|
|
623
648
|
params.set("code_challenge_method", "S256");
|
|
649
|
+
if (options.prompt) {
|
|
650
|
+
params.set("prompt", options.prompt);
|
|
651
|
+
}
|
|
624
652
|
window.location.href = `${authorizeEndpoint}?${params.toString()}`;
|
|
625
653
|
}
|
|
626
654
|
/**
|
|
@@ -632,7 +660,7 @@ var _SsoClient = class _SsoClient {
|
|
|
632
660
|
* state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
|
|
633
661
|
* 同一次登录只使用其中一个入口。
|
|
634
662
|
*/
|
|
635
|
-
async getLoginUrl(returnUrl) {
|
|
663
|
+
async getLoginUrl(returnUrl, options = {}) {
|
|
636
664
|
const verifier = generateCodeVerifier();
|
|
637
665
|
const challenge = await generateCodeChallenge(verifier);
|
|
638
666
|
const state = generateState();
|
|
@@ -640,6 +668,11 @@ var _SsoClient = class _SsoClient {
|
|
|
640
668
|
savePkceVerifier(this.config.clientId, verifier);
|
|
641
669
|
saveOAuthState(state, this.config.clientId);
|
|
642
670
|
saveOAuthNonce(nonce, this.config.clientId);
|
|
671
|
+
if (options.prompt === "none") {
|
|
672
|
+
saveSilentProbe(state, this.config.clientId);
|
|
673
|
+
} else {
|
|
674
|
+
removeSilentProbe(this.config.clientId);
|
|
675
|
+
}
|
|
643
676
|
if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);
|
|
644
677
|
const authorizeEndpoint = await this._getAuthorizeEndpoint();
|
|
645
678
|
const params = new URLSearchParams();
|
|
@@ -651,6 +684,9 @@ var _SsoClient = class _SsoClient {
|
|
|
651
684
|
params.set("nonce", nonce);
|
|
652
685
|
params.set("code_challenge", challenge);
|
|
653
686
|
params.set("code_challenge_method", "S256");
|
|
687
|
+
if (options.prompt) {
|
|
688
|
+
params.set("prompt", options.prompt);
|
|
689
|
+
}
|
|
654
690
|
return `${authorizeEndpoint}?${params.toString()}`;
|
|
655
691
|
}
|
|
656
692
|
/**
|
|
@@ -763,7 +799,15 @@ var _SsoClient = class _SsoClient {
|
|
|
763
799
|
if (popup && !popup.closed) {
|
|
764
800
|
popup.close();
|
|
765
801
|
}
|
|
766
|
-
this.handleCallback(event.data.callbackUrl).then(
|
|
802
|
+
this.handleCallback(event.data.callbackUrl).then((tokenData) => {
|
|
803
|
+
if (tokenData === null) {
|
|
804
|
+
reject(
|
|
805
|
+
new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09")
|
|
806
|
+
);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
resolve(tokenData);
|
|
810
|
+
}).catch(reject);
|
|
767
811
|
};
|
|
768
812
|
const pollTimer = setInterval(() => {
|
|
769
813
|
if (popup.closed) {
|
|
@@ -801,16 +845,29 @@ var _SsoClient = class _SsoClient {
|
|
|
801
845
|
* 成功后 token 自动保存到 token 存储(默认 sessionStorage,可通过 setTokenStorage 定制)。
|
|
802
846
|
*
|
|
803
847
|
* @param callbackUrl - 完整的回调 URL(window.location.href)
|
|
804
|
-
* @returns TokenData
|
|
848
|
+
* @returns TokenData;prompt=none 静默探测且无 SSO 会话时返回 null
|
|
849
|
+
* (IdP 回跳 login_required / consent_required / interaction_required
|
|
850
|
+
* 且回调 state 与探测标记匹配),此时临时数据已清理,调用方按"未登录"处理即可
|
|
805
851
|
*/
|
|
806
852
|
async handleCallback(callbackUrl) {
|
|
807
853
|
const url = new URL(callbackUrl);
|
|
808
854
|
const params = url.searchParams;
|
|
809
855
|
const error = params.get("error");
|
|
810
856
|
if (error) {
|
|
857
|
+
const returnedErrorState = params.get("state");
|
|
858
|
+
const probeState = getSilentProbe(this.config.clientId);
|
|
859
|
+
const savedStateForProbe = getOAuthState(this.config.clientId);
|
|
860
|
+
if (SILENT_PROBE_ERRORS.has(error) && probeState && savedStateForProbe && timingSafeEqualString(savedStateForProbe, returnedErrorState ?? "") && timingSafeEqualString(probeState, returnedErrorState ?? "")) {
|
|
861
|
+
removeOAuthState(this.config.clientId);
|
|
862
|
+
removeOAuthNonce(this.config.clientId);
|
|
863
|
+
removePkceVerifier(this.config.clientId);
|
|
864
|
+
removeSilentProbe(this.config.clientId);
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
811
867
|
removeOAuthState(this.config.clientId);
|
|
812
868
|
removeOAuthNonce(this.config.clientId);
|
|
813
869
|
removePkceVerifier(this.config.clientId);
|
|
870
|
+
removeSilentProbe(this.config.clientId);
|
|
814
871
|
const desc = params.get("error_description") || error;
|
|
815
872
|
throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
|
|
816
873
|
}
|
|
@@ -820,6 +877,7 @@ var _SsoClient = class _SsoClient {
|
|
|
820
877
|
removeOAuthState(this.config.clientId);
|
|
821
878
|
removeOAuthNonce(this.config.clientId);
|
|
822
879
|
removePkceVerifier(this.config.clientId);
|
|
880
|
+
removeSilentProbe(this.config.clientId);
|
|
823
881
|
throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
|
|
824
882
|
}
|
|
825
883
|
const savedState = getOAuthState(this.config.clientId);
|
|
@@ -827,6 +885,7 @@ var _SsoClient = class _SsoClient {
|
|
|
827
885
|
removeOAuthState(this.config.clientId);
|
|
828
886
|
removeOAuthNonce(this.config.clientId);
|
|
829
887
|
removePkceVerifier(this.config.clientId);
|
|
888
|
+
removeSilentProbe(this.config.clientId);
|
|
830
889
|
throw new SsoError(
|
|
831
890
|
"state_mismatch",
|
|
832
891
|
"State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
|
|
@@ -890,6 +949,7 @@ var _SsoClient = class _SsoClient {
|
|
|
890
949
|
removeOAuthState(this.config.clientId);
|
|
891
950
|
removeOAuthNonce(this.config.clientId);
|
|
892
951
|
removePkceVerifier(this.config.clientId);
|
|
952
|
+
removeSilentProbe(this.config.clientId);
|
|
893
953
|
const now = Date.now();
|
|
894
954
|
const tokenData = {
|
|
895
955
|
access_token: data.access_token,
|
|
@@ -1532,6 +1592,14 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1532
1592
|
try {
|
|
1533
1593
|
const tokenData = await client.handleCallback(window.location.href);
|
|
1534
1594
|
if (cancelled) return;
|
|
1595
|
+
if (tokenData === null) {
|
|
1596
|
+
const clientId2 = client.config.clientId;
|
|
1597
|
+
const probeReturnUrl = getReturnUrl(clientId2);
|
|
1598
|
+
removeReturnUrl(clientId2);
|
|
1599
|
+
const target = probeReturnUrl && isTrustedReturnUrl(probeReturnUrl, window.location.origin) ? probeReturnUrl : "/";
|
|
1600
|
+
window.location.href = target + (target.includes("?") ? "&" : "?") + "sso_probe=no_session";
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1535
1603
|
await refreshUser();
|
|
1536
1604
|
if (cancelled) return;
|
|
1537
1605
|
const clientId = client.config.clientId;
|