@nihplod/sso-sdk 1.5.0 → 1.7.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 +21 -2
- package/dist/index.d.mts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +148 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +148 -84
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +87 -2
- package/dist/next/index.d.ts +87 -2
- package/dist/next/index.js +330 -64
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +329 -64
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +32 -2
- package/dist/react/index.d.ts +32 -2
- package/dist/react/index.js +196 -105
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +196 -105
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -89,24 +89,6 @@ var SILENT_PROBE_KEY = "silent_probe";
|
|
|
89
89
|
function buildKey(base, clientId) {
|
|
90
90
|
return clientId ? `${base}:${clientId}` : base;
|
|
91
91
|
}
|
|
92
|
-
function createMemoryStorageAdapter() {
|
|
93
|
-
const store = /* @__PURE__ */ new Map();
|
|
94
|
-
return {
|
|
95
|
-
get(key) {
|
|
96
|
-
return store.get(key) ?? null;
|
|
97
|
-
},
|
|
98
|
-
set(key, value) {
|
|
99
|
-
store.set(key, value);
|
|
100
|
-
},
|
|
101
|
-
remove(key) {
|
|
102
|
-
store.delete(key);
|
|
103
|
-
},
|
|
104
|
-
keys() {
|
|
105
|
-
return [...store.keys()];
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
var memoryStorageAdapter = createMemoryStorageAdapter();
|
|
110
92
|
var localStorageAdapter = {
|
|
111
93
|
get(key) {
|
|
112
94
|
try {
|
|
@@ -428,7 +410,19 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
428
410
|
return cachedJwks.jwks;
|
|
429
411
|
}
|
|
430
412
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
431
|
-
|
|
413
|
+
let jwksUri = `${baseUrl}/api/oauth/jwks`;
|
|
414
|
+
if (discovery?.jwks_uri) {
|
|
415
|
+
try {
|
|
416
|
+
if (new URL(discovery.jwks_uri).origin === new URL(baseUrl).origin) {
|
|
417
|
+
jwksUri = discovery.jwks_uri;
|
|
418
|
+
} else {
|
|
419
|
+
console.warn(
|
|
420
|
+
"[SSO SDK] Discovery \u7684 jwks_uri \u4E0E ssoBaseUrl \u4E0D\u540C\u6E90\uFF0C\u5DF2\u5FFD\u7565\u5E76\u4F7F\u7528\u9ED8\u8BA4 JWKS \u7AEF\u70B9"
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
432
426
|
try {
|
|
433
427
|
const res = await fetch(jwksUri, {
|
|
434
428
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
@@ -558,7 +552,10 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
558
552
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
559
553
|
throw new SsoError("id_token_expired", "ID Token \u5DF2\u8FC7\u671F");
|
|
560
554
|
}
|
|
561
|
-
if (typeof payload.iat
|
|
555
|
+
if (typeof payload.iat !== "number") {
|
|
556
|
+
throw new SsoError("id_token_invalid", "ID Token \u7F3A\u5C11 iat \u58F0\u660E");
|
|
557
|
+
}
|
|
558
|
+
if (payload.iat * 1e3 > Date.now() + 6e4) {
|
|
562
559
|
throw new SsoError("id_token_invalid", "ID Token iat \u5728\u672A\u6765\uFF0C\u7591\u4F3C\u4F2A\u9020\u6216\u65F6\u949F\u5F02\u5E38");
|
|
563
560
|
}
|
|
564
561
|
if (typeof payload.sub !== "string" || !payload.sub) {
|
|
@@ -575,13 +572,17 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
575
572
|
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
576
573
|
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
577
574
|
}
|
|
575
|
+
} else if (typeof payload.nonce === "string" && payload.nonce) {
|
|
576
|
+
throw new SsoError(
|
|
577
|
+
"id_token_nonce_mismatch",
|
|
578
|
+
"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"
|
|
579
|
+
);
|
|
578
580
|
}
|
|
579
581
|
return { sub: payload.sub };
|
|
580
582
|
}
|
|
581
583
|
|
|
582
|
-
// src/core/
|
|
584
|
+
// src/core/http.ts
|
|
583
585
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
584
|
-
var REVOKE_TIMEOUT_MS = 3e3;
|
|
585
586
|
async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
586
587
|
const controller = new AbortController();
|
|
587
588
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -591,6 +592,10 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS
|
|
|
591
592
|
clearTimeout(timer);
|
|
592
593
|
}
|
|
593
594
|
}
|
|
595
|
+
|
|
596
|
+
// src/core/SsoClient.ts
|
|
597
|
+
var REVOKE_TIMEOUT_MS = 3e3;
|
|
598
|
+
var EXPIRY_SKEW_MS = 1e4;
|
|
594
599
|
function assertValidTokenResponse(data, requireRefreshToken) {
|
|
595
600
|
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
596
601
|
throw new SsoError("token_request_failed", "Token \u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
@@ -608,16 +613,14 @@ function assertValidTokenResponse(data, requireRefreshToken) {
|
|
|
608
613
|
);
|
|
609
614
|
}
|
|
610
615
|
}
|
|
611
|
-
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
|
|
612
|
-
"login_required",
|
|
613
|
-
"consent_required",
|
|
614
|
-
"interaction_required"
|
|
615
|
-
]);
|
|
616
|
+
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set(["login_required", "consent_required", "interaction_required"]);
|
|
616
617
|
var _SsoClient = class _SsoClient {
|
|
617
618
|
constructor(config) {
|
|
618
619
|
this._discovery = null;
|
|
619
620
|
this._discoveryFetchedAt = 0;
|
|
620
621
|
this._refreshLock = null;
|
|
622
|
+
/** Discovery 端点 origin 校验失败只告警一次(拒绝的文档不缓存,避免每次调用刷屏) */
|
|
623
|
+
this._discoveryOriginWarned = false;
|
|
621
624
|
if (!config.clientId) throw new SsoError("invalid_config", "clientId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
622
625
|
if (!config.redirectUri) throw new SsoError("invalid_config", "redirectUri \u4E0D\u80FD\u4E3A\u7A7A");
|
|
623
626
|
if (!config.ssoBaseUrl) throw new SsoError("invalid_config", "ssoBaseUrl \u4E0D\u80FD\u4E3A\u7A7A");
|
|
@@ -641,6 +644,68 @@ var _SsoClient = class _SsoClient {
|
|
|
641
644
|
// ============================================
|
|
642
645
|
// 内部方法
|
|
643
646
|
// ============================================
|
|
647
|
+
/**
|
|
648
|
+
* 校验 Discovery 文档端点 origin(防文档被篡改后 token/code 被导向攻击者服务器):
|
|
649
|
+
* - authorization_endpoint 必须与 ssoBaseUrl 同源(浏览器跳转目标);
|
|
650
|
+
* - token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl 同源,
|
|
651
|
+
* 或(配置了 serverBaseUrl 时)与内网地址同源。
|
|
652
|
+
* 任一不匹配:告警并返回 null(上层回退到默认端点,fail-safe)。
|
|
653
|
+
* 可通过 config.allowCrossOriginDiscoveryEndpoints 关闭(见配置项警告)。
|
|
654
|
+
*/
|
|
655
|
+
_sanitizeDiscovery(d) {
|
|
656
|
+
if (this.config.allowCrossOriginDiscoveryEndpoints) return d;
|
|
657
|
+
const allowedOrigins = /* @__PURE__ */ new Set();
|
|
658
|
+
try {
|
|
659
|
+
allowedOrigins.add(new URL(this.config.ssoBaseUrl).origin);
|
|
660
|
+
allowedOrigins.add(new URL(this._serverBase).origin);
|
|
661
|
+
} catch {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
const sameOrigin = (endpoint) => {
|
|
665
|
+
try {
|
|
666
|
+
return allowedOrigins.has(new URL(endpoint).origin);
|
|
667
|
+
} catch {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
const publicOrigin = new URL(this.config.ssoBaseUrl).origin;
|
|
672
|
+
let authorizationOk = false;
|
|
673
|
+
try {
|
|
674
|
+
authorizationOk = new URL(d.authorization_endpoint).origin === publicOrigin;
|
|
675
|
+
} catch {
|
|
676
|
+
authorizationOk = false;
|
|
677
|
+
}
|
|
678
|
+
const bad = [];
|
|
679
|
+
if (!authorizationOk) bad.push("authorization_endpoint");
|
|
680
|
+
if (!sameOrigin(d.token_endpoint)) bad.push("token_endpoint");
|
|
681
|
+
if (!sameOrigin(d.userinfo_endpoint)) bad.push("userinfo_endpoint");
|
|
682
|
+
if (d.jwks_uri && !sameOrigin(d.jwks_uri)) bad.push("jwks_uri");
|
|
683
|
+
if (d.revocation_endpoint && !sameOrigin(d.revocation_endpoint)) {
|
|
684
|
+
bad.push("revocation_endpoint");
|
|
685
|
+
}
|
|
686
|
+
if (d.introspection_endpoint && !sameOrigin(d.introspection_endpoint)) {
|
|
687
|
+
bad.push("introspection_endpoint");
|
|
688
|
+
}
|
|
689
|
+
if (d.end_session_endpoint) {
|
|
690
|
+
try {
|
|
691
|
+
if (new URL(d.end_session_endpoint).origin !== publicOrigin) {
|
|
692
|
+
bad.push("end_session_endpoint");
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
bad.push("end_session_endpoint");
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (bad.length > 0) {
|
|
699
|
+
if (!this._discoveryOriginWarned) {
|
|
700
|
+
this._discoveryOriginWarned = true;
|
|
701
|
+
console.warn(
|
|
702
|
+
`[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`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
return d;
|
|
708
|
+
}
|
|
644
709
|
/**
|
|
645
710
|
* 获取 OIDC Discovery 文档(带缓存 + 超时)
|
|
646
711
|
*
|
|
@@ -655,19 +720,17 @@ var _SsoClient = class _SsoClient {
|
|
|
655
720
|
const url = `${this._serverBase}/api/oauth/.well-known/openid-configuration`;
|
|
656
721
|
try {
|
|
657
722
|
const controller = new AbortController();
|
|
658
|
-
const timeoutId = setTimeout(
|
|
659
|
-
() => controller.abort(),
|
|
660
|
-
_SsoClient.DISCOVERY_TIMEOUT_MS
|
|
661
|
-
);
|
|
723
|
+
const timeoutId = setTimeout(() => controller.abort(), _SsoClient.DISCOVERY_TIMEOUT_MS);
|
|
662
724
|
const res = await fetch(url, { signal: controller.signal });
|
|
663
725
|
clearTimeout(timeoutId);
|
|
664
726
|
if (!res.ok) {
|
|
665
|
-
console.warn(
|
|
666
|
-
`[SSO SDK] OIDC Discovery \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}, \u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9`
|
|
667
|
-
);
|
|
727
|
+
console.warn(`[SSO SDK] OIDC Discovery \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}, \u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9`);
|
|
668
728
|
return null;
|
|
669
729
|
}
|
|
670
|
-
|
|
730
|
+
const doc = await res.json();
|
|
731
|
+
const sanitized = this._sanitizeDiscovery(doc);
|
|
732
|
+
if (!sanitized) return null;
|
|
733
|
+
this._discovery = sanitized;
|
|
671
734
|
this._discoveryFetchedAt = now;
|
|
672
735
|
return this._discovery;
|
|
673
736
|
} catch (err) {
|
|
@@ -876,10 +939,7 @@ var _SsoClient = class _SsoClient {
|
|
|
876
939
|
removeOAuthState(this.config.clientId);
|
|
877
940
|
removeOAuthNonce(this.config.clientId);
|
|
878
941
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
879
|
-
throw new SsoError(
|
|
880
|
-
"popup_blocked",
|
|
881
|
-
"\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5"
|
|
882
|
-
);
|
|
942
|
+
throw new SsoError("popup_blocked", "\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5");
|
|
883
943
|
}
|
|
884
944
|
const redirectUriOrigin = new URL(this.config.redirectUri).origin;
|
|
885
945
|
try {
|
|
@@ -908,9 +968,7 @@ var _SsoClient = class _SsoClient {
|
|
|
908
968
|
}
|
|
909
969
|
this.handleCallback(event.data.callbackUrl).then((tokenData) => {
|
|
910
970
|
if (tokenData === null) {
|
|
911
|
-
reject(
|
|
912
|
-
new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09")
|
|
913
|
-
);
|
|
971
|
+
reject(new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09"));
|
|
914
972
|
return;
|
|
915
973
|
}
|
|
916
974
|
resolve(tokenData);
|
|
@@ -921,12 +979,7 @@ var _SsoClient = class _SsoClient {
|
|
|
921
979
|
if (!completed) {
|
|
922
980
|
completed = true;
|
|
923
981
|
cleanup();
|
|
924
|
-
reject(
|
|
925
|
-
new SsoError(
|
|
926
|
-
"popup_closed",
|
|
927
|
-
"\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED"
|
|
928
|
-
)
|
|
929
|
-
);
|
|
982
|
+
reject(new SsoError("popup_closed", "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED"));
|
|
930
983
|
}
|
|
931
984
|
}
|
|
932
985
|
}, 500);
|
|
@@ -993,18 +1046,24 @@ var _SsoClient = class _SsoClient {
|
|
|
993
1046
|
removeOAuthNonce(this.config.clientId);
|
|
994
1047
|
removePkceVerifier(this.config.clientId);
|
|
995
1048
|
removeSilentProbe(this.config.clientId);
|
|
996
|
-
throw new SsoError(
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1049
|
+
throw new SsoError("state_mismatch", "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB");
|
|
1050
|
+
}
|
|
1051
|
+
const returnedIss = params.get("iss");
|
|
1052
|
+
if (returnedIss) {
|
|
1053
|
+
const discovery = await this._getDiscovery();
|
|
1054
|
+
const expectedIssuer = (discovery?.issuer || this.config.ssoBaseUrl).replace(/\/+$/, "");
|
|
1055
|
+
if (returnedIss.replace(/\/+$/, "") !== expectedIssuer) {
|
|
1056
|
+
removeOAuthState(this.config.clientId);
|
|
1057
|
+
removeOAuthNonce(this.config.clientId);
|
|
1058
|
+
removePkceVerifier(this.config.clientId);
|
|
1059
|
+
removeSilentProbe(this.config.clientId);
|
|
1060
|
+
throw new SsoError("issuer_mismatch", "\u6388\u6743\u54CD\u5E94 issuer \u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 IdP \u6DF7\u53E0\u653B\u51FB");
|
|
1061
|
+
}
|
|
1000
1062
|
}
|
|
1001
1063
|
const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
|
|
1002
1064
|
const verifier = getPkceVerifier(this.config.clientId);
|
|
1003
1065
|
if (!verifier) {
|
|
1004
|
-
throw new SsoError(
|
|
1005
|
-
"pkce_required",
|
|
1006
|
-
"code_verifier \u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u6765\u81EA\u5176\u4ED6\u6807\u7B7E\u9875\uFF09"
|
|
1007
|
-
);
|
|
1066
|
+
throw new SsoError("pkce_required", "code_verifier \u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u6765\u81EA\u5176\u4ED6\u6807\u7B7E\u9875\uFF09");
|
|
1008
1067
|
}
|
|
1009
1068
|
const tokenEndpoint = await this._getTokenEndpoint();
|
|
1010
1069
|
const body = new URLSearchParams();
|
|
@@ -1027,6 +1086,10 @@ var _SsoClient = class _SsoClient {
|
|
|
1027
1086
|
throw new SsoError("network_error", "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", err);
|
|
1028
1087
|
}
|
|
1029
1088
|
if (!res.ok) {
|
|
1089
|
+
removeOAuthState(this.config.clientId);
|
|
1090
|
+
removeOAuthNonce(this.config.clientId);
|
|
1091
|
+
removePkceVerifier(this.config.clientId);
|
|
1092
|
+
removeSilentProbe(this.config.clientId);
|
|
1030
1093
|
let errData = {};
|
|
1031
1094
|
try {
|
|
1032
1095
|
errData = await res.json();
|
|
@@ -1042,11 +1105,12 @@ var _SsoClient = class _SsoClient {
|
|
|
1042
1105
|
assertValidTokenResponse(data, true);
|
|
1043
1106
|
const requestedScopes = (this.config.scopes || "openid profile").split(" ").filter(Boolean);
|
|
1044
1107
|
if (requestedScopes.includes("openid") && !data.id_token) {
|
|
1108
|
+
removeOAuthState(this.config.clientId);
|
|
1109
|
+
removeOAuthNonce(this.config.clientId);
|
|
1110
|
+
removePkceVerifier(this.config.clientId);
|
|
1111
|
+
removeSilentProbe(this.config.clientId);
|
|
1045
1112
|
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
|
-
);
|
|
1113
|
+
throw new SsoError("id_token_invalid", "Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09");
|
|
1050
1114
|
}
|
|
1051
1115
|
if (data.id_token) {
|
|
1052
1116
|
try {
|
|
@@ -1111,19 +1175,14 @@ var _SsoClient = class _SsoClient {
|
|
|
1111
1175
|
}
|
|
1112
1176
|
let res = null;
|
|
1113
1177
|
let lastErr;
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
break;
|
|
1123
|
-
} catch (err) {
|
|
1124
|
-
lastErr = err;
|
|
1125
|
-
if (attempt < 1) await new Promise((r) => setTimeout(r, 1e3));
|
|
1126
|
-
}
|
|
1178
|
+
try {
|
|
1179
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
1180
|
+
method: "POST",
|
|
1181
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1182
|
+
body: body.toString()
|
|
1183
|
+
});
|
|
1184
|
+
} catch (err) {
|
|
1185
|
+
lastErr = err;
|
|
1127
1186
|
}
|
|
1128
1187
|
if (lastErr || !res) {
|
|
1129
1188
|
throw new SsoError("network_error", "\u5237\u65B0 Token \u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", lastErr);
|
|
@@ -1186,7 +1245,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1186
1245
|
if (!tokenData) {
|
|
1187
1246
|
throw new SsoError("not_authenticated", "\u672A\u767B\u5F55");
|
|
1188
1247
|
}
|
|
1189
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1248
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1190
1249
|
tokenData = await this.refreshToken();
|
|
1191
1250
|
}
|
|
1192
1251
|
const userinfoEndpoint = await this._getUserinfoEndpoint();
|
|
@@ -1205,10 +1264,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1205
1264
|
removeTokenData(this.config.clientId);
|
|
1206
1265
|
throw new SsoError("not_authenticated", "Token \u5DF2\u5931\u6548");
|
|
1207
1266
|
}
|
|
1208
|
-
throw new SsoError(
|
|
1209
|
-
"userinfo_failed",
|
|
1210
|
-
`\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: HTTP ${res.status}`
|
|
1211
|
-
);
|
|
1267
|
+
throw new SsoError("userinfo_failed", `\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: HTTP ${res.status}`);
|
|
1212
1268
|
}
|
|
1213
1269
|
return await res.json();
|
|
1214
1270
|
}
|
|
@@ -1221,7 +1277,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1221
1277
|
async getAccessToken() {
|
|
1222
1278
|
let tokenData = getTokenData(this.config.clientId);
|
|
1223
1279
|
if (!tokenData) return null;
|
|
1224
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1280
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1225
1281
|
tokenData = await this.refreshToken();
|
|
1226
1282
|
}
|
|
1227
1283
|
return tokenData.access_token;
|
|
@@ -1230,6 +1286,9 @@ var _SsoClient = class _SsoClient {
|
|
|
1230
1286
|
* 检查是否已认证(不发起网络请求)
|
|
1231
1287
|
*
|
|
1232
1288
|
* 仅检查本地是否存在未过期的 access_token。
|
|
1289
|
+
* 故意不做 EXPIRY_SKEW_MS 提前量:这是同步的 UI 提示判定(是否显示"已登录"),
|
|
1290
|
+
* 提前 10s 翻转 false 会让调用方在 token 仍有效时误显示未登录;真正发请求的
|
|
1291
|
+
* getAccessToken/getUserInfo 已带提前量。
|
|
1233
1292
|
*/
|
|
1234
1293
|
isAuthenticated() {
|
|
1235
1294
|
const tokenData = getTokenData(this.config.clientId);
|
|
@@ -1265,7 +1324,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1265
1324
|
if (this.config.clientSecret) {
|
|
1266
1325
|
revokeBody.set("client_secret", this.config.clientSecret);
|
|
1267
1326
|
}
|
|
1268
|
-
|
|
1327
|
+
const doRevoke = () => fetchWithTimeout(
|
|
1269
1328
|
revokeUrl,
|
|
1270
1329
|
{
|
|
1271
1330
|
method: "POST",
|
|
@@ -1274,6 +1333,14 @@ var _SsoClient = class _SsoClient {
|
|
|
1274
1333
|
},
|
|
1275
1334
|
REVOKE_TIMEOUT_MS
|
|
1276
1335
|
);
|
|
1336
|
+
let res = await doRevoke();
|
|
1337
|
+
if (!res.ok && (res.status >= 500 || res.status === 429)) {
|
|
1338
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1339
|
+
res = await doRevoke();
|
|
1340
|
+
}
|
|
1341
|
+
if (!res.ok) {
|
|
1342
|
+
this.debugLog("logout: \u670D\u52A1\u7AEF\u64A4\u9500 refresh_token \u5931\u8D25", { status: res.status });
|
|
1343
|
+
}
|
|
1277
1344
|
} catch {
|
|
1278
1345
|
}
|
|
1279
1346
|
}
|
|
@@ -1284,10 +1351,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1284
1351
|
logoutUrl.searchParams.set("client_id", this.config.clientId);
|
|
1285
1352
|
const postLogoutUri = this.config.postLogoutRedirectUri || this.config.redirectUri;
|
|
1286
1353
|
if (postLogoutUri) {
|
|
1287
|
-
logoutUrl.searchParams.set(
|
|
1288
|
-
"post_logout_redirect_uri",
|
|
1289
|
-
postLogoutUri
|
|
1290
|
-
);
|
|
1354
|
+
logoutUrl.searchParams.set("post_logout_redirect_uri", postLogoutUri);
|
|
1291
1355
|
}
|
|
1292
1356
|
if (idTokenHint) {
|
|
1293
1357
|
logoutUrl.searchParams.set("id_token_hint", idTokenHint);
|