@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.js
CHANGED
|
@@ -154,18 +154,25 @@ function createMemoryStorageAdapter() {
|
|
|
154
154
|
},
|
|
155
155
|
remove(key) {
|
|
156
156
|
store.delete(key);
|
|
157
|
+
},
|
|
158
|
+
keys() {
|
|
159
|
+
return [...store.keys()];
|
|
157
160
|
}
|
|
158
161
|
};
|
|
159
162
|
}
|
|
160
163
|
var memoryStorageAdapter = createMemoryStorageAdapter();
|
|
161
164
|
var localStorageAdapter = {
|
|
162
165
|
get(key) {
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
try {
|
|
167
|
+
if (typeof localStorage === "undefined") return null;
|
|
168
|
+
return localStorage.getItem(STORAGE_PREFIX + key);
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
165
172
|
},
|
|
166
173
|
set(key, value) {
|
|
167
|
-
if (typeof localStorage === "undefined") return;
|
|
168
174
|
try {
|
|
175
|
+
if (typeof localStorage === "undefined") return;
|
|
169
176
|
localStorage.setItem(STORAGE_PREFIX + key, value);
|
|
170
177
|
} catch (err) {
|
|
171
178
|
console.warn(
|
|
@@ -174,34 +181,68 @@ var localStorageAdapter = {
|
|
|
174
181
|
}
|
|
175
182
|
},
|
|
176
183
|
remove(key) {
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
try {
|
|
185
|
+
if (typeof localStorage === "undefined") return;
|
|
186
|
+
localStorage.removeItem(STORAGE_PREFIX + key);
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
keys() {
|
|
191
|
+
try {
|
|
192
|
+
if (typeof localStorage === "undefined") return [];
|
|
193
|
+
const result = [];
|
|
194
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
195
|
+
const key = localStorage.key(i);
|
|
196
|
+
if (key?.startsWith(STORAGE_PREFIX)) result.push(key.slice(STORAGE_PREFIX.length));
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
} catch {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
179
202
|
}
|
|
180
203
|
};
|
|
181
204
|
function createSessionStorageAdapter() {
|
|
182
205
|
const fallback = /* @__PURE__ */ new Map();
|
|
183
206
|
return {
|
|
184
207
|
get(key) {
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
try {
|
|
209
|
+
if (typeof sessionStorage !== "undefined") {
|
|
210
|
+
return sessionStorage.getItem(STORAGE_PREFIX + key) ?? fallback.get(key) ?? null;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
187
213
|
}
|
|
188
214
|
return fallback.get(key) ?? null;
|
|
189
215
|
},
|
|
190
216
|
set(key, value) {
|
|
191
|
-
if (typeof sessionStorage === "undefined") {
|
|
192
|
-
fallback.set(key, value);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
217
|
try {
|
|
196
|
-
|
|
218
|
+
if (typeof sessionStorage !== "undefined") {
|
|
219
|
+
sessionStorage.setItem(STORAGE_PREFIX + key, value);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
197
222
|
} catch {
|
|
198
|
-
fallback.set(key, value);
|
|
199
223
|
}
|
|
224
|
+
fallback.set(key, value);
|
|
200
225
|
},
|
|
201
226
|
remove(key) {
|
|
202
227
|
fallback.delete(key);
|
|
203
|
-
|
|
204
|
-
|
|
228
|
+
try {
|
|
229
|
+
if (typeof sessionStorage === "undefined") return;
|
|
230
|
+
sessionStorage.removeItem(STORAGE_PREFIX + key);
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
keys() {
|
|
235
|
+
const result = new Set(fallback.keys());
|
|
236
|
+
try {
|
|
237
|
+
if (typeof sessionStorage !== "undefined") {
|
|
238
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
239
|
+
const key = sessionStorage.key(i);
|
|
240
|
+
if (key?.startsWith(STORAGE_PREFIX)) result.add(key.slice(STORAGE_PREFIX.length));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
return [...result];
|
|
205
246
|
}
|
|
206
247
|
};
|
|
207
248
|
}
|
|
@@ -303,23 +344,44 @@ function clearAllSsoData(clientId) {
|
|
|
303
344
|
removeReturnUrl();
|
|
304
345
|
removeLogoutState();
|
|
305
346
|
removeSilentProbe();
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
typeof sessionStorage !== "undefined"
|
|
309
|
-
|
|
310
|
-
|
|
347
|
+
const stores = [];
|
|
348
|
+
try {
|
|
349
|
+
if (typeof sessionStorage !== "undefined") stores.push(sessionStorage);
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
if (typeof localStorage !== "undefined") stores.push(localStorage);
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
const abstractKeys = /* @__PURE__ */ new Set();
|
|
311
357
|
for (const store of stores) {
|
|
312
|
-
if (!store) continue;
|
|
313
358
|
const keys = [];
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
359
|
+
try {
|
|
360
|
+
for (let i = 0; i < store.length; i++) {
|
|
361
|
+
const key = store.key(i);
|
|
362
|
+
if (key?.startsWith(STORAGE_PREFIX)) keys.push(key);
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
continue;
|
|
317
366
|
}
|
|
318
367
|
for (const key of keys) {
|
|
319
|
-
|
|
320
|
-
|
|
368
|
+
abstractKeys.add(key.slice(STORAGE_PREFIX.length));
|
|
369
|
+
try {
|
|
370
|
+
store.removeItem(key);
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
321
373
|
}
|
|
322
374
|
}
|
|
375
|
+
for (const store of [_transient, _storage]) {
|
|
376
|
+
try {
|
|
377
|
+
for (const key of store.keys?.() ?? []) abstractKeys.add(key);
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
for (const key of abstractKeys) {
|
|
382
|
+
_transient.remove(key);
|
|
383
|
+
_storage.remove(key);
|
|
384
|
+
}
|
|
323
385
|
}
|
|
324
386
|
function clearVerifiersForClients(clientIds) {
|
|
325
387
|
for (const clientId of clientIds) {
|
|
@@ -328,12 +390,17 @@ function clearVerifiersForClients(clientIds) {
|
|
|
328
390
|
}
|
|
329
391
|
|
|
330
392
|
// src/core/security.ts
|
|
393
|
+
var UNSAFE_URL_CHAR_PATTERN = /[\\\u0000-\u001F\u007F]/;
|
|
331
394
|
function isTrustedReturnUrl(url, currentOrigin) {
|
|
332
395
|
if (!url) return false;
|
|
333
|
-
if (
|
|
334
|
-
if (url.startsWith("
|
|
396
|
+
if (UNSAFE_URL_CHAR_PATTERN.test(url)) return false;
|
|
397
|
+
if (url.startsWith("//")) return false;
|
|
398
|
+
if (url.startsWith("/")) return true;
|
|
335
399
|
try {
|
|
336
|
-
|
|
400
|
+
const parsed = new URL(url);
|
|
401
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
402
|
+
if (parsed.username || parsed.password) return false;
|
|
403
|
+
return parsed.origin === currentOrigin;
|
|
337
404
|
} catch {
|
|
338
405
|
return false;
|
|
339
406
|
}
|
|
@@ -545,7 +612,10 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
545
612
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
546
613
|
throw new SsoError("id_token_expired", "ID Token \u5DF2\u8FC7\u671F");
|
|
547
614
|
}
|
|
548
|
-
if (typeof payload.iat
|
|
615
|
+
if (typeof payload.iat !== "number") {
|
|
616
|
+
throw new SsoError("id_token_invalid", "ID Token \u7F3A\u5C11 iat \u58F0\u660E");
|
|
617
|
+
}
|
|
618
|
+
if (payload.iat * 1e3 > Date.now() + 6e4) {
|
|
549
619
|
throw new SsoError("id_token_invalid", "ID Token iat \u5728\u672A\u6765\uFF0C\u7591\u4F3C\u4F2A\u9020\u6216\u65F6\u949F\u5F02\u5E38");
|
|
550
620
|
}
|
|
551
621
|
if (typeof payload.sub !== "string" || !payload.sub) {
|
|
@@ -562,11 +632,47 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
562
632
|
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
563
633
|
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
564
634
|
}
|
|
635
|
+
} else if (typeof payload.nonce === "string" && payload.nonce) {
|
|
636
|
+
throw new SsoError(
|
|
637
|
+
"id_token_nonce_mismatch",
|
|
638
|
+
"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"
|
|
639
|
+
);
|
|
565
640
|
}
|
|
566
641
|
return { sub: payload.sub };
|
|
567
642
|
}
|
|
568
643
|
|
|
644
|
+
// src/core/http.ts
|
|
645
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
646
|
+
async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
647
|
+
const controller = new AbortController();
|
|
648
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
649
|
+
try {
|
|
650
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
651
|
+
} finally {
|
|
652
|
+
clearTimeout(timer);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
569
656
|
// src/core/SsoClient.ts
|
|
657
|
+
var REVOKE_TIMEOUT_MS = 3e3;
|
|
658
|
+
var EXPIRY_SKEW_MS = 1e4;
|
|
659
|
+
function assertValidTokenResponse(data, requireRefreshToken) {
|
|
660
|
+
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
661
|
+
throw new SsoError("token_request_failed", "Token \u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
662
|
+
}
|
|
663
|
+
if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
|
|
664
|
+
throw new SsoError(
|
|
665
|
+
"token_request_failed",
|
|
666
|
+
`Token \u54CD\u5E94 expires_in \u975E\u6CD5: ${String(data.expires_in)}`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (requireRefreshToken && (typeof data.refresh_token !== "string" || !data.refresh_token)) {
|
|
670
|
+
throw new SsoError(
|
|
671
|
+
"token_request_failed",
|
|
672
|
+
"Token \u54CD\u5E94\u7F3A\u5C11 refresh_token\uFF08authorization_code \u4EA4\u6362\u5FC5\u987B\u8FD4\u56DE\uFF09"
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
570
676
|
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
|
|
571
677
|
"login_required",
|
|
572
678
|
"consent_required",
|
|
@@ -577,6 +683,8 @@ var _SsoClient = class _SsoClient {
|
|
|
577
683
|
this._discovery = null;
|
|
578
684
|
this._discoveryFetchedAt = 0;
|
|
579
685
|
this._refreshLock = null;
|
|
686
|
+
/** Discovery 端点 origin 校验失败只告警一次(拒绝的文档不缓存,避免每次调用刷屏) */
|
|
687
|
+
this._discoveryOriginWarned = false;
|
|
580
688
|
if (!config.clientId) throw new SsoError("invalid_config", "clientId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
581
689
|
if (!config.redirectUri) throw new SsoError("invalid_config", "redirectUri \u4E0D\u80FD\u4E3A\u7A7A");
|
|
582
690
|
if (!config.ssoBaseUrl) throw new SsoError("invalid_config", "ssoBaseUrl \u4E0D\u80FD\u4E3A\u7A7A");
|
|
@@ -585,8 +693,67 @@ var _SsoClient = class _SsoClient {
|
|
|
585
693
|
this._serverBase = (config.serverBaseUrl ?? base).replace(/\/+$/, "");
|
|
586
694
|
}
|
|
587
695
|
// ============================================
|
|
696
|
+
// 调试日志
|
|
697
|
+
// ============================================
|
|
698
|
+
/**
|
|
699
|
+
* 调试日志(config.debug=true 时输出,前缀 [SSO SDK]):
|
|
700
|
+
* 覆盖登录发起 / 回调结果 / 刷新失败 / 登出等关键节点,便于接入方定位问题。
|
|
701
|
+
* 不会输出 token/授权码等敏感值。
|
|
702
|
+
*/
|
|
703
|
+
debugLog(...args) {
|
|
704
|
+
if (this.config.debug) {
|
|
705
|
+
console.warn("[SSO SDK]", ...args);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// ============================================
|
|
588
709
|
// 内部方法
|
|
589
710
|
// ============================================
|
|
711
|
+
/**
|
|
712
|
+
* 校验 Discovery 文档端点 origin(防文档被篡改后 token/code 被导向攻击者服务器):
|
|
713
|
+
* - authorization_endpoint 必须与 ssoBaseUrl 同源(浏览器跳转目标);
|
|
714
|
+
* - token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl 同源,
|
|
715
|
+
* 或(配置了 serverBaseUrl 时)与内网地址同源。
|
|
716
|
+
* 任一不匹配:告警并返回 null(上层回退到默认端点,fail-safe)。
|
|
717
|
+
* 可通过 config.allowCrossOriginDiscoveryEndpoints 关闭(见配置项警告)。
|
|
718
|
+
*/
|
|
719
|
+
_sanitizeDiscovery(d) {
|
|
720
|
+
if (this.config.allowCrossOriginDiscoveryEndpoints) return d;
|
|
721
|
+
const allowedOrigins = /* @__PURE__ */ new Set();
|
|
722
|
+
try {
|
|
723
|
+
allowedOrigins.add(new URL(this.config.ssoBaseUrl).origin);
|
|
724
|
+
allowedOrigins.add(new URL(this._serverBase).origin);
|
|
725
|
+
} catch {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
const sameOrigin = (endpoint) => {
|
|
729
|
+
try {
|
|
730
|
+
return allowedOrigins.has(new URL(endpoint).origin);
|
|
731
|
+
} catch {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
const publicOrigin = new URL(this.config.ssoBaseUrl).origin;
|
|
736
|
+
let authorizationOk = false;
|
|
737
|
+
try {
|
|
738
|
+
authorizationOk = new URL(d.authorization_endpoint).origin === publicOrigin;
|
|
739
|
+
} catch {
|
|
740
|
+
authorizationOk = false;
|
|
741
|
+
}
|
|
742
|
+
const bad = [];
|
|
743
|
+
if (!authorizationOk) bad.push("authorization_endpoint");
|
|
744
|
+
if (!sameOrigin(d.token_endpoint)) bad.push("token_endpoint");
|
|
745
|
+
if (!sameOrigin(d.userinfo_endpoint)) bad.push("userinfo_endpoint");
|
|
746
|
+
if (bad.length > 0) {
|
|
747
|
+
if (!this._discoveryOriginWarned) {
|
|
748
|
+
this._discoveryOriginWarned = true;
|
|
749
|
+
console.warn(
|
|
750
|
+
`[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`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
return d;
|
|
756
|
+
}
|
|
590
757
|
/**
|
|
591
758
|
* 获取 OIDC Discovery 文档(带缓存 + 超时)
|
|
592
759
|
*
|
|
@@ -613,7 +780,10 @@ var _SsoClient = class _SsoClient {
|
|
|
613
780
|
);
|
|
614
781
|
return null;
|
|
615
782
|
}
|
|
616
|
-
|
|
783
|
+
const doc = await res.json();
|
|
784
|
+
const sanitized = this._sanitizeDiscovery(doc);
|
|
785
|
+
if (!sanitized) return null;
|
|
786
|
+
this._discovery = sanitized;
|
|
617
787
|
this._discoveryFetchedAt = now;
|
|
618
788
|
return this._discovery;
|
|
619
789
|
} catch (err) {
|
|
@@ -964,7 +1134,7 @@ var _SsoClient = class _SsoClient {
|
|
|
964
1134
|
}
|
|
965
1135
|
let res;
|
|
966
1136
|
try {
|
|
967
|
-
res = await
|
|
1137
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
968
1138
|
method: "POST",
|
|
969
1139
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
970
1140
|
body: body.toString()
|
|
@@ -973,6 +1143,10 @@ var _SsoClient = class _SsoClient {
|
|
|
973
1143
|
throw new SsoError("network_error", "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", err);
|
|
974
1144
|
}
|
|
975
1145
|
if (!res.ok) {
|
|
1146
|
+
removeOAuthState(this.config.clientId);
|
|
1147
|
+
removeOAuthNonce(this.config.clientId);
|
|
1148
|
+
removePkceVerifier(this.config.clientId);
|
|
1149
|
+
removeSilentProbe(this.config.clientId);
|
|
976
1150
|
let errData = {};
|
|
977
1151
|
try {
|
|
978
1152
|
errData = await res.json();
|
|
@@ -985,6 +1159,19 @@ var _SsoClient = class _SsoClient {
|
|
|
985
1159
|
);
|
|
986
1160
|
}
|
|
987
1161
|
const data = await res.json();
|
|
1162
|
+
assertValidTokenResponse(data, true);
|
|
1163
|
+
const requestedScopes = (this.config.scopes || "openid profile").split(" ").filter(Boolean);
|
|
1164
|
+
if (requestedScopes.includes("openid") && !data.id_token) {
|
|
1165
|
+
removeOAuthState(this.config.clientId);
|
|
1166
|
+
removeOAuthNonce(this.config.clientId);
|
|
1167
|
+
removePkceVerifier(this.config.clientId);
|
|
1168
|
+
removeSilentProbe(this.config.clientId);
|
|
1169
|
+
removeTokenData(this.config.clientId);
|
|
1170
|
+
throw new SsoError(
|
|
1171
|
+
"id_token_invalid",
|
|
1172
|
+
"Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09"
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
988
1175
|
if (data.id_token) {
|
|
989
1176
|
try {
|
|
990
1177
|
await validateIdToken(
|
|
@@ -1014,6 +1201,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1014
1201
|
expires_at: now + data.expires_in * 1e3
|
|
1015
1202
|
};
|
|
1016
1203
|
saveTokenData(tokenData, this.config.clientId);
|
|
1204
|
+
this.debugLog("handleCallback: token \u4EA4\u6362\u6210\u529F", { clientId: this.config.clientId });
|
|
1017
1205
|
return tokenData;
|
|
1018
1206
|
}
|
|
1019
1207
|
/**
|
|
@@ -1049,7 +1237,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1049
1237
|
let lastErr;
|
|
1050
1238
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1051
1239
|
try {
|
|
1052
|
-
res = await
|
|
1240
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
1053
1241
|
method: "POST",
|
|
1054
1242
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1055
1243
|
body: body.toString()
|
|
@@ -1071,15 +1259,20 @@ var _SsoClient = class _SsoClient {
|
|
|
1071
1259
|
} catch {
|
|
1072
1260
|
}
|
|
1073
1261
|
const errorCode = errData.error || "";
|
|
1074
|
-
if (errorCode === "invalid_grant"
|
|
1262
|
+
if (errorCode === "invalid_grant") {
|
|
1075
1263
|
removeTokenData(this.config.clientId);
|
|
1076
1264
|
}
|
|
1265
|
+
this.debugLog("refreshToken: \u5237\u65B0\u5931\u8D25", { status: res.status, error: errorCode });
|
|
1266
|
+
if (!errorCode && res.status === 401) {
|
|
1267
|
+
throw new SsoError("sso_server_error", "\u5237\u65B0 Token \u5931\u8D25\uFF08\u7F51\u5173\u5F02\u5E38\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
1268
|
+
}
|
|
1077
1269
|
throw new SsoError(
|
|
1078
1270
|
mapOAuthErrorToSsoCode(errorCode, "refresh"),
|
|
1079
1271
|
errData.error_description || `\u5237\u65B0 Token \u5931\u8D25: HTTP ${res.status}`
|
|
1080
1272
|
);
|
|
1081
1273
|
}
|
|
1082
1274
|
const data = await res.json();
|
|
1275
|
+
assertValidTokenResponse(data, false);
|
|
1083
1276
|
if (data.id_token) {
|
|
1084
1277
|
try {
|
|
1085
1278
|
await validateIdToken(
|
|
@@ -1117,13 +1310,13 @@ var _SsoClient = class _SsoClient {
|
|
|
1117
1310
|
if (!tokenData) {
|
|
1118
1311
|
throw new SsoError("not_authenticated", "\u672A\u767B\u5F55");
|
|
1119
1312
|
}
|
|
1120
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1313
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1121
1314
|
tokenData = await this.refreshToken();
|
|
1122
1315
|
}
|
|
1123
1316
|
const userinfoEndpoint = await this._getUserinfoEndpoint();
|
|
1124
1317
|
let res;
|
|
1125
1318
|
try {
|
|
1126
|
-
res = await
|
|
1319
|
+
res = await fetchWithTimeout(userinfoEndpoint, {
|
|
1127
1320
|
headers: {
|
|
1128
1321
|
Authorization: `Bearer ${tokenData.access_token}`
|
|
1129
1322
|
}
|
|
@@ -1152,7 +1345,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1152
1345
|
async getAccessToken() {
|
|
1153
1346
|
let tokenData = getTokenData(this.config.clientId);
|
|
1154
1347
|
if (!tokenData) return null;
|
|
1155
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1348
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1156
1349
|
tokenData = await this.refreshToken();
|
|
1157
1350
|
}
|
|
1158
1351
|
return tokenData.access_token;
|
|
@@ -1161,6 +1354,9 @@ var _SsoClient = class _SsoClient {
|
|
|
1161
1354
|
* 检查是否已认证(不发起网络请求)
|
|
1162
1355
|
*
|
|
1163
1356
|
* 仅检查本地是否存在未过期的 access_token。
|
|
1357
|
+
* 故意不做 EXPIRY_SKEW_MS 提前量:这是同步的 UI 提示判定(是否显示"已登录"),
|
|
1358
|
+
* 提前 10s 翻转 false 会让调用方在 token 仍有效时误显示未登录;真正发请求的
|
|
1359
|
+
* getAccessToken/getUserInfo 已带提前量。
|
|
1164
1360
|
*/
|
|
1165
1361
|
isAuthenticated() {
|
|
1166
1362
|
const tokenData = getTokenData(this.config.clientId);
|
|
@@ -1180,6 +1376,10 @@ var _SsoClient = class _SsoClient {
|
|
|
1180
1376
|
const refreshToken = tokenData?.refresh_token;
|
|
1181
1377
|
const idTokenHint = tokenData?.id_token;
|
|
1182
1378
|
clearAllSsoData(this.config.clientId);
|
|
1379
|
+
this.debugLog("logout: \u672C\u5730\u6570\u636E\u5DF2\u6E05\u9664", {
|
|
1380
|
+
clientId: this.config.clientId,
|
|
1381
|
+
redirectToSso
|
|
1382
|
+
});
|
|
1183
1383
|
if (refreshToken && this.config.clientId) {
|
|
1184
1384
|
try {
|
|
1185
1385
|
const discovery = this.config.serverBaseUrl ? null : await this._getDiscovery();
|
|
@@ -1192,11 +1392,15 @@ var _SsoClient = class _SsoClient {
|
|
|
1192
1392
|
if (this.config.clientSecret) {
|
|
1193
1393
|
revokeBody.set("client_secret", this.config.clientSecret);
|
|
1194
1394
|
}
|
|
1195
|
-
await
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1395
|
+
await fetchWithTimeout(
|
|
1396
|
+
revokeUrl,
|
|
1397
|
+
{
|
|
1398
|
+
method: "POST",
|
|
1399
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1400
|
+
body: revokeBody.toString()
|
|
1401
|
+
},
|
|
1402
|
+
REVOKE_TIMEOUT_MS
|
|
1403
|
+
);
|
|
1200
1404
|
} catch {
|
|
1201
1405
|
}
|
|
1202
1406
|
}
|