@nihplod/sso-sdk 1.4.1 → 1.5.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 +82 -9
- package/dist/index.d.mts +27 -3
- package/dist/index.d.ts +27 -3
- package/dist/index.js +169 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +169 -38
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +50 -0
- package/dist/next/index.d.ts +50 -0
- package/dist/next/index.js +304 -95
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +304 -95
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +25 -1
- package/dist/react/index.d.ts +25 -1
- package/dist/react/index.js +329 -71
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +329 -71
- package/dist/react/index.mjs.map +1 -1
- package/package.json +9 -3
package/dist/index.mjs
CHANGED
|
@@ -100,18 +100,25 @@ function createMemoryStorageAdapter() {
|
|
|
100
100
|
},
|
|
101
101
|
remove(key) {
|
|
102
102
|
store.delete(key);
|
|
103
|
+
},
|
|
104
|
+
keys() {
|
|
105
|
+
return [...store.keys()];
|
|
103
106
|
}
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
109
|
var memoryStorageAdapter = createMemoryStorageAdapter();
|
|
107
110
|
var localStorageAdapter = {
|
|
108
111
|
get(key) {
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
try {
|
|
113
|
+
if (typeof localStorage === "undefined") return null;
|
|
114
|
+
return localStorage.getItem(STORAGE_PREFIX + key);
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
111
118
|
},
|
|
112
119
|
set(key, value) {
|
|
113
|
-
if (typeof localStorage === "undefined") return;
|
|
114
120
|
try {
|
|
121
|
+
if (typeof localStorage === "undefined") return;
|
|
115
122
|
localStorage.setItem(STORAGE_PREFIX + key, value);
|
|
116
123
|
} catch (err) {
|
|
117
124
|
console.warn(
|
|
@@ -120,34 +127,68 @@ var localStorageAdapter = {
|
|
|
120
127
|
}
|
|
121
128
|
},
|
|
122
129
|
remove(key) {
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
try {
|
|
131
|
+
if (typeof localStorage === "undefined") return;
|
|
132
|
+
localStorage.removeItem(STORAGE_PREFIX + key);
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
keys() {
|
|
137
|
+
try {
|
|
138
|
+
if (typeof localStorage === "undefined") return [];
|
|
139
|
+
const result = [];
|
|
140
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
141
|
+
const key = localStorage.key(i);
|
|
142
|
+
if (key?.startsWith(STORAGE_PREFIX)) result.push(key.slice(STORAGE_PREFIX.length));
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
} catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
125
148
|
}
|
|
126
149
|
};
|
|
127
150
|
function createSessionStorageAdapter() {
|
|
128
151
|
const fallback = /* @__PURE__ */ new Map();
|
|
129
152
|
return {
|
|
130
153
|
get(key) {
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
try {
|
|
155
|
+
if (typeof sessionStorage !== "undefined") {
|
|
156
|
+
return sessionStorage.getItem(STORAGE_PREFIX + key) ?? fallback.get(key) ?? null;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
133
159
|
}
|
|
134
160
|
return fallback.get(key) ?? null;
|
|
135
161
|
},
|
|
136
162
|
set(key, value) {
|
|
137
|
-
if (typeof sessionStorage === "undefined") {
|
|
138
|
-
fallback.set(key, value);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
163
|
try {
|
|
142
|
-
|
|
164
|
+
if (typeof sessionStorage !== "undefined") {
|
|
165
|
+
sessionStorage.setItem(STORAGE_PREFIX + key, value);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
143
168
|
} catch {
|
|
144
|
-
fallback.set(key, value);
|
|
145
169
|
}
|
|
170
|
+
fallback.set(key, value);
|
|
146
171
|
},
|
|
147
172
|
remove(key) {
|
|
148
173
|
fallback.delete(key);
|
|
149
|
-
|
|
150
|
-
|
|
174
|
+
try {
|
|
175
|
+
if (typeof sessionStorage === "undefined") return;
|
|
176
|
+
sessionStorage.removeItem(STORAGE_PREFIX + key);
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
keys() {
|
|
181
|
+
const result = new Set(fallback.keys());
|
|
182
|
+
try {
|
|
183
|
+
if (typeof sessionStorage !== "undefined") {
|
|
184
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
185
|
+
const key = sessionStorage.key(i);
|
|
186
|
+
if (key?.startsWith(STORAGE_PREFIX)) result.add(key.slice(STORAGE_PREFIX.length));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
return [...result];
|
|
151
192
|
}
|
|
152
193
|
};
|
|
153
194
|
}
|
|
@@ -249,23 +290,44 @@ function clearAllSsoData(clientId) {
|
|
|
249
290
|
removeReturnUrl();
|
|
250
291
|
removeLogoutState();
|
|
251
292
|
removeSilentProbe();
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
typeof sessionStorage !== "undefined"
|
|
255
|
-
|
|
256
|
-
|
|
293
|
+
const stores = [];
|
|
294
|
+
try {
|
|
295
|
+
if (typeof sessionStorage !== "undefined") stores.push(sessionStorage);
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
if (typeof localStorage !== "undefined") stores.push(localStorage);
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
const abstractKeys = /* @__PURE__ */ new Set();
|
|
257
303
|
for (const store of stores) {
|
|
258
|
-
if (!store) continue;
|
|
259
304
|
const keys = [];
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
305
|
+
try {
|
|
306
|
+
for (let i = 0; i < store.length; i++) {
|
|
307
|
+
const key = store.key(i);
|
|
308
|
+
if (key?.startsWith(STORAGE_PREFIX)) keys.push(key);
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
continue;
|
|
263
312
|
}
|
|
264
313
|
for (const key of keys) {
|
|
265
|
-
|
|
266
|
-
|
|
314
|
+
abstractKeys.add(key.slice(STORAGE_PREFIX.length));
|
|
315
|
+
try {
|
|
316
|
+
store.removeItem(key);
|
|
317
|
+
} catch {
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const store of [_transient, _storage]) {
|
|
322
|
+
try {
|
|
323
|
+
for (const key of store.keys?.() ?? []) abstractKeys.add(key);
|
|
324
|
+
} catch {
|
|
267
325
|
}
|
|
268
326
|
}
|
|
327
|
+
for (const key of abstractKeys) {
|
|
328
|
+
_transient.remove(key);
|
|
329
|
+
_storage.remove(key);
|
|
330
|
+
}
|
|
269
331
|
}
|
|
270
332
|
function clearVerifiersForClients(clientIds) {
|
|
271
333
|
for (const clientId of clientIds) {
|
|
@@ -274,12 +336,17 @@ function clearVerifiersForClients(clientIds) {
|
|
|
274
336
|
}
|
|
275
337
|
|
|
276
338
|
// src/core/security.ts
|
|
339
|
+
var UNSAFE_URL_CHAR_PATTERN = /[\\\u0000-\u001F\u007F]/;
|
|
277
340
|
function isTrustedReturnUrl(url, currentOrigin) {
|
|
278
341
|
if (!url) return false;
|
|
279
|
-
if (
|
|
280
|
-
if (url.startsWith("
|
|
342
|
+
if (UNSAFE_URL_CHAR_PATTERN.test(url)) return false;
|
|
343
|
+
if (url.startsWith("//")) return false;
|
|
344
|
+
if (url.startsWith("/")) return true;
|
|
281
345
|
try {
|
|
282
|
-
|
|
346
|
+
const parsed = new URL(url);
|
|
347
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
348
|
+
if (parsed.username || parsed.password) return false;
|
|
349
|
+
return parsed.origin === currentOrigin;
|
|
283
350
|
} catch {
|
|
284
351
|
return false;
|
|
285
352
|
}
|
|
@@ -513,6 +580,34 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
513
580
|
}
|
|
514
581
|
|
|
515
582
|
// src/core/SsoClient.ts
|
|
583
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
584
|
+
var REVOKE_TIMEOUT_MS = 3e3;
|
|
585
|
+
async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
586
|
+
const controller = new AbortController();
|
|
587
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
588
|
+
try {
|
|
589
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
590
|
+
} finally {
|
|
591
|
+
clearTimeout(timer);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function assertValidTokenResponse(data, requireRefreshToken) {
|
|
595
|
+
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
596
|
+
throw new SsoError("token_request_failed", "Token \u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
597
|
+
}
|
|
598
|
+
if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
|
|
599
|
+
throw new SsoError(
|
|
600
|
+
"token_request_failed",
|
|
601
|
+
`Token \u54CD\u5E94 expires_in \u975E\u6CD5: ${String(data.expires_in)}`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
if (requireRefreshToken && (typeof data.refresh_token !== "string" || !data.refresh_token)) {
|
|
605
|
+
throw new SsoError(
|
|
606
|
+
"token_request_failed",
|
|
607
|
+
"Token \u54CD\u5E94\u7F3A\u5C11 refresh_token\uFF08authorization_code \u4EA4\u6362\u5FC5\u987B\u8FD4\u56DE\uFF09"
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
516
611
|
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
|
|
517
612
|
"login_required",
|
|
518
613
|
"consent_required",
|
|
@@ -531,6 +626,19 @@ var _SsoClient = class _SsoClient {
|
|
|
531
626
|
this._serverBase = (config.serverBaseUrl ?? base).replace(/\/+$/, "");
|
|
532
627
|
}
|
|
533
628
|
// ============================================
|
|
629
|
+
// 调试日志
|
|
630
|
+
// ============================================
|
|
631
|
+
/**
|
|
632
|
+
* 调试日志(config.debug=true 时输出,前缀 [SSO SDK]):
|
|
633
|
+
* 覆盖登录发起 / 回调结果 / 刷新失败 / 登出等关键节点,便于接入方定位问题。
|
|
634
|
+
* 不会输出 token/授权码等敏感值。
|
|
635
|
+
*/
|
|
636
|
+
debugLog(...args) {
|
|
637
|
+
if (this.config.debug) {
|
|
638
|
+
console.warn("[SSO SDK]", ...args);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// ============================================
|
|
534
642
|
// 内部方法
|
|
535
643
|
// ============================================
|
|
536
644
|
/**
|
|
@@ -910,7 +1018,7 @@ var _SsoClient = class _SsoClient {
|
|
|
910
1018
|
}
|
|
911
1019
|
let res;
|
|
912
1020
|
try {
|
|
913
|
-
res = await
|
|
1021
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
914
1022
|
method: "POST",
|
|
915
1023
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
916
1024
|
body: body.toString()
|
|
@@ -931,6 +1039,15 @@ var _SsoClient = class _SsoClient {
|
|
|
931
1039
|
);
|
|
932
1040
|
}
|
|
933
1041
|
const data = await res.json();
|
|
1042
|
+
assertValidTokenResponse(data, true);
|
|
1043
|
+
const requestedScopes = (this.config.scopes || "openid profile").split(" ").filter(Boolean);
|
|
1044
|
+
if (requestedScopes.includes("openid") && !data.id_token) {
|
|
1045
|
+
removeTokenData(this.config.clientId);
|
|
1046
|
+
throw new SsoError(
|
|
1047
|
+
"id_token_invalid",
|
|
1048
|
+
"Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09"
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
934
1051
|
if (data.id_token) {
|
|
935
1052
|
try {
|
|
936
1053
|
await validateIdToken(
|
|
@@ -960,6 +1077,7 @@ var _SsoClient = class _SsoClient {
|
|
|
960
1077
|
expires_at: now + data.expires_in * 1e3
|
|
961
1078
|
};
|
|
962
1079
|
saveTokenData(tokenData, this.config.clientId);
|
|
1080
|
+
this.debugLog("handleCallback: token \u4EA4\u6362\u6210\u529F", { clientId: this.config.clientId });
|
|
963
1081
|
return tokenData;
|
|
964
1082
|
}
|
|
965
1083
|
/**
|
|
@@ -995,7 +1113,7 @@ var _SsoClient = class _SsoClient {
|
|
|
995
1113
|
let lastErr;
|
|
996
1114
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
997
1115
|
try {
|
|
998
|
-
res = await
|
|
1116
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
999
1117
|
method: "POST",
|
|
1000
1118
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1001
1119
|
body: body.toString()
|
|
@@ -1017,15 +1135,20 @@ var _SsoClient = class _SsoClient {
|
|
|
1017
1135
|
} catch {
|
|
1018
1136
|
}
|
|
1019
1137
|
const errorCode = errData.error || "";
|
|
1020
|
-
if (errorCode === "invalid_grant"
|
|
1138
|
+
if (errorCode === "invalid_grant") {
|
|
1021
1139
|
removeTokenData(this.config.clientId);
|
|
1022
1140
|
}
|
|
1141
|
+
this.debugLog("refreshToken: \u5237\u65B0\u5931\u8D25", { status: res.status, error: errorCode });
|
|
1142
|
+
if (!errorCode && res.status === 401) {
|
|
1143
|
+
throw new SsoError("sso_server_error", "\u5237\u65B0 Token \u5931\u8D25\uFF08\u7F51\u5173\u5F02\u5E38\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
1144
|
+
}
|
|
1023
1145
|
throw new SsoError(
|
|
1024
1146
|
mapOAuthErrorToSsoCode(errorCode, "refresh"),
|
|
1025
1147
|
errData.error_description || `\u5237\u65B0 Token \u5931\u8D25: HTTP ${res.status}`
|
|
1026
1148
|
);
|
|
1027
1149
|
}
|
|
1028
1150
|
const data = await res.json();
|
|
1151
|
+
assertValidTokenResponse(data, false);
|
|
1029
1152
|
if (data.id_token) {
|
|
1030
1153
|
try {
|
|
1031
1154
|
await validateIdToken(
|
|
@@ -1069,7 +1192,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1069
1192
|
const userinfoEndpoint = await this._getUserinfoEndpoint();
|
|
1070
1193
|
let res;
|
|
1071
1194
|
try {
|
|
1072
|
-
res = await
|
|
1195
|
+
res = await fetchWithTimeout(userinfoEndpoint, {
|
|
1073
1196
|
headers: {
|
|
1074
1197
|
Authorization: `Bearer ${tokenData.access_token}`
|
|
1075
1198
|
}
|
|
@@ -1126,6 +1249,10 @@ var _SsoClient = class _SsoClient {
|
|
|
1126
1249
|
const refreshToken = tokenData?.refresh_token;
|
|
1127
1250
|
const idTokenHint = tokenData?.id_token;
|
|
1128
1251
|
clearAllSsoData(this.config.clientId);
|
|
1252
|
+
this.debugLog("logout: \u672C\u5730\u6570\u636E\u5DF2\u6E05\u9664", {
|
|
1253
|
+
clientId: this.config.clientId,
|
|
1254
|
+
redirectToSso
|
|
1255
|
+
});
|
|
1129
1256
|
if (refreshToken && this.config.clientId) {
|
|
1130
1257
|
try {
|
|
1131
1258
|
const discovery = this.config.serverBaseUrl ? null : await this._getDiscovery();
|
|
@@ -1138,11 +1265,15 @@ var _SsoClient = class _SsoClient {
|
|
|
1138
1265
|
if (this.config.clientSecret) {
|
|
1139
1266
|
revokeBody.set("client_secret", this.config.clientSecret);
|
|
1140
1267
|
}
|
|
1141
|
-
await
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1268
|
+
await fetchWithTimeout(
|
|
1269
|
+
revokeUrl,
|
|
1270
|
+
{
|
|
1271
|
+
method: "POST",
|
|
1272
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1273
|
+
body: revokeBody.toString()
|
|
1274
|
+
},
|
|
1275
|
+
REVOKE_TIMEOUT_MS
|
|
1276
|
+
);
|
|
1146
1277
|
} catch {
|
|
1147
1278
|
}
|
|
1148
1279
|
}
|