@nihplod/sso-sdk 1.1.0 → 1.2.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.
- package/README.md +73 -6
- package/dist/index.d.mts +51 -10
- package/dist/index.d.ts +51 -10
- package/dist/index.js +68 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +65 -9
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +38 -5
- package/dist/next/index.d.ts +38 -5
- package/dist/next/index.js +90 -49
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +88 -48
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +113 -10
- package/dist/react/index.d.ts +113 -10
- package/dist/react/index.js +166 -55
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +169 -59
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ __export(src_exports, {
|
|
|
29
29
|
generateCodeChallenge: () => generateCodeChallenge,
|
|
30
30
|
generateCodeVerifier: () => generateCodeVerifier,
|
|
31
31
|
generateState: () => generateState,
|
|
32
|
+
getLogoutState: () => getLogoutState,
|
|
32
33
|
getOAuthState: () => getOAuthState,
|
|
33
34
|
getPkceVerifier: () => getPkceVerifier,
|
|
34
35
|
getReturnUrl: () => getReturnUrl,
|
|
@@ -36,10 +37,12 @@ __export(src_exports, {
|
|
|
36
37
|
getTokenStorage: () => getTokenStorage,
|
|
37
38
|
isTrustedReturnUrl: () => isTrustedReturnUrl,
|
|
38
39
|
mapOAuthErrorToSsoCode: () => mapOAuthErrorToSsoCode,
|
|
40
|
+
removeLogoutState: () => removeLogoutState,
|
|
39
41
|
removeOAuthState: () => removeOAuthState,
|
|
40
42
|
removePkceVerifier: () => removePkceVerifier,
|
|
41
43
|
removeReturnUrl: () => removeReturnUrl,
|
|
42
44
|
removeTokenData: () => removeTokenData,
|
|
45
|
+
saveLogoutState: () => saveLogoutState,
|
|
43
46
|
saveOAuthState: () => saveOAuthState,
|
|
44
47
|
savePkceVerifier: () => savePkceVerifier,
|
|
45
48
|
saveReturnUrl: () => saveReturnUrl,
|
|
@@ -134,6 +137,7 @@ var TOKEN_KEY = "token";
|
|
|
134
137
|
var VERIFIER_KEY_PREFIX = "pkce_verifier_";
|
|
135
138
|
var STATE_KEY = "oauth_state";
|
|
136
139
|
var RETURN_URL_KEY = "return_url";
|
|
140
|
+
var LOGOUT_STATE_KEY = "logout_state";
|
|
137
141
|
function buildKey(base, clientId) {
|
|
138
142
|
return clientId ? `${base}:${clientId}` : base;
|
|
139
143
|
}
|
|
@@ -201,9 +205,9 @@ function createSessionStorageAdapter() {
|
|
|
201
205
|
}
|
|
202
206
|
var _transient = createSessionStorageAdapter();
|
|
203
207
|
function createSecureStorage(options = {}) {
|
|
204
|
-
return options.persist ? localStorageAdapter :
|
|
208
|
+
return options.persist ? localStorageAdapter : createSessionStorageAdapter();
|
|
205
209
|
}
|
|
206
|
-
var _storage =
|
|
210
|
+
var _storage = createSessionStorageAdapter();
|
|
207
211
|
function setTokenStorage(storage) {
|
|
208
212
|
_storage = storage;
|
|
209
213
|
}
|
|
@@ -243,6 +247,15 @@ function getOAuthState(clientId) {
|
|
|
243
247
|
function removeOAuthState(clientId) {
|
|
244
248
|
_transient.remove(buildKey(STATE_KEY, clientId));
|
|
245
249
|
}
|
|
250
|
+
function saveLogoutState(state, clientId) {
|
|
251
|
+
_transient.set(buildKey(LOGOUT_STATE_KEY, clientId), state);
|
|
252
|
+
}
|
|
253
|
+
function getLogoutState(clientId) {
|
|
254
|
+
return _transient.get(buildKey(LOGOUT_STATE_KEY, clientId));
|
|
255
|
+
}
|
|
256
|
+
function removeLogoutState(clientId) {
|
|
257
|
+
_transient.remove(buildKey(LOGOUT_STATE_KEY, clientId));
|
|
258
|
+
}
|
|
246
259
|
function saveReturnUrl(url, clientId) {
|
|
247
260
|
_transient.set(buildKey(RETURN_URL_KEY, clientId), url);
|
|
248
261
|
}
|
|
@@ -257,6 +270,7 @@ function clearAllSsoData(clientId) {
|
|
|
257
270
|
removeTokenData(clientId);
|
|
258
271
|
removeOAuthState(clientId);
|
|
259
272
|
removeReturnUrl(clientId);
|
|
273
|
+
removeLogoutState(clientId);
|
|
260
274
|
removePkceVerifier(clientId);
|
|
261
275
|
removePkceVerifier(`${clientId}_popup_nonce`);
|
|
262
276
|
return;
|
|
@@ -264,6 +278,7 @@ function clearAllSsoData(clientId) {
|
|
|
264
278
|
removeTokenData();
|
|
265
279
|
removeOAuthState();
|
|
266
280
|
removeReturnUrl();
|
|
281
|
+
removeLogoutState();
|
|
267
282
|
const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;
|
|
268
283
|
const stores = [
|
|
269
284
|
typeof sessionStorage !== "undefined" ? sessionStorage : null,
|
|
@@ -291,6 +306,7 @@ function clearVerifiersForClients(clientIds) {
|
|
|
291
306
|
// src/core/security.ts
|
|
292
307
|
function isTrustedReturnUrl(url, currentOrigin) {
|
|
293
308
|
if (!url) return false;
|
|
309
|
+
if (url.includes("\\")) return false;
|
|
294
310
|
if (url.startsWith("/") && !url.startsWith("//")) return true;
|
|
295
311
|
try {
|
|
296
312
|
return new URL(url).origin === currentOrigin;
|
|
@@ -496,7 +512,7 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
496
512
|
if (typeof payload.exp !== "number") {
|
|
497
513
|
throw new SsoError("id_token_invalid", "ID Token \u7F3A\u5C11 exp \u58F0\u660E");
|
|
498
514
|
}
|
|
499
|
-
if (Date.now() >= payload.exp * 1e3) {
|
|
515
|
+
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
500
516
|
throw new SsoError("id_token_expired", "ID Token \u5DF2\u8FC7\u671F");
|
|
501
517
|
}
|
|
502
518
|
if (typeof payload.iat === "number" && payload.iat * 1e3 > Date.now() + 6e4) {
|
|
@@ -589,7 +605,7 @@ var _SsoClient = class _SsoClient {
|
|
|
589
605
|
*/
|
|
590
606
|
_saveReturnUrlIfTrusted(returnUrl) {
|
|
591
607
|
if (isTrustedReturnUrl(returnUrl, window.location.origin)) {
|
|
592
|
-
saveReturnUrl(returnUrl);
|
|
608
|
+
saveReturnUrl(returnUrl, this.config.clientId);
|
|
593
609
|
} else {
|
|
594
610
|
console.warn(`[SSO SDK] returnUrl \u672A\u901A\u8FC7\u540C\u6E90\u6821\u9A8C\uFF0C\u5DF2\u5FFD\u7565: ${returnUrl}`);
|
|
595
611
|
}
|
|
@@ -605,6 +621,10 @@ var _SsoClient = class _SsoClient {
|
|
|
605
621
|
*
|
|
606
622
|
* @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;
|
|
607
623
|
* 仅允许相对路径或同源绝对 URL,否则忽略并告警)
|
|
624
|
+
*
|
|
625
|
+
* ⚠️ 不要与 getLoginUrl() 混用:两者都会重新生成并覆盖 sessionStorage 中的
|
|
626
|
+
* state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
|
|
627
|
+
* 同一次登录只使用其中一个入口。
|
|
608
628
|
*/
|
|
609
629
|
async login(returnUrl) {
|
|
610
630
|
const verifier = generateCodeVerifier();
|
|
@@ -630,6 +650,10 @@ var _SsoClient = class _SsoClient {
|
|
|
630
650
|
* 构建登录 URL(不跳转,返回 URL 字符串)
|
|
631
651
|
*
|
|
632
652
|
* 适用于需要手动处理跳转的场景。
|
|
653
|
+
*
|
|
654
|
+
* ⚠️ 不要与 login() 混用:两者都会重新生成并覆盖 sessionStorage 中的
|
|
655
|
+
* state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
|
|
656
|
+
* 同一次登录只使用其中一个入口。
|
|
633
657
|
*/
|
|
634
658
|
async getLoginUrl(returnUrl) {
|
|
635
659
|
const verifier = generateCodeVerifier();
|
|
@@ -782,7 +806,7 @@ var _SsoClient = class _SsoClient {
|
|
|
782
806
|
* 处理 OAuth 回调
|
|
783
807
|
*
|
|
784
808
|
* 解析回调 URL,校验 state 参数,用授权码交换 token。
|
|
785
|
-
* 成功后 token 自动保存到 token
|
|
809
|
+
* 成功后 token 自动保存到 token 存储(默认 sessionStorage,可通过 setTokenStorage 定制)。
|
|
786
810
|
*
|
|
787
811
|
* @param callbackUrl - 完整的回调 URL(window.location.href)
|
|
788
812
|
* @returns TokenData 或 null
|
|
@@ -792,12 +816,16 @@ var _SsoClient = class _SsoClient {
|
|
|
792
816
|
const params = url.searchParams;
|
|
793
817
|
const error = params.get("error");
|
|
794
818
|
if (error) {
|
|
819
|
+
removeOAuthState(this.config.clientId);
|
|
820
|
+
removePkceVerifier(this.config.clientId);
|
|
795
821
|
const desc = params.get("error_description") || error;
|
|
796
|
-
throw new SsoError(
|
|
822
|
+
throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
|
|
797
823
|
}
|
|
798
824
|
const code = params.get("code");
|
|
799
825
|
const returnedState = params.get("state");
|
|
800
826
|
if (!code) {
|
|
827
|
+
removeOAuthState(this.config.clientId);
|
|
828
|
+
removePkceVerifier(this.config.clientId);
|
|
801
829
|
throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
|
|
802
830
|
}
|
|
803
831
|
const savedState = getOAuthState(this.config.clientId);
|
|
@@ -849,8 +877,6 @@ var _SsoClient = class _SsoClient {
|
|
|
849
877
|
);
|
|
850
878
|
}
|
|
851
879
|
const data = await res.json();
|
|
852
|
-
removeOAuthState(this.config.clientId);
|
|
853
|
-
removePkceVerifier(this.config.clientId);
|
|
854
880
|
if (data.id_token) {
|
|
855
881
|
try {
|
|
856
882
|
await validateIdToken(
|
|
@@ -864,6 +890,8 @@ var _SsoClient = class _SsoClient {
|
|
|
864
890
|
throw err;
|
|
865
891
|
}
|
|
866
892
|
}
|
|
893
|
+
removeOAuthState(this.config.clientId);
|
|
894
|
+
removePkceVerifier(this.config.clientId);
|
|
867
895
|
const now = Date.now();
|
|
868
896
|
const tokenData = {
|
|
869
897
|
access_token: data.access_token,
|
|
@@ -1032,7 +1060,9 @@ var _SsoClient = class _SsoClient {
|
|
|
1032
1060
|
* 登出
|
|
1033
1061
|
*
|
|
1034
1062
|
* 清除本地所有 token 和临时数据,并尝试撤销服务端 refresh_token。
|
|
1035
|
-
* @param redirectToSso - 是否重定向到 SSO 登出页(默认 false
|
|
1063
|
+
* @param redirectToSso - 是否重定向到 SSO 登出页(默认 false)。
|
|
1064
|
+
* 为 true 时携带 state 参数(已保存到 sessionStorage),
|
|
1065
|
+
* 回跳页面应调用 validateLogoutState() 校验防登出 CSRF。
|
|
1036
1066
|
*/
|
|
1037
1067
|
async logout(redirectToSso = false) {
|
|
1038
1068
|
const tokenData = getTokenData(this.config.clientId);
|
|
@@ -1075,10 +1105,36 @@ var _SsoClient = class _SsoClient {
|
|
|
1075
1105
|
logoutUrl.searchParams.set("id_token_hint", idTokenHint);
|
|
1076
1106
|
}
|
|
1077
1107
|
const state = generateState();
|
|
1108
|
+
saveLogoutState(state, this.config.clientId);
|
|
1078
1109
|
logoutUrl.searchParams.set("state", state);
|
|
1079
1110
|
window.location.href = logoutUrl.toString();
|
|
1080
1111
|
}
|
|
1081
1112
|
}
|
|
1113
|
+
/**
|
|
1114
|
+
* 校验 RP-Initiated Logout 回跳的 state 参数(登出 CSRF 防护)
|
|
1115
|
+
*
|
|
1116
|
+
* 在 post_logout_redirect_uri 指向的页面加载时调用;
|
|
1117
|
+
* 仅在 URL 携带 state 且与 logout(redirectToSso=true) 保存的值一致时返回 true,
|
|
1118
|
+
* 校验后清除已保存的 logout state(一次性)。
|
|
1119
|
+
*
|
|
1120
|
+
* @param url - 当前页面完整 URL(window.location.href)
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```typescript
|
|
1124
|
+
* if (sso.validateLogoutState(window.location.href)) {
|
|
1125
|
+
* // 来自 SSO 登出的可信回跳
|
|
1126
|
+
* }
|
|
1127
|
+
* ```
|
|
1128
|
+
*/
|
|
1129
|
+
validateLogoutState(url) {
|
|
1130
|
+
const returnedState = new URL(url).searchParams.get("state");
|
|
1131
|
+
if (!returnedState) return false;
|
|
1132
|
+
const savedState = getLogoutState(this.config.clientId);
|
|
1133
|
+
if (!savedState) return false;
|
|
1134
|
+
if (!timingSafeEqualString(savedState, returnedState)) return false;
|
|
1135
|
+
removeLogoutState(this.config.clientId);
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1082
1138
|
/**
|
|
1083
1139
|
* 获取 OIDC Discovery 文档
|
|
1084
1140
|
*
|
|
@@ -1105,6 +1161,7 @@ var SsoClient = _SsoClient;
|
|
|
1105
1161
|
generateCodeChallenge,
|
|
1106
1162
|
generateCodeVerifier,
|
|
1107
1163
|
generateState,
|
|
1164
|
+
getLogoutState,
|
|
1108
1165
|
getOAuthState,
|
|
1109
1166
|
getPkceVerifier,
|
|
1110
1167
|
getReturnUrl,
|
|
@@ -1112,10 +1169,12 @@ var SsoClient = _SsoClient;
|
|
|
1112
1169
|
getTokenStorage,
|
|
1113
1170
|
isTrustedReturnUrl,
|
|
1114
1171
|
mapOAuthErrorToSsoCode,
|
|
1172
|
+
removeLogoutState,
|
|
1115
1173
|
removeOAuthState,
|
|
1116
1174
|
removePkceVerifier,
|
|
1117
1175
|
removeReturnUrl,
|
|
1118
1176
|
removeTokenData,
|
|
1177
|
+
saveLogoutState,
|
|
1119
1178
|
saveOAuthState,
|
|
1120
1179
|
savePkceVerifier,
|
|
1121
1180
|
saveReturnUrl,
|