@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/react/index.js
CHANGED
|
@@ -135,6 +135,9 @@ function createMemoryStorageAdapter() {
|
|
|
135
135
|
},
|
|
136
136
|
remove(key) {
|
|
137
137
|
store.delete(key);
|
|
138
|
+
},
|
|
139
|
+
keys() {
|
|
140
|
+
return [...store.keys()];
|
|
138
141
|
}
|
|
139
142
|
};
|
|
140
143
|
}
|
|
@@ -143,26 +146,44 @@ function createSessionStorageAdapter() {
|
|
|
143
146
|
const fallback = /* @__PURE__ */ new Map();
|
|
144
147
|
return {
|
|
145
148
|
get(key) {
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
try {
|
|
150
|
+
if (typeof sessionStorage !== "undefined") {
|
|
151
|
+
return sessionStorage.getItem(STORAGE_PREFIX + key) ?? fallback.get(key) ?? null;
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
148
154
|
}
|
|
149
155
|
return fallback.get(key) ?? null;
|
|
150
156
|
},
|
|
151
157
|
set(key, value) {
|
|
152
|
-
if (typeof sessionStorage === "undefined") {
|
|
153
|
-
fallback.set(key, value);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
158
|
try {
|
|
157
|
-
|
|
159
|
+
if (typeof sessionStorage !== "undefined") {
|
|
160
|
+
sessionStorage.setItem(STORAGE_PREFIX + key, value);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
158
163
|
} catch {
|
|
159
|
-
fallback.set(key, value);
|
|
160
164
|
}
|
|
165
|
+
fallback.set(key, value);
|
|
161
166
|
},
|
|
162
167
|
remove(key) {
|
|
163
168
|
fallback.delete(key);
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
try {
|
|
170
|
+
if (typeof sessionStorage === "undefined") return;
|
|
171
|
+
sessionStorage.removeItem(STORAGE_PREFIX + key);
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
keys() {
|
|
176
|
+
const result = new Set(fallback.keys());
|
|
177
|
+
try {
|
|
178
|
+
if (typeof sessionStorage !== "undefined") {
|
|
179
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
180
|
+
const key = sessionStorage.key(i);
|
|
181
|
+
if (key?.startsWith(STORAGE_PREFIX)) result.add(key.slice(STORAGE_PREFIX.length));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
return [...result];
|
|
166
187
|
}
|
|
167
188
|
};
|
|
168
189
|
}
|
|
@@ -255,32 +276,58 @@ function clearAllSsoData(clientId) {
|
|
|
255
276
|
removeReturnUrl();
|
|
256
277
|
removeLogoutState();
|
|
257
278
|
removeSilentProbe();
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
typeof sessionStorage !== "undefined"
|
|
261
|
-
|
|
262
|
-
|
|
279
|
+
const stores = [];
|
|
280
|
+
try {
|
|
281
|
+
if (typeof sessionStorage !== "undefined") stores.push(sessionStorage);
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
if (typeof localStorage !== "undefined") stores.push(localStorage);
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
const abstractKeys = /* @__PURE__ */ new Set();
|
|
263
289
|
for (const store of stores) {
|
|
264
|
-
if (!store) continue;
|
|
265
290
|
const keys = [];
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
291
|
+
try {
|
|
292
|
+
for (let i = 0; i < store.length; i++) {
|
|
293
|
+
const key = store.key(i);
|
|
294
|
+
if (key?.startsWith(STORAGE_PREFIX)) keys.push(key);
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
continue;
|
|
269
298
|
}
|
|
270
299
|
for (const key of keys) {
|
|
271
|
-
|
|
272
|
-
|
|
300
|
+
abstractKeys.add(key.slice(STORAGE_PREFIX.length));
|
|
301
|
+
try {
|
|
302
|
+
store.removeItem(key);
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (const store of [_transient, _storage]) {
|
|
308
|
+
try {
|
|
309
|
+
for (const key of store.keys?.() ?? []) abstractKeys.add(key);
|
|
310
|
+
} catch {
|
|
273
311
|
}
|
|
274
312
|
}
|
|
313
|
+
for (const key of abstractKeys) {
|
|
314
|
+
_transient.remove(key);
|
|
315
|
+
_storage.remove(key);
|
|
316
|
+
}
|
|
275
317
|
}
|
|
276
318
|
|
|
277
319
|
// src/core/security.ts
|
|
320
|
+
var UNSAFE_URL_CHAR_PATTERN = /[\\\u0000-\u001F\u007F]/;
|
|
278
321
|
function isTrustedReturnUrl(url, currentOrigin) {
|
|
279
322
|
if (!url) return false;
|
|
280
|
-
if (
|
|
281
|
-
if (url.startsWith("
|
|
323
|
+
if (UNSAFE_URL_CHAR_PATTERN.test(url)) return false;
|
|
324
|
+
if (url.startsWith("//")) return false;
|
|
325
|
+
if (url.startsWith("/")) return true;
|
|
282
326
|
try {
|
|
283
|
-
|
|
327
|
+
const parsed = new URL(url);
|
|
328
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
329
|
+
if (parsed.username || parsed.password) return false;
|
|
330
|
+
return parsed.origin === currentOrigin;
|
|
284
331
|
} catch {
|
|
285
332
|
return false;
|
|
286
333
|
}
|
|
@@ -492,7 +539,10 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
492
539
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
493
540
|
throw new SsoError("id_token_expired", "ID Token \u5DF2\u8FC7\u671F");
|
|
494
541
|
}
|
|
495
|
-
if (typeof payload.iat
|
|
542
|
+
if (typeof payload.iat !== "number") {
|
|
543
|
+
throw new SsoError("id_token_invalid", "ID Token \u7F3A\u5C11 iat \u58F0\u660E");
|
|
544
|
+
}
|
|
545
|
+
if (payload.iat * 1e3 > Date.now() + 6e4) {
|
|
496
546
|
throw new SsoError("id_token_invalid", "ID Token iat \u5728\u672A\u6765\uFF0C\u7591\u4F3C\u4F2A\u9020\u6216\u65F6\u949F\u5F02\u5E38");
|
|
497
547
|
}
|
|
498
548
|
if (typeof payload.sub !== "string" || !payload.sub) {
|
|
@@ -509,11 +559,47 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
509
559
|
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
510
560
|
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
511
561
|
}
|
|
562
|
+
} else if (typeof payload.nonce === "string" && payload.nonce) {
|
|
563
|
+
throw new SsoError(
|
|
564
|
+
"id_token_nonce_mismatch",
|
|
565
|
+
"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"
|
|
566
|
+
);
|
|
512
567
|
}
|
|
513
568
|
return { sub: payload.sub };
|
|
514
569
|
}
|
|
515
570
|
|
|
571
|
+
// src/core/http.ts
|
|
572
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
573
|
+
async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
574
|
+
const controller = new AbortController();
|
|
575
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
576
|
+
try {
|
|
577
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
578
|
+
} finally {
|
|
579
|
+
clearTimeout(timer);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
516
583
|
// src/core/SsoClient.ts
|
|
584
|
+
var REVOKE_TIMEOUT_MS = 3e3;
|
|
585
|
+
var EXPIRY_SKEW_MS = 1e4;
|
|
586
|
+
function assertValidTokenResponse(data, requireRefreshToken) {
|
|
587
|
+
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
588
|
+
throw new SsoError("token_request_failed", "Token \u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
589
|
+
}
|
|
590
|
+
if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
|
|
591
|
+
throw new SsoError(
|
|
592
|
+
"token_request_failed",
|
|
593
|
+
`Token \u54CD\u5E94 expires_in \u975E\u6CD5: ${String(data.expires_in)}`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (requireRefreshToken && (typeof data.refresh_token !== "string" || !data.refresh_token)) {
|
|
597
|
+
throw new SsoError(
|
|
598
|
+
"token_request_failed",
|
|
599
|
+
"Token \u54CD\u5E94\u7F3A\u5C11 refresh_token\uFF08authorization_code \u4EA4\u6362\u5FC5\u987B\u8FD4\u56DE\uFF09"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
517
603
|
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
|
|
518
604
|
"login_required",
|
|
519
605
|
"consent_required",
|
|
@@ -524,6 +610,8 @@ var _SsoClient = class _SsoClient {
|
|
|
524
610
|
this._discovery = null;
|
|
525
611
|
this._discoveryFetchedAt = 0;
|
|
526
612
|
this._refreshLock = null;
|
|
613
|
+
/** Discovery 端点 origin 校验失败只告警一次(拒绝的文档不缓存,避免每次调用刷屏) */
|
|
614
|
+
this._discoveryOriginWarned = false;
|
|
527
615
|
if (!config.clientId) throw new SsoError("invalid_config", "clientId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
528
616
|
if (!config.redirectUri) throw new SsoError("invalid_config", "redirectUri \u4E0D\u80FD\u4E3A\u7A7A");
|
|
529
617
|
if (!config.ssoBaseUrl) throw new SsoError("invalid_config", "ssoBaseUrl \u4E0D\u80FD\u4E3A\u7A7A");
|
|
@@ -532,8 +620,67 @@ var _SsoClient = class _SsoClient {
|
|
|
532
620
|
this._serverBase = (config.serverBaseUrl ?? base).replace(/\/+$/, "");
|
|
533
621
|
}
|
|
534
622
|
// ============================================
|
|
623
|
+
// 调试日志
|
|
624
|
+
// ============================================
|
|
625
|
+
/**
|
|
626
|
+
* 调试日志(config.debug=true 时输出,前缀 [SSO SDK]):
|
|
627
|
+
* 覆盖登录发起 / 回调结果 / 刷新失败 / 登出等关键节点,便于接入方定位问题。
|
|
628
|
+
* 不会输出 token/授权码等敏感值。
|
|
629
|
+
*/
|
|
630
|
+
debugLog(...args) {
|
|
631
|
+
if (this.config.debug) {
|
|
632
|
+
console.warn("[SSO SDK]", ...args);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// ============================================
|
|
535
636
|
// 内部方法
|
|
536
637
|
// ============================================
|
|
638
|
+
/**
|
|
639
|
+
* 校验 Discovery 文档端点 origin(防文档被篡改后 token/code 被导向攻击者服务器):
|
|
640
|
+
* - authorization_endpoint 必须与 ssoBaseUrl 同源(浏览器跳转目标);
|
|
641
|
+
* - token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl 同源,
|
|
642
|
+
* 或(配置了 serverBaseUrl 时)与内网地址同源。
|
|
643
|
+
* 任一不匹配:告警并返回 null(上层回退到默认端点,fail-safe)。
|
|
644
|
+
* 可通过 config.allowCrossOriginDiscoveryEndpoints 关闭(见配置项警告)。
|
|
645
|
+
*/
|
|
646
|
+
_sanitizeDiscovery(d) {
|
|
647
|
+
if (this.config.allowCrossOriginDiscoveryEndpoints) return d;
|
|
648
|
+
const allowedOrigins = /* @__PURE__ */ new Set();
|
|
649
|
+
try {
|
|
650
|
+
allowedOrigins.add(new URL(this.config.ssoBaseUrl).origin);
|
|
651
|
+
allowedOrigins.add(new URL(this._serverBase).origin);
|
|
652
|
+
} catch {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
const sameOrigin = (endpoint) => {
|
|
656
|
+
try {
|
|
657
|
+
return allowedOrigins.has(new URL(endpoint).origin);
|
|
658
|
+
} catch {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
const publicOrigin = new URL(this.config.ssoBaseUrl).origin;
|
|
663
|
+
let authorizationOk = false;
|
|
664
|
+
try {
|
|
665
|
+
authorizationOk = new URL(d.authorization_endpoint).origin === publicOrigin;
|
|
666
|
+
} catch {
|
|
667
|
+
authorizationOk = false;
|
|
668
|
+
}
|
|
669
|
+
const bad = [];
|
|
670
|
+
if (!authorizationOk) bad.push("authorization_endpoint");
|
|
671
|
+
if (!sameOrigin(d.token_endpoint)) bad.push("token_endpoint");
|
|
672
|
+
if (!sameOrigin(d.userinfo_endpoint)) bad.push("userinfo_endpoint");
|
|
673
|
+
if (bad.length > 0) {
|
|
674
|
+
if (!this._discoveryOriginWarned) {
|
|
675
|
+
this._discoveryOriginWarned = true;
|
|
676
|
+
console.warn(
|
|
677
|
+
`[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`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
return d;
|
|
683
|
+
}
|
|
537
684
|
/**
|
|
538
685
|
* 获取 OIDC Discovery 文档(带缓存 + 超时)
|
|
539
686
|
*
|
|
@@ -560,7 +707,10 @@ var _SsoClient = class _SsoClient {
|
|
|
560
707
|
);
|
|
561
708
|
return null;
|
|
562
709
|
}
|
|
563
|
-
|
|
710
|
+
const doc = await res.json();
|
|
711
|
+
const sanitized = this._sanitizeDiscovery(doc);
|
|
712
|
+
if (!sanitized) return null;
|
|
713
|
+
this._discovery = sanitized;
|
|
564
714
|
this._discoveryFetchedAt = now;
|
|
565
715
|
return this._discovery;
|
|
566
716
|
} catch (err) {
|
|
@@ -911,7 +1061,7 @@ var _SsoClient = class _SsoClient {
|
|
|
911
1061
|
}
|
|
912
1062
|
let res;
|
|
913
1063
|
try {
|
|
914
|
-
res = await
|
|
1064
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
915
1065
|
method: "POST",
|
|
916
1066
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
917
1067
|
body: body.toString()
|
|
@@ -920,6 +1070,10 @@ var _SsoClient = class _SsoClient {
|
|
|
920
1070
|
throw new SsoError("network_error", "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", err);
|
|
921
1071
|
}
|
|
922
1072
|
if (!res.ok) {
|
|
1073
|
+
removeOAuthState(this.config.clientId);
|
|
1074
|
+
removeOAuthNonce(this.config.clientId);
|
|
1075
|
+
removePkceVerifier(this.config.clientId);
|
|
1076
|
+
removeSilentProbe(this.config.clientId);
|
|
923
1077
|
let errData = {};
|
|
924
1078
|
try {
|
|
925
1079
|
errData = await res.json();
|
|
@@ -932,6 +1086,19 @@ var _SsoClient = class _SsoClient {
|
|
|
932
1086
|
);
|
|
933
1087
|
}
|
|
934
1088
|
const data = await res.json();
|
|
1089
|
+
assertValidTokenResponse(data, true);
|
|
1090
|
+
const requestedScopes = (this.config.scopes || "openid profile").split(" ").filter(Boolean);
|
|
1091
|
+
if (requestedScopes.includes("openid") && !data.id_token) {
|
|
1092
|
+
removeOAuthState(this.config.clientId);
|
|
1093
|
+
removeOAuthNonce(this.config.clientId);
|
|
1094
|
+
removePkceVerifier(this.config.clientId);
|
|
1095
|
+
removeSilentProbe(this.config.clientId);
|
|
1096
|
+
removeTokenData(this.config.clientId);
|
|
1097
|
+
throw new SsoError(
|
|
1098
|
+
"id_token_invalid",
|
|
1099
|
+
"Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09"
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
935
1102
|
if (data.id_token) {
|
|
936
1103
|
try {
|
|
937
1104
|
await validateIdToken(
|
|
@@ -961,6 +1128,7 @@ var _SsoClient = class _SsoClient {
|
|
|
961
1128
|
expires_at: now + data.expires_in * 1e3
|
|
962
1129
|
};
|
|
963
1130
|
saveTokenData(tokenData, this.config.clientId);
|
|
1131
|
+
this.debugLog("handleCallback: token \u4EA4\u6362\u6210\u529F", { clientId: this.config.clientId });
|
|
964
1132
|
return tokenData;
|
|
965
1133
|
}
|
|
966
1134
|
/**
|
|
@@ -996,7 +1164,7 @@ var _SsoClient = class _SsoClient {
|
|
|
996
1164
|
let lastErr;
|
|
997
1165
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
998
1166
|
try {
|
|
999
|
-
res = await
|
|
1167
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
1000
1168
|
method: "POST",
|
|
1001
1169
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1002
1170
|
body: body.toString()
|
|
@@ -1018,15 +1186,20 @@ var _SsoClient = class _SsoClient {
|
|
|
1018
1186
|
} catch {
|
|
1019
1187
|
}
|
|
1020
1188
|
const errorCode = errData.error || "";
|
|
1021
|
-
if (errorCode === "invalid_grant"
|
|
1189
|
+
if (errorCode === "invalid_grant") {
|
|
1022
1190
|
removeTokenData(this.config.clientId);
|
|
1023
1191
|
}
|
|
1192
|
+
this.debugLog("refreshToken: \u5237\u65B0\u5931\u8D25", { status: res.status, error: errorCode });
|
|
1193
|
+
if (!errorCode && res.status === 401) {
|
|
1194
|
+
throw new SsoError("sso_server_error", "\u5237\u65B0 Token \u5931\u8D25\uFF08\u7F51\u5173\u5F02\u5E38\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
1195
|
+
}
|
|
1024
1196
|
throw new SsoError(
|
|
1025
1197
|
mapOAuthErrorToSsoCode(errorCode, "refresh"),
|
|
1026
1198
|
errData.error_description || `\u5237\u65B0 Token \u5931\u8D25: HTTP ${res.status}`
|
|
1027
1199
|
);
|
|
1028
1200
|
}
|
|
1029
1201
|
const data = await res.json();
|
|
1202
|
+
assertValidTokenResponse(data, false);
|
|
1030
1203
|
if (data.id_token) {
|
|
1031
1204
|
try {
|
|
1032
1205
|
await validateIdToken(
|
|
@@ -1064,13 +1237,13 @@ var _SsoClient = class _SsoClient {
|
|
|
1064
1237
|
if (!tokenData) {
|
|
1065
1238
|
throw new SsoError("not_authenticated", "\u672A\u767B\u5F55");
|
|
1066
1239
|
}
|
|
1067
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1240
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1068
1241
|
tokenData = await this.refreshToken();
|
|
1069
1242
|
}
|
|
1070
1243
|
const userinfoEndpoint = await this._getUserinfoEndpoint();
|
|
1071
1244
|
let res;
|
|
1072
1245
|
try {
|
|
1073
|
-
res = await
|
|
1246
|
+
res = await fetchWithTimeout(userinfoEndpoint, {
|
|
1074
1247
|
headers: {
|
|
1075
1248
|
Authorization: `Bearer ${tokenData.access_token}`
|
|
1076
1249
|
}
|
|
@@ -1099,7 +1272,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1099
1272
|
async getAccessToken() {
|
|
1100
1273
|
let tokenData = getTokenData(this.config.clientId);
|
|
1101
1274
|
if (!tokenData) return null;
|
|
1102
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1275
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1103
1276
|
tokenData = await this.refreshToken();
|
|
1104
1277
|
}
|
|
1105
1278
|
return tokenData.access_token;
|
|
@@ -1108,6 +1281,9 @@ var _SsoClient = class _SsoClient {
|
|
|
1108
1281
|
* 检查是否已认证(不发起网络请求)
|
|
1109
1282
|
*
|
|
1110
1283
|
* 仅检查本地是否存在未过期的 access_token。
|
|
1284
|
+
* 故意不做 EXPIRY_SKEW_MS 提前量:这是同步的 UI 提示判定(是否显示"已登录"),
|
|
1285
|
+
* 提前 10s 翻转 false 会让调用方在 token 仍有效时误显示未登录;真正发请求的
|
|
1286
|
+
* getAccessToken/getUserInfo 已带提前量。
|
|
1111
1287
|
*/
|
|
1112
1288
|
isAuthenticated() {
|
|
1113
1289
|
const tokenData = getTokenData(this.config.clientId);
|
|
@@ -1127,6 +1303,10 @@ var _SsoClient = class _SsoClient {
|
|
|
1127
1303
|
const refreshToken = tokenData?.refresh_token;
|
|
1128
1304
|
const idTokenHint = tokenData?.id_token;
|
|
1129
1305
|
clearAllSsoData(this.config.clientId);
|
|
1306
|
+
this.debugLog("logout: \u672C\u5730\u6570\u636E\u5DF2\u6E05\u9664", {
|
|
1307
|
+
clientId: this.config.clientId,
|
|
1308
|
+
redirectToSso
|
|
1309
|
+
});
|
|
1130
1310
|
if (refreshToken && this.config.clientId) {
|
|
1131
1311
|
try {
|
|
1132
1312
|
const discovery = this.config.serverBaseUrl ? null : await this._getDiscovery();
|
|
@@ -1139,11 +1319,15 @@ var _SsoClient = class _SsoClient {
|
|
|
1139
1319
|
if (this.config.clientSecret) {
|
|
1140
1320
|
revokeBody.set("client_secret", this.config.clientSecret);
|
|
1141
1321
|
}
|
|
1142
|
-
await
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1322
|
+
await fetchWithTimeout(
|
|
1323
|
+
revokeUrl,
|
|
1324
|
+
{
|
|
1325
|
+
method: "POST",
|
|
1326
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1327
|
+
body: revokeBody.toString()
|
|
1328
|
+
},
|
|
1329
|
+
REVOKE_TIMEOUT_MS
|
|
1330
|
+
);
|
|
1147
1331
|
} catch {
|
|
1148
1332
|
}
|
|
1149
1333
|
}
|
|
@@ -1213,6 +1397,21 @@ var SsoClient = _SsoClient;
|
|
|
1213
1397
|
var SsoContext = (0, import_react.createContext)(null);
|
|
1214
1398
|
var REFRESH_LOCK_PREFIX = "nihplod_sso_refresh_lock:";
|
|
1215
1399
|
var LOCK_TTL_MS = 5e3;
|
|
1400
|
+
var CHANNEL_PREFIX = "nihplod_sso_events:";
|
|
1401
|
+
var TAB_ID = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
1402
|
+
function broadcastSsoEvent(clientId, type, tokenData) {
|
|
1403
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
1404
|
+
try {
|
|
1405
|
+
const channel = new BroadcastChannel(CHANNEL_PREFIX + clientId);
|
|
1406
|
+
channel.postMessage({
|
|
1407
|
+
type,
|
|
1408
|
+
sourceTabId: TAB_ID,
|
|
1409
|
+
...type === "token" && tokenData ? { tokenData } : {}
|
|
1410
|
+
});
|
|
1411
|
+
channel.close();
|
|
1412
|
+
} catch {
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1216
1415
|
var ownedLocks = /* @__PURE__ */ new Map();
|
|
1217
1416
|
function lockKey(clientId) {
|
|
1218
1417
|
return REFRESH_LOCK_PREFIX + clientId;
|
|
@@ -1271,21 +1470,37 @@ function SsoProvider({
|
|
|
1271
1470
|
config,
|
|
1272
1471
|
children,
|
|
1273
1472
|
refreshThreshold = 60,
|
|
1274
|
-
onTokenRefreshed
|
|
1473
|
+
onTokenRefreshed,
|
|
1474
|
+
onSessionExpired
|
|
1275
1475
|
}) {
|
|
1276
1476
|
const [user, setUser] = (0, import_react.useState)(null);
|
|
1277
1477
|
const [isLoading, setIsLoading] = (0, import_react.useState)(true);
|
|
1278
1478
|
const [isAuthenticated, setIsAuthenticated] = (0, import_react.useState)(false);
|
|
1279
1479
|
const [error, setError] = (0, import_react.useState)(null);
|
|
1480
|
+
const [sessionExpired, setSessionExpired] = (0, import_react.useState)(false);
|
|
1280
1481
|
const [client] = (0, import_react.useState)(() => new SsoClient(config));
|
|
1281
1482
|
const refreshTimerRef = (0, import_react.useRef)(null);
|
|
1282
1483
|
const loadedRef = (0, import_react.useRef)(false);
|
|
1484
|
+
const onSessionExpiredRef = (0, import_react.useRef)(onSessionExpired);
|
|
1485
|
+
(0, import_react.useEffect)(() => {
|
|
1486
|
+
onSessionExpiredRef.current = onSessionExpired;
|
|
1487
|
+
});
|
|
1488
|
+
const sessionExpiredNotifiedRef = (0, import_react.useRef)(false);
|
|
1489
|
+
const refreshFailureCountRef = (0, import_react.useRef)(0);
|
|
1490
|
+
const MAX_REFRESH_FAILURES = 3;
|
|
1491
|
+
const markSessionExpired = (0, import_react.useCallback)((error2) => {
|
|
1492
|
+
setError(error2);
|
|
1493
|
+
setSessionExpired(true);
|
|
1494
|
+
if (!sessionExpiredNotifiedRef.current) {
|
|
1495
|
+
sessionExpiredNotifiedRef.current = true;
|
|
1496
|
+
onSessionExpiredRef.current?.(error2);
|
|
1497
|
+
}
|
|
1498
|
+
}, []);
|
|
1283
1499
|
const loadUser = (0, import_react.useCallback)(async () => {
|
|
1284
1500
|
const tokenData = getTokenData(client.config.clientId);
|
|
1285
1501
|
if (!tokenData) {
|
|
1286
1502
|
setUser(null);
|
|
1287
1503
|
setIsAuthenticated(false);
|
|
1288
|
-
setError(null);
|
|
1289
1504
|
setIsLoading(false);
|
|
1290
1505
|
return;
|
|
1291
1506
|
}
|
|
@@ -1294,19 +1509,22 @@ function SsoProvider({
|
|
|
1294
1509
|
setUser(u);
|
|
1295
1510
|
setIsAuthenticated(true);
|
|
1296
1511
|
setError(null);
|
|
1512
|
+
setSessionExpired(false);
|
|
1513
|
+
sessionExpiredNotifiedRef.current = false;
|
|
1514
|
+
refreshFailureCountRef.current = 0;
|
|
1297
1515
|
} catch (err) {
|
|
1298
1516
|
setUser(null);
|
|
1299
1517
|
setIsAuthenticated(false);
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
)
|
|
1303
|
-
if (err instanceof SsoError && (err.code === "not_authenticated" || err.code === "session_expired")) {
|
|
1518
|
+
const errorObj = err instanceof SsoError ? err : new SsoError("userinfo_failed", err instanceof Error ? err.message : String(err));
|
|
1519
|
+
setError(errorObj);
|
|
1520
|
+
if (errorObj.code === "not_authenticated" || errorObj.code === "session_expired") {
|
|
1304
1521
|
removeTokenData(client.config.clientId);
|
|
1522
|
+
markSessionExpired(errorObj);
|
|
1305
1523
|
}
|
|
1306
1524
|
} finally {
|
|
1307
1525
|
setIsLoading(false);
|
|
1308
1526
|
}
|
|
1309
|
-
}, [client]);
|
|
1527
|
+
}, [client, markSessionExpired]);
|
|
1310
1528
|
(0, import_react.useEffect)(() => {
|
|
1311
1529
|
if (loadedRef.current) return;
|
|
1312
1530
|
loadedRef.current = true;
|
|
@@ -1323,13 +1541,28 @@ function SsoProvider({
|
|
|
1323
1541
|
const attemptRefresh = () => withRefreshLock(client.config.clientId, async () => {
|
|
1324
1542
|
try {
|
|
1325
1543
|
const td = await client.refreshToken();
|
|
1544
|
+
refreshFailureCountRef.current = 0;
|
|
1326
1545
|
onTokenRefreshed?.(td.access_token);
|
|
1546
|
+
broadcastSsoEvent(client.config.clientId, "token", td);
|
|
1327
1547
|
loadUser();
|
|
1328
|
-
} catch {
|
|
1548
|
+
} catch (err) {
|
|
1549
|
+
if (err instanceof SsoError && (err.code === "session_expired" || err.code === "no_refresh_token" || err.code === "not_authenticated")) {
|
|
1550
|
+
removeTokenData(client.config.clientId);
|
|
1551
|
+
markSessionExpired(err);
|
|
1552
|
+
} else {
|
|
1553
|
+
refreshFailureCountRef.current += 1;
|
|
1554
|
+
if (refreshFailureCountRef.current >= MAX_REFRESH_FAILURES) {
|
|
1555
|
+
markSessionExpired(
|
|
1556
|
+
err instanceof SsoError ? err : new SsoError("session_expired", "\u591A\u6B21\u5237\u65B0\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55")
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1329
1560
|
setTimeout(() => loadUser(), 500);
|
|
1330
1561
|
}
|
|
1562
|
+
}).finally(() => {
|
|
1563
|
+
if (active) scheduleNextRefresh(5e3);
|
|
1331
1564
|
});
|
|
1332
|
-
const scheduleNextRefresh = () => {
|
|
1565
|
+
const scheduleNextRefresh = (minDelayMs = 0) => {
|
|
1333
1566
|
if (!active) return;
|
|
1334
1567
|
if (refreshTimerRef.current) {
|
|
1335
1568
|
clearTimeout(refreshTimerRef.current);
|
|
@@ -1338,26 +1571,27 @@ function SsoProvider({
|
|
|
1338
1571
|
const tokenData = getTokenData(client.config.clientId);
|
|
1339
1572
|
if (!tokenData) return;
|
|
1340
1573
|
const remainingSec = (tokenData.expires_at - Date.now()) / 1e3;
|
|
1341
|
-
if (remainingSec <= 0) {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1574
|
+
if (remainingSec <= 0 || remainingSec <= refreshThreshold) {
|
|
1575
|
+
refreshTimerRef.current = setTimeout(
|
|
1576
|
+
() => {
|
|
1577
|
+
if (active) void attemptRefresh();
|
|
1578
|
+
},
|
|
1579
|
+
Math.max(minDelayMs, 1e3)
|
|
1580
|
+
);
|
|
1347
1581
|
return;
|
|
1348
1582
|
}
|
|
1349
|
-
const delayMs = (remainingSec - refreshThreshold) * 1e3;
|
|
1583
|
+
const delayMs = Math.max((remainingSec - refreshThreshold) * 1e3, minDelayMs, 1e3);
|
|
1350
1584
|
refreshTimerRef.current = setTimeout(() => {
|
|
1351
1585
|
if (!active) return;
|
|
1352
1586
|
const td = getTokenData(client.config.clientId);
|
|
1353
1587
|
if (!td) return;
|
|
1354
1588
|
const secLeft = (td.expires_at - Date.now()) / 1e3;
|
|
1355
1589
|
if (secLeft <= refreshThreshold) {
|
|
1356
|
-
void attemptRefresh()
|
|
1357
|
-
|
|
1358
|
-
|
|
1590
|
+
void attemptRefresh();
|
|
1591
|
+
} else {
|
|
1592
|
+
scheduleNextRefresh();
|
|
1359
1593
|
}
|
|
1360
|
-
},
|
|
1594
|
+
}, delayMs);
|
|
1361
1595
|
};
|
|
1362
1596
|
scheduleNextRefresh();
|
|
1363
1597
|
return () => {
|
|
@@ -1377,6 +1611,30 @@ function SsoProvider({
|
|
|
1377
1611
|
window.addEventListener("storage", handleStorageChange);
|
|
1378
1612
|
return () => window.removeEventListener("storage", handleStorageChange);
|
|
1379
1613
|
}, [loadUser]);
|
|
1614
|
+
(0, import_react.useEffect)(() => {
|
|
1615
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
1616
|
+
const channel = new BroadcastChannel(CHANNEL_PREFIX + client.config.clientId);
|
|
1617
|
+
channel.onmessage = (event) => {
|
|
1618
|
+
const data = event.data;
|
|
1619
|
+
if (!data || data.sourceTabId === TAB_ID) return;
|
|
1620
|
+
if (data.type === "logout") {
|
|
1621
|
+
removeTokenData(client.config.clientId);
|
|
1622
|
+
setUser(null);
|
|
1623
|
+
setIsAuthenticated(false);
|
|
1624
|
+
setError(null);
|
|
1625
|
+
setSessionExpired(false);
|
|
1626
|
+
sessionExpiredNotifiedRef.current = false;
|
|
1627
|
+
refreshFailureCountRef.current = 0;
|
|
1628
|
+
} else if (data.type === "token") {
|
|
1629
|
+
const td = data.tokenData;
|
|
1630
|
+
if (td && typeof td.access_token === "string" && typeof td.expires_at === "number" && typeof td.refresh_token === "string") {
|
|
1631
|
+
saveTokenData(td, client.config.clientId);
|
|
1632
|
+
}
|
|
1633
|
+
loadUser();
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
return () => channel.close();
|
|
1637
|
+
}, [client, loadUser]);
|
|
1380
1638
|
const login = (0, import_react.useCallback)(
|
|
1381
1639
|
async (returnUrl) => {
|
|
1382
1640
|
await client.login(returnUrl);
|
|
@@ -1393,10 +1651,17 @@ function SsoProvider({
|
|
|
1393
1651
|
);
|
|
1394
1652
|
const logout = (0, import_react.useCallback)(
|
|
1395
1653
|
async (redirectToSso = false) => {
|
|
1396
|
-
await client.logout(redirectToSso);
|
|
1397
1654
|
setUser(null);
|
|
1398
1655
|
setIsAuthenticated(false);
|
|
1399
1656
|
setError(null);
|
|
1657
|
+
setSessionExpired(false);
|
|
1658
|
+
sessionExpiredNotifiedRef.current = false;
|
|
1659
|
+
refreshFailureCountRef.current = 0;
|
|
1660
|
+
broadcastSsoEvent(client.config.clientId, "logout");
|
|
1661
|
+
try {
|
|
1662
|
+
await client.logout(redirectToSso);
|
|
1663
|
+
} catch {
|
|
1664
|
+
}
|
|
1400
1665
|
},
|
|
1401
1666
|
[client]
|
|
1402
1667
|
);
|
|
@@ -1416,6 +1681,7 @@ function SsoProvider({
|
|
|
1416
1681
|
logout,
|
|
1417
1682
|
refreshUser,
|
|
1418
1683
|
getAccessToken,
|
|
1684
|
+
sessionExpired,
|
|
1419
1685
|
client
|
|
1420
1686
|
};
|
|
1421
1687
|
return import_react.default.createElement(SsoContext.Provider, { value }, children);
|
|
@@ -1510,6 +1776,36 @@ function withAuth(Component) {
|
|
|
1510
1776
|
|
|
1511
1777
|
// src/react/CallbackPage.tsx
|
|
1512
1778
|
var import_react3 = __toESM(require("react"));
|
|
1779
|
+
var CALLBACK_ERROR_HINTS = {
|
|
1780
|
+
state_mismatch: "\u767B\u5F55\u4F1A\u8BDD\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1781
|
+
pkce_required: "\u767B\u5F55\u4F1A\u8BDD\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1782
|
+
user_denied_authorization: "\u4F60\u5DF2\u53D6\u6D88\u767B\u5F55",
|
|
1783
|
+
session_expired: "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1784
|
+
authorization_code_expired: "\u767B\u5F55\u4FE1\u606F\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1785
|
+
authorization_code_used: "\u767B\u5F55\u4FE1\u606F\u5DF2\u88AB\u4F7F\u7528\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1786
|
+
token_request_failed: "\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1787
|
+
userinfo_failed: "\u767B\u5F55\u4FE1\u606F\u83B7\u53D6\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
|
|
1788
|
+
not_authenticated: "\u8BF7\u5148\u767B\u5F55\u540E\u518D\u8BBF\u95EE",
|
|
1789
|
+
network_error: "\u7F51\u7EDC\u5F02\u5E38\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5",
|
|
1790
|
+
rate_limited: "\u64CD\u4F5C\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
1791
|
+
popup_blocked: "\u767B\u5F55\u7A97\u53E3\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5",
|
|
1792
|
+
popup_closed: "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1793
|
+
client_disabled: "\u8BE5\u5E94\u7528\u5DF2\u505C\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
|
|
1794
|
+
account_disabled: "\u8D26\u53F7\u6682\u65F6\u65E0\u6CD5\u4F7F\u7528\uFF0C\u8BF7\u8054\u7CFB\u5BA2\u670D",
|
|
1795
|
+
sso_server_error: "\u767B\u5F55\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
1796
|
+
invalid_config: "\u5E94\u7528\u914D\u7F6E\u6709\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
|
|
1797
|
+
no_refresh_token: "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"
|
|
1798
|
+
};
|
|
1799
|
+
function getCallbackErrorMessage(err) {
|
|
1800
|
+
if (err instanceof SsoError) {
|
|
1801
|
+
const hint = CALLBACK_ERROR_HINTS[err.code];
|
|
1802
|
+
if (hint) return hint;
|
|
1803
|
+
if (err.code.startsWith("id_token")) return "\u767B\u5F55\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
|
|
1804
|
+
if (/[\u4e00-\u9fff]/.test(err.description)) return err.description;
|
|
1805
|
+
return "\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
|
|
1806
|
+
}
|
|
1807
|
+
return "\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5";
|
|
1808
|
+
}
|
|
1513
1809
|
function DefaultCallbackError({ error }) {
|
|
1514
1810
|
return import_react3.default.createElement(
|
|
1515
1811
|
"div",
|
|
@@ -1520,18 +1816,54 @@ function DefaultCallbackError({ error }) {
|
|
|
1520
1816
|
alignItems: "center",
|
|
1521
1817
|
justifyContent: "center",
|
|
1522
1818
|
minHeight: "100vh",
|
|
1523
|
-
fontFamily: "system-ui, sans-serif"
|
|
1819
|
+
fontFamily: "system-ui, sans-serif",
|
|
1820
|
+
padding: "2rem",
|
|
1821
|
+
textAlign: "center"
|
|
1524
1822
|
}
|
|
1525
1823
|
},
|
|
1526
1824
|
import_react3.default.createElement(
|
|
1527
1825
|
"p",
|
|
1528
|
-
{ style: { color: "#dc2626", marginBottom: "
|
|
1826
|
+
{ style: { color: "#dc2626", marginBottom: "1.5rem" } },
|
|
1529
1827
|
error
|
|
1530
1828
|
),
|
|
1531
1829
|
import_react3.default.createElement(
|
|
1532
|
-
"
|
|
1533
|
-
{
|
|
1534
|
-
|
|
1830
|
+
"div",
|
|
1831
|
+
{ style: { display: "flex", gap: "0.75rem" } },
|
|
1832
|
+
import_react3.default.createElement(
|
|
1833
|
+
"a",
|
|
1834
|
+
{
|
|
1835
|
+
href: "/",
|
|
1836
|
+
style: {
|
|
1837
|
+
display: "inline-block",
|
|
1838
|
+
padding: "0.6rem 1.4rem",
|
|
1839
|
+
background: "#2c2c2c",
|
|
1840
|
+
color: "#fff",
|
|
1841
|
+
fontSize: "0.8125rem",
|
|
1842
|
+
textDecoration: "none"
|
|
1843
|
+
}
|
|
1844
|
+
},
|
|
1845
|
+
"\u91CD\u65B0\u767B\u5F55"
|
|
1846
|
+
),
|
|
1847
|
+
import_react3.default.createElement(
|
|
1848
|
+
"button",
|
|
1849
|
+
{
|
|
1850
|
+
type: "button",
|
|
1851
|
+
onClick: () => {
|
|
1852
|
+
if (window.history.length > 1) window.history.back();
|
|
1853
|
+
else window.location.href = "/";
|
|
1854
|
+
},
|
|
1855
|
+
style: {
|
|
1856
|
+
display: "inline-block",
|
|
1857
|
+
padding: "0.6rem 1.4rem",
|
|
1858
|
+
border: "1px solid rgba(44, 44, 44, 0.25)",
|
|
1859
|
+
background: "transparent",
|
|
1860
|
+
color: "#2c2c2c",
|
|
1861
|
+
fontSize: "0.8125rem",
|
|
1862
|
+
cursor: "pointer"
|
|
1863
|
+
}
|
|
1864
|
+
},
|
|
1865
|
+
"\u8FD4\u56DE\u4E0A\u4E00\u9875"
|
|
1866
|
+
)
|
|
1535
1867
|
)
|
|
1536
1868
|
);
|
|
1537
1869
|
}
|
|
@@ -1546,9 +1878,13 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1546
1878
|
onSuccessRef.current = onSuccess;
|
|
1547
1879
|
onErrorRef.current = onError;
|
|
1548
1880
|
});
|
|
1881
|
+
const callbackPromiseRef = (0, import_react3.useRef)(null);
|
|
1882
|
+
const postProcessedRef = (0, import_react3.useRef)(false);
|
|
1883
|
+
const errorHandledRef = (0, import_react3.useRef)(false);
|
|
1549
1884
|
(0, import_react3.useEffect)(() => {
|
|
1550
|
-
|
|
1551
|
-
|
|
1885
|
+
const popupNonce = new URL(window.location.href).searchParams.get("popup_nonce");
|
|
1886
|
+
if (window.opener && !window.opener.closed && popupNonce) {
|
|
1887
|
+
const nonce = popupNonce;
|
|
1552
1888
|
let targetOrigin = window.location.origin;
|
|
1553
1889
|
try {
|
|
1554
1890
|
targetOrigin = window.opener.location.origin;
|
|
@@ -1557,7 +1893,7 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1557
1893
|
const message = {
|
|
1558
1894
|
type: "nihplod_sso_popup_callback",
|
|
1559
1895
|
callbackUrl: window.location.href,
|
|
1560
|
-
nonce
|
|
1896
|
+
nonce
|
|
1561
1897
|
};
|
|
1562
1898
|
let acked = false;
|
|
1563
1899
|
const send = () => {
|
|
@@ -1572,9 +1908,10 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1572
1908
|
setAwaitingManualClose(true);
|
|
1573
1909
|
}, 1e4);
|
|
1574
1910
|
const handleAck = (event) => {
|
|
1911
|
+
if (event.source !== window.opener) return;
|
|
1575
1912
|
if (event.origin !== targetOrigin) return;
|
|
1576
1913
|
if (!event.data || event.data.type !== "nihplod_sso_popup_ack") return;
|
|
1577
|
-
if (
|
|
1914
|
+
if (event.data.nonce !== nonce) return;
|
|
1578
1915
|
acked = true;
|
|
1579
1916
|
clearInterval(resendTimer);
|
|
1580
1917
|
clearTimeout(ackTimeout);
|
|
@@ -1590,8 +1927,12 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1590
1927
|
let cancelled = false;
|
|
1591
1928
|
async function handleCallback() {
|
|
1592
1929
|
try {
|
|
1593
|
-
|
|
1594
|
-
|
|
1930
|
+
if (!callbackPromiseRef.current) {
|
|
1931
|
+
callbackPromiseRef.current = client.handleCallback(window.location.href);
|
|
1932
|
+
}
|
|
1933
|
+
const tokenData = await callbackPromiseRef.current;
|
|
1934
|
+
if (cancelled || postProcessedRef.current) return;
|
|
1935
|
+
postProcessedRef.current = true;
|
|
1595
1936
|
if (tokenData === null) {
|
|
1596
1937
|
const clientId2 = client.config.clientId;
|
|
1597
1938
|
const probeReturnUrl = getReturnUrl(clientId2);
|
|
@@ -1612,14 +1953,12 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1612
1953
|
}
|
|
1613
1954
|
window.location.href = returnUrl && isTrustedReturnUrl(returnUrl, window.location.origin) ? returnUrl : "/";
|
|
1614
1955
|
} catch (err) {
|
|
1615
|
-
if (cancelled) return;
|
|
1956
|
+
if (cancelled || errorHandledRef.current) return;
|
|
1957
|
+
errorHandledRef.current = true;
|
|
1958
|
+
callbackPromiseRef.current = null;
|
|
1616
1959
|
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
1617
1960
|
onErrorRef.current?.(errorObj);
|
|
1618
|
-
|
|
1619
|
-
setError(errorObj.description || `SSO \u9519\u8BEF (${errorObj.code})`);
|
|
1620
|
-
} else {
|
|
1621
|
-
setError(`\u767B\u5F55\u56DE\u8C03\u5904\u7406\u5931\u8D25: ${errorObj.message}`);
|
|
1622
|
-
}
|
|
1961
|
+
setError(getCallbackErrorMessage(errorObj));
|
|
1623
1962
|
setProcessing(false);
|
|
1624
1963
|
}
|
|
1625
1964
|
}
|