@nihplod/sso-sdk 1.4.1 → 1.6.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 +101 -9
- package/dist/index.d.mts +52 -3
- package/dist/index.d.ts +52 -3
- package/dist/index.js +246 -42
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +246 -42
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +88 -2
- package/dist/next/index.d.ts +88 -2
- package/dist/next/index.js +380 -109
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +380 -109
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +50 -1
- package/dist/react/index.d.ts +50 -1
- package/dist/react/index.js +414 -75
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +414 -75
- 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
|
+
}
|
|
267
319
|
}
|
|
268
320
|
}
|
|
321
|
+
for (const store of [_transient, _storage]) {
|
|
322
|
+
try {
|
|
323
|
+
for (const key of store.keys?.() ?? []) abstractKeys.add(key);
|
|
324
|
+
} catch {
|
|
325
|
+
}
|
|
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
|
}
|
|
@@ -491,7 +558,10 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
491
558
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
492
559
|
throw new SsoError("id_token_expired", "ID Token \u5DF2\u8FC7\u671F");
|
|
493
560
|
}
|
|
494
|
-
if (typeof payload.iat
|
|
561
|
+
if (typeof payload.iat !== "number") {
|
|
562
|
+
throw new SsoError("id_token_invalid", "ID Token \u7F3A\u5C11 iat \u58F0\u660E");
|
|
563
|
+
}
|
|
564
|
+
if (payload.iat * 1e3 > Date.now() + 6e4) {
|
|
495
565
|
throw new SsoError("id_token_invalid", "ID Token iat \u5728\u672A\u6765\uFF0C\u7591\u4F3C\u4F2A\u9020\u6216\u65F6\u949F\u5F02\u5E38");
|
|
496
566
|
}
|
|
497
567
|
if (typeof payload.sub !== "string" || !payload.sub) {
|
|
@@ -508,11 +578,47 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
508
578
|
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
509
579
|
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
510
580
|
}
|
|
581
|
+
} else if (typeof payload.nonce === "string" && payload.nonce) {
|
|
582
|
+
throw new SsoError(
|
|
583
|
+
"id_token_nonce_mismatch",
|
|
584
|
+
"ID Token \u643A\u5E26 nonce \u4F46\u672C\u5730\u65E0\u671F\u671B\u503C\uFF08\u53EF\u80FD\u975E\u672C\u6B21\u767B\u5F55\u4F1A\u8BDD\u7B7E\u53D1\uFF09"
|
|
585
|
+
);
|
|
511
586
|
}
|
|
512
587
|
return { sub: payload.sub };
|
|
513
588
|
}
|
|
514
589
|
|
|
590
|
+
// src/core/http.ts
|
|
591
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
592
|
+
async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
593
|
+
const controller = new AbortController();
|
|
594
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
595
|
+
try {
|
|
596
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
597
|
+
} finally {
|
|
598
|
+
clearTimeout(timer);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
515
602
|
// src/core/SsoClient.ts
|
|
603
|
+
var REVOKE_TIMEOUT_MS = 3e3;
|
|
604
|
+
var EXPIRY_SKEW_MS = 1e4;
|
|
605
|
+
function assertValidTokenResponse(data, requireRefreshToken) {
|
|
606
|
+
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
607
|
+
throw new SsoError("token_request_failed", "Token \u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
608
|
+
}
|
|
609
|
+
if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
|
|
610
|
+
throw new SsoError(
|
|
611
|
+
"token_request_failed",
|
|
612
|
+
`Token \u54CD\u5E94 expires_in \u975E\u6CD5: ${String(data.expires_in)}`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
if (requireRefreshToken && (typeof data.refresh_token !== "string" || !data.refresh_token)) {
|
|
616
|
+
throw new SsoError(
|
|
617
|
+
"token_request_failed",
|
|
618
|
+
"Token \u54CD\u5E94\u7F3A\u5C11 refresh_token\uFF08authorization_code \u4EA4\u6362\u5FC5\u987B\u8FD4\u56DE\uFF09"
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
516
622
|
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
|
|
517
623
|
"login_required",
|
|
518
624
|
"consent_required",
|
|
@@ -523,6 +629,8 @@ var _SsoClient = class _SsoClient {
|
|
|
523
629
|
this._discovery = null;
|
|
524
630
|
this._discoveryFetchedAt = 0;
|
|
525
631
|
this._refreshLock = null;
|
|
632
|
+
/** Discovery 端点 origin 校验失败只告警一次(拒绝的文档不缓存,避免每次调用刷屏) */
|
|
633
|
+
this._discoveryOriginWarned = false;
|
|
526
634
|
if (!config.clientId) throw new SsoError("invalid_config", "clientId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
527
635
|
if (!config.redirectUri) throw new SsoError("invalid_config", "redirectUri \u4E0D\u80FD\u4E3A\u7A7A");
|
|
528
636
|
if (!config.ssoBaseUrl) throw new SsoError("invalid_config", "ssoBaseUrl \u4E0D\u80FD\u4E3A\u7A7A");
|
|
@@ -531,8 +639,67 @@ var _SsoClient = class _SsoClient {
|
|
|
531
639
|
this._serverBase = (config.serverBaseUrl ?? base).replace(/\/+$/, "");
|
|
532
640
|
}
|
|
533
641
|
// ============================================
|
|
642
|
+
// 调试日志
|
|
643
|
+
// ============================================
|
|
644
|
+
/**
|
|
645
|
+
* 调试日志(config.debug=true 时输出,前缀 [SSO SDK]):
|
|
646
|
+
* 覆盖登录发起 / 回调结果 / 刷新失败 / 登出等关键节点,便于接入方定位问题。
|
|
647
|
+
* 不会输出 token/授权码等敏感值。
|
|
648
|
+
*/
|
|
649
|
+
debugLog(...args) {
|
|
650
|
+
if (this.config.debug) {
|
|
651
|
+
console.warn("[SSO SDK]", ...args);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// ============================================
|
|
534
655
|
// 内部方法
|
|
535
656
|
// ============================================
|
|
657
|
+
/**
|
|
658
|
+
* 校验 Discovery 文档端点 origin(防文档被篡改后 token/code 被导向攻击者服务器):
|
|
659
|
+
* - authorization_endpoint 必须与 ssoBaseUrl 同源(浏览器跳转目标);
|
|
660
|
+
* - token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl 同源,
|
|
661
|
+
* 或(配置了 serverBaseUrl 时)与内网地址同源。
|
|
662
|
+
* 任一不匹配:告警并返回 null(上层回退到默认端点,fail-safe)。
|
|
663
|
+
* 可通过 config.allowCrossOriginDiscoveryEndpoints 关闭(见配置项警告)。
|
|
664
|
+
*/
|
|
665
|
+
_sanitizeDiscovery(d) {
|
|
666
|
+
if (this.config.allowCrossOriginDiscoveryEndpoints) return d;
|
|
667
|
+
const allowedOrigins = /* @__PURE__ */ new Set();
|
|
668
|
+
try {
|
|
669
|
+
allowedOrigins.add(new URL(this.config.ssoBaseUrl).origin);
|
|
670
|
+
allowedOrigins.add(new URL(this._serverBase).origin);
|
|
671
|
+
} catch {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
const sameOrigin = (endpoint) => {
|
|
675
|
+
try {
|
|
676
|
+
return allowedOrigins.has(new URL(endpoint).origin);
|
|
677
|
+
} catch {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const publicOrigin = new URL(this.config.ssoBaseUrl).origin;
|
|
682
|
+
let authorizationOk = false;
|
|
683
|
+
try {
|
|
684
|
+
authorizationOk = new URL(d.authorization_endpoint).origin === publicOrigin;
|
|
685
|
+
} catch {
|
|
686
|
+
authorizationOk = false;
|
|
687
|
+
}
|
|
688
|
+
const bad = [];
|
|
689
|
+
if (!authorizationOk) bad.push("authorization_endpoint");
|
|
690
|
+
if (!sameOrigin(d.token_endpoint)) bad.push("token_endpoint");
|
|
691
|
+
if (!sameOrigin(d.userinfo_endpoint)) bad.push("userinfo_endpoint");
|
|
692
|
+
if (bad.length > 0) {
|
|
693
|
+
if (!this._discoveryOriginWarned) {
|
|
694
|
+
this._discoveryOriginWarned = true;
|
|
695
|
+
console.warn(
|
|
696
|
+
`[SSO SDK] OIDC Discovery \u7AEF\u70B9 origin \u4E0E ssoBaseUrl/serverBaseUrl \u4E0D\u5339\u914D\uFF08${bad.join(", ")}\uFF09\uFF0C\u5DF2\u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9\u3002\u82E5 SSO \u4E2D\u5FC3\u786E\u5B9E\u6258\u7BA1\u8DE8\u6E90\u7AEF\u70B9\uFF0C\u8BF7\u8BBE\u7F6E allowCrossOriginDiscoveryEndpoints: true`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
return d;
|
|
702
|
+
}
|
|
536
703
|
/**
|
|
537
704
|
* 获取 OIDC Discovery 文档(带缓存 + 超时)
|
|
538
705
|
*
|
|
@@ -559,7 +726,10 @@ var _SsoClient = class _SsoClient {
|
|
|
559
726
|
);
|
|
560
727
|
return null;
|
|
561
728
|
}
|
|
562
|
-
|
|
729
|
+
const doc = await res.json();
|
|
730
|
+
const sanitized = this._sanitizeDiscovery(doc);
|
|
731
|
+
if (!sanitized) return null;
|
|
732
|
+
this._discovery = sanitized;
|
|
563
733
|
this._discoveryFetchedAt = now;
|
|
564
734
|
return this._discovery;
|
|
565
735
|
} catch (err) {
|
|
@@ -910,7 +1080,7 @@ var _SsoClient = class _SsoClient {
|
|
|
910
1080
|
}
|
|
911
1081
|
let res;
|
|
912
1082
|
try {
|
|
913
|
-
res = await
|
|
1083
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
914
1084
|
method: "POST",
|
|
915
1085
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
916
1086
|
body: body.toString()
|
|
@@ -919,6 +1089,10 @@ var _SsoClient = class _SsoClient {
|
|
|
919
1089
|
throw new SsoError("network_error", "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", err);
|
|
920
1090
|
}
|
|
921
1091
|
if (!res.ok) {
|
|
1092
|
+
removeOAuthState(this.config.clientId);
|
|
1093
|
+
removeOAuthNonce(this.config.clientId);
|
|
1094
|
+
removePkceVerifier(this.config.clientId);
|
|
1095
|
+
removeSilentProbe(this.config.clientId);
|
|
922
1096
|
let errData = {};
|
|
923
1097
|
try {
|
|
924
1098
|
errData = await res.json();
|
|
@@ -931,6 +1105,19 @@ var _SsoClient = class _SsoClient {
|
|
|
931
1105
|
);
|
|
932
1106
|
}
|
|
933
1107
|
const data = await res.json();
|
|
1108
|
+
assertValidTokenResponse(data, true);
|
|
1109
|
+
const requestedScopes = (this.config.scopes || "openid profile").split(" ").filter(Boolean);
|
|
1110
|
+
if (requestedScopes.includes("openid") && !data.id_token) {
|
|
1111
|
+
removeOAuthState(this.config.clientId);
|
|
1112
|
+
removeOAuthNonce(this.config.clientId);
|
|
1113
|
+
removePkceVerifier(this.config.clientId);
|
|
1114
|
+
removeSilentProbe(this.config.clientId);
|
|
1115
|
+
removeTokenData(this.config.clientId);
|
|
1116
|
+
throw new SsoError(
|
|
1117
|
+
"id_token_invalid",
|
|
1118
|
+
"Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09"
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
934
1121
|
if (data.id_token) {
|
|
935
1122
|
try {
|
|
936
1123
|
await validateIdToken(
|
|
@@ -960,6 +1147,7 @@ var _SsoClient = class _SsoClient {
|
|
|
960
1147
|
expires_at: now + data.expires_in * 1e3
|
|
961
1148
|
};
|
|
962
1149
|
saveTokenData(tokenData, this.config.clientId);
|
|
1150
|
+
this.debugLog("handleCallback: token \u4EA4\u6362\u6210\u529F", { clientId: this.config.clientId });
|
|
963
1151
|
return tokenData;
|
|
964
1152
|
}
|
|
965
1153
|
/**
|
|
@@ -995,7 +1183,7 @@ var _SsoClient = class _SsoClient {
|
|
|
995
1183
|
let lastErr;
|
|
996
1184
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
997
1185
|
try {
|
|
998
|
-
res = await
|
|
1186
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
999
1187
|
method: "POST",
|
|
1000
1188
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1001
1189
|
body: body.toString()
|
|
@@ -1017,15 +1205,20 @@ var _SsoClient = class _SsoClient {
|
|
|
1017
1205
|
} catch {
|
|
1018
1206
|
}
|
|
1019
1207
|
const errorCode = errData.error || "";
|
|
1020
|
-
if (errorCode === "invalid_grant"
|
|
1208
|
+
if (errorCode === "invalid_grant") {
|
|
1021
1209
|
removeTokenData(this.config.clientId);
|
|
1022
1210
|
}
|
|
1211
|
+
this.debugLog("refreshToken: \u5237\u65B0\u5931\u8D25", { status: res.status, error: errorCode });
|
|
1212
|
+
if (!errorCode && res.status === 401) {
|
|
1213
|
+
throw new SsoError("sso_server_error", "\u5237\u65B0 Token \u5931\u8D25\uFF08\u7F51\u5173\u5F02\u5E38\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
1214
|
+
}
|
|
1023
1215
|
throw new SsoError(
|
|
1024
1216
|
mapOAuthErrorToSsoCode(errorCode, "refresh"),
|
|
1025
1217
|
errData.error_description || `\u5237\u65B0 Token \u5931\u8D25: HTTP ${res.status}`
|
|
1026
1218
|
);
|
|
1027
1219
|
}
|
|
1028
1220
|
const data = await res.json();
|
|
1221
|
+
assertValidTokenResponse(data, false);
|
|
1029
1222
|
if (data.id_token) {
|
|
1030
1223
|
try {
|
|
1031
1224
|
await validateIdToken(
|
|
@@ -1063,13 +1256,13 @@ var _SsoClient = class _SsoClient {
|
|
|
1063
1256
|
if (!tokenData) {
|
|
1064
1257
|
throw new SsoError("not_authenticated", "\u672A\u767B\u5F55");
|
|
1065
1258
|
}
|
|
1066
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1259
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1067
1260
|
tokenData = await this.refreshToken();
|
|
1068
1261
|
}
|
|
1069
1262
|
const userinfoEndpoint = await this._getUserinfoEndpoint();
|
|
1070
1263
|
let res;
|
|
1071
1264
|
try {
|
|
1072
|
-
res = await
|
|
1265
|
+
res = await fetchWithTimeout(userinfoEndpoint, {
|
|
1073
1266
|
headers: {
|
|
1074
1267
|
Authorization: `Bearer ${tokenData.access_token}`
|
|
1075
1268
|
}
|
|
@@ -1098,7 +1291,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1098
1291
|
async getAccessToken() {
|
|
1099
1292
|
let tokenData = getTokenData(this.config.clientId);
|
|
1100
1293
|
if (!tokenData) return null;
|
|
1101
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1294
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1102
1295
|
tokenData = await this.refreshToken();
|
|
1103
1296
|
}
|
|
1104
1297
|
return tokenData.access_token;
|
|
@@ -1107,6 +1300,9 @@ var _SsoClient = class _SsoClient {
|
|
|
1107
1300
|
* 检查是否已认证(不发起网络请求)
|
|
1108
1301
|
*
|
|
1109
1302
|
* 仅检查本地是否存在未过期的 access_token。
|
|
1303
|
+
* 故意不做 EXPIRY_SKEW_MS 提前量:这是同步的 UI 提示判定(是否显示"已登录"),
|
|
1304
|
+
* 提前 10s 翻转 false 会让调用方在 token 仍有效时误显示未登录;真正发请求的
|
|
1305
|
+
* getAccessToken/getUserInfo 已带提前量。
|
|
1110
1306
|
*/
|
|
1111
1307
|
isAuthenticated() {
|
|
1112
1308
|
const tokenData = getTokenData(this.config.clientId);
|
|
@@ -1126,6 +1322,10 @@ var _SsoClient = class _SsoClient {
|
|
|
1126
1322
|
const refreshToken = tokenData?.refresh_token;
|
|
1127
1323
|
const idTokenHint = tokenData?.id_token;
|
|
1128
1324
|
clearAllSsoData(this.config.clientId);
|
|
1325
|
+
this.debugLog("logout: \u672C\u5730\u6570\u636E\u5DF2\u6E05\u9664", {
|
|
1326
|
+
clientId: this.config.clientId,
|
|
1327
|
+
redirectToSso
|
|
1328
|
+
});
|
|
1129
1329
|
if (refreshToken && this.config.clientId) {
|
|
1130
1330
|
try {
|
|
1131
1331
|
const discovery = this.config.serverBaseUrl ? null : await this._getDiscovery();
|
|
@@ -1138,11 +1338,15 @@ var _SsoClient = class _SsoClient {
|
|
|
1138
1338
|
if (this.config.clientSecret) {
|
|
1139
1339
|
revokeBody.set("client_secret", this.config.clientSecret);
|
|
1140
1340
|
}
|
|
1141
|
-
await
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1341
|
+
await fetchWithTimeout(
|
|
1342
|
+
revokeUrl,
|
|
1343
|
+
{
|
|
1344
|
+
method: "POST",
|
|
1345
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1346
|
+
body: revokeBody.toString()
|
|
1347
|
+
},
|
|
1348
|
+
REVOKE_TIMEOUT_MS
|
|
1349
|
+
);
|
|
1146
1350
|
} catch {
|
|
1147
1351
|
}
|
|
1148
1352
|
}
|