@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.mjs
CHANGED
|
@@ -101,6 +101,9 @@ function createMemoryStorageAdapter() {
|
|
|
101
101
|
},
|
|
102
102
|
remove(key) {
|
|
103
103
|
store.delete(key);
|
|
104
|
+
},
|
|
105
|
+
keys() {
|
|
106
|
+
return [...store.keys()];
|
|
104
107
|
}
|
|
105
108
|
};
|
|
106
109
|
}
|
|
@@ -109,26 +112,44 @@ function createSessionStorageAdapter() {
|
|
|
109
112
|
const fallback = /* @__PURE__ */ new Map();
|
|
110
113
|
return {
|
|
111
114
|
get(key) {
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
try {
|
|
116
|
+
if (typeof sessionStorage !== "undefined") {
|
|
117
|
+
return sessionStorage.getItem(STORAGE_PREFIX + key) ?? fallback.get(key) ?? null;
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
114
120
|
}
|
|
115
121
|
return fallback.get(key) ?? null;
|
|
116
122
|
},
|
|
117
123
|
set(key, value) {
|
|
118
|
-
if (typeof sessionStorage === "undefined") {
|
|
119
|
-
fallback.set(key, value);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
124
|
try {
|
|
123
|
-
|
|
125
|
+
if (typeof sessionStorage !== "undefined") {
|
|
126
|
+
sessionStorage.setItem(STORAGE_PREFIX + key, value);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
124
129
|
} catch {
|
|
125
|
-
fallback.set(key, value);
|
|
126
130
|
}
|
|
131
|
+
fallback.set(key, value);
|
|
127
132
|
},
|
|
128
133
|
remove(key) {
|
|
129
134
|
fallback.delete(key);
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
try {
|
|
136
|
+
if (typeof sessionStorage === "undefined") return;
|
|
137
|
+
sessionStorage.removeItem(STORAGE_PREFIX + key);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
keys() {
|
|
142
|
+
const result = new Set(fallback.keys());
|
|
143
|
+
try {
|
|
144
|
+
if (typeof sessionStorage !== "undefined") {
|
|
145
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
146
|
+
const key = sessionStorage.key(i);
|
|
147
|
+
if (key?.startsWith(STORAGE_PREFIX)) result.add(key.slice(STORAGE_PREFIX.length));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
return [...result];
|
|
132
153
|
}
|
|
133
154
|
};
|
|
134
155
|
}
|
|
@@ -221,32 +242,58 @@ function clearAllSsoData(clientId) {
|
|
|
221
242
|
removeReturnUrl();
|
|
222
243
|
removeLogoutState();
|
|
223
244
|
removeSilentProbe();
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
typeof sessionStorage !== "undefined"
|
|
227
|
-
|
|
228
|
-
|
|
245
|
+
const stores = [];
|
|
246
|
+
try {
|
|
247
|
+
if (typeof sessionStorage !== "undefined") stores.push(sessionStorage);
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
if (typeof localStorage !== "undefined") stores.push(localStorage);
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
const abstractKeys = /* @__PURE__ */ new Set();
|
|
229
255
|
for (const store of stores) {
|
|
230
|
-
if (!store) continue;
|
|
231
256
|
const keys = [];
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
257
|
+
try {
|
|
258
|
+
for (let i = 0; i < store.length; i++) {
|
|
259
|
+
const key = store.key(i);
|
|
260
|
+
if (key?.startsWith(STORAGE_PREFIX)) keys.push(key);
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
continue;
|
|
235
264
|
}
|
|
236
265
|
for (const key of keys) {
|
|
237
|
-
|
|
238
|
-
|
|
266
|
+
abstractKeys.add(key.slice(STORAGE_PREFIX.length));
|
|
267
|
+
try {
|
|
268
|
+
store.removeItem(key);
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const store of [_transient, _storage]) {
|
|
274
|
+
try {
|
|
275
|
+
for (const key of store.keys?.() ?? []) abstractKeys.add(key);
|
|
276
|
+
} catch {
|
|
239
277
|
}
|
|
240
278
|
}
|
|
279
|
+
for (const key of abstractKeys) {
|
|
280
|
+
_transient.remove(key);
|
|
281
|
+
_storage.remove(key);
|
|
282
|
+
}
|
|
241
283
|
}
|
|
242
284
|
|
|
243
285
|
// src/core/security.ts
|
|
286
|
+
var UNSAFE_URL_CHAR_PATTERN = /[\\\u0000-\u001F\u007F]/;
|
|
244
287
|
function isTrustedReturnUrl(url, currentOrigin) {
|
|
245
288
|
if (!url) return false;
|
|
246
|
-
if (
|
|
247
|
-
if (url.startsWith("
|
|
289
|
+
if (UNSAFE_URL_CHAR_PATTERN.test(url)) return false;
|
|
290
|
+
if (url.startsWith("//")) return false;
|
|
291
|
+
if (url.startsWith("/")) return true;
|
|
248
292
|
try {
|
|
249
|
-
|
|
293
|
+
const parsed = new URL(url);
|
|
294
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
295
|
+
if (parsed.username || parsed.password) return false;
|
|
296
|
+
return parsed.origin === currentOrigin;
|
|
250
297
|
} catch {
|
|
251
298
|
return false;
|
|
252
299
|
}
|
|
@@ -458,7 +505,10 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
458
505
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
459
506
|
throw new SsoError("id_token_expired", "ID Token \u5DF2\u8FC7\u671F");
|
|
460
507
|
}
|
|
461
|
-
if (typeof payload.iat
|
|
508
|
+
if (typeof payload.iat !== "number") {
|
|
509
|
+
throw new SsoError("id_token_invalid", "ID Token \u7F3A\u5C11 iat \u58F0\u660E");
|
|
510
|
+
}
|
|
511
|
+
if (payload.iat * 1e3 > Date.now() + 6e4) {
|
|
462
512
|
throw new SsoError("id_token_invalid", "ID Token iat \u5728\u672A\u6765\uFF0C\u7591\u4F3C\u4F2A\u9020\u6216\u65F6\u949F\u5F02\u5E38");
|
|
463
513
|
}
|
|
464
514
|
if (typeof payload.sub !== "string" || !payload.sub) {
|
|
@@ -475,11 +525,47 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
475
525
|
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
476
526
|
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
477
527
|
}
|
|
528
|
+
} else if (typeof payload.nonce === "string" && payload.nonce) {
|
|
529
|
+
throw new SsoError(
|
|
530
|
+
"id_token_nonce_mismatch",
|
|
531
|
+
"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"
|
|
532
|
+
);
|
|
478
533
|
}
|
|
479
534
|
return { sub: payload.sub };
|
|
480
535
|
}
|
|
481
536
|
|
|
537
|
+
// src/core/http.ts
|
|
538
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
539
|
+
async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
540
|
+
const controller = new AbortController();
|
|
541
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
542
|
+
try {
|
|
543
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
544
|
+
} finally {
|
|
545
|
+
clearTimeout(timer);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
482
549
|
// src/core/SsoClient.ts
|
|
550
|
+
var REVOKE_TIMEOUT_MS = 3e3;
|
|
551
|
+
var EXPIRY_SKEW_MS = 1e4;
|
|
552
|
+
function assertValidTokenResponse(data, requireRefreshToken) {
|
|
553
|
+
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
554
|
+
throw new SsoError("token_request_failed", "Token \u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
555
|
+
}
|
|
556
|
+
if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
|
|
557
|
+
throw new SsoError(
|
|
558
|
+
"token_request_failed",
|
|
559
|
+
`Token \u54CD\u5E94 expires_in \u975E\u6CD5: ${String(data.expires_in)}`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
if (requireRefreshToken && (typeof data.refresh_token !== "string" || !data.refresh_token)) {
|
|
563
|
+
throw new SsoError(
|
|
564
|
+
"token_request_failed",
|
|
565
|
+
"Token \u54CD\u5E94\u7F3A\u5C11 refresh_token\uFF08authorization_code \u4EA4\u6362\u5FC5\u987B\u8FD4\u56DE\uFF09"
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
483
569
|
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
|
|
484
570
|
"login_required",
|
|
485
571
|
"consent_required",
|
|
@@ -490,6 +576,8 @@ var _SsoClient = class _SsoClient {
|
|
|
490
576
|
this._discovery = null;
|
|
491
577
|
this._discoveryFetchedAt = 0;
|
|
492
578
|
this._refreshLock = null;
|
|
579
|
+
/** Discovery 端点 origin 校验失败只告警一次(拒绝的文档不缓存,避免每次调用刷屏) */
|
|
580
|
+
this._discoveryOriginWarned = false;
|
|
493
581
|
if (!config.clientId) throw new SsoError("invalid_config", "clientId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
494
582
|
if (!config.redirectUri) throw new SsoError("invalid_config", "redirectUri \u4E0D\u80FD\u4E3A\u7A7A");
|
|
495
583
|
if (!config.ssoBaseUrl) throw new SsoError("invalid_config", "ssoBaseUrl \u4E0D\u80FD\u4E3A\u7A7A");
|
|
@@ -498,8 +586,67 @@ var _SsoClient = class _SsoClient {
|
|
|
498
586
|
this._serverBase = (config.serverBaseUrl ?? base).replace(/\/+$/, "");
|
|
499
587
|
}
|
|
500
588
|
// ============================================
|
|
589
|
+
// 调试日志
|
|
590
|
+
// ============================================
|
|
591
|
+
/**
|
|
592
|
+
* 调试日志(config.debug=true 时输出,前缀 [SSO SDK]):
|
|
593
|
+
* 覆盖登录发起 / 回调结果 / 刷新失败 / 登出等关键节点,便于接入方定位问题。
|
|
594
|
+
* 不会输出 token/授权码等敏感值。
|
|
595
|
+
*/
|
|
596
|
+
debugLog(...args) {
|
|
597
|
+
if (this.config.debug) {
|
|
598
|
+
console.warn("[SSO SDK]", ...args);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// ============================================
|
|
501
602
|
// 内部方法
|
|
502
603
|
// ============================================
|
|
604
|
+
/**
|
|
605
|
+
* 校验 Discovery 文档端点 origin(防文档被篡改后 token/code 被导向攻击者服务器):
|
|
606
|
+
* - authorization_endpoint 必须与 ssoBaseUrl 同源(浏览器跳转目标);
|
|
607
|
+
* - token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl 同源,
|
|
608
|
+
* 或(配置了 serverBaseUrl 时)与内网地址同源。
|
|
609
|
+
* 任一不匹配:告警并返回 null(上层回退到默认端点,fail-safe)。
|
|
610
|
+
* 可通过 config.allowCrossOriginDiscoveryEndpoints 关闭(见配置项警告)。
|
|
611
|
+
*/
|
|
612
|
+
_sanitizeDiscovery(d) {
|
|
613
|
+
if (this.config.allowCrossOriginDiscoveryEndpoints) return d;
|
|
614
|
+
const allowedOrigins = /* @__PURE__ */ new Set();
|
|
615
|
+
try {
|
|
616
|
+
allowedOrigins.add(new URL(this.config.ssoBaseUrl).origin);
|
|
617
|
+
allowedOrigins.add(new URL(this._serverBase).origin);
|
|
618
|
+
} catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
const sameOrigin = (endpoint) => {
|
|
622
|
+
try {
|
|
623
|
+
return allowedOrigins.has(new URL(endpoint).origin);
|
|
624
|
+
} catch {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
const publicOrigin = new URL(this.config.ssoBaseUrl).origin;
|
|
629
|
+
let authorizationOk = false;
|
|
630
|
+
try {
|
|
631
|
+
authorizationOk = new URL(d.authorization_endpoint).origin === publicOrigin;
|
|
632
|
+
} catch {
|
|
633
|
+
authorizationOk = false;
|
|
634
|
+
}
|
|
635
|
+
const bad = [];
|
|
636
|
+
if (!authorizationOk) bad.push("authorization_endpoint");
|
|
637
|
+
if (!sameOrigin(d.token_endpoint)) bad.push("token_endpoint");
|
|
638
|
+
if (!sameOrigin(d.userinfo_endpoint)) bad.push("userinfo_endpoint");
|
|
639
|
+
if (bad.length > 0) {
|
|
640
|
+
if (!this._discoveryOriginWarned) {
|
|
641
|
+
this._discoveryOriginWarned = true;
|
|
642
|
+
console.warn(
|
|
643
|
+
`[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`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
return d;
|
|
649
|
+
}
|
|
503
650
|
/**
|
|
504
651
|
* 获取 OIDC Discovery 文档(带缓存 + 超时)
|
|
505
652
|
*
|
|
@@ -526,7 +673,10 @@ var _SsoClient = class _SsoClient {
|
|
|
526
673
|
);
|
|
527
674
|
return null;
|
|
528
675
|
}
|
|
529
|
-
|
|
676
|
+
const doc = await res.json();
|
|
677
|
+
const sanitized = this._sanitizeDiscovery(doc);
|
|
678
|
+
if (!sanitized) return null;
|
|
679
|
+
this._discovery = sanitized;
|
|
530
680
|
this._discoveryFetchedAt = now;
|
|
531
681
|
return this._discovery;
|
|
532
682
|
} catch (err) {
|
|
@@ -877,7 +1027,7 @@ var _SsoClient = class _SsoClient {
|
|
|
877
1027
|
}
|
|
878
1028
|
let res;
|
|
879
1029
|
try {
|
|
880
|
-
res = await
|
|
1030
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
881
1031
|
method: "POST",
|
|
882
1032
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
883
1033
|
body: body.toString()
|
|
@@ -886,6 +1036,10 @@ var _SsoClient = class _SsoClient {
|
|
|
886
1036
|
throw new SsoError("network_error", "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", err);
|
|
887
1037
|
}
|
|
888
1038
|
if (!res.ok) {
|
|
1039
|
+
removeOAuthState(this.config.clientId);
|
|
1040
|
+
removeOAuthNonce(this.config.clientId);
|
|
1041
|
+
removePkceVerifier(this.config.clientId);
|
|
1042
|
+
removeSilentProbe(this.config.clientId);
|
|
889
1043
|
let errData = {};
|
|
890
1044
|
try {
|
|
891
1045
|
errData = await res.json();
|
|
@@ -898,6 +1052,19 @@ var _SsoClient = class _SsoClient {
|
|
|
898
1052
|
);
|
|
899
1053
|
}
|
|
900
1054
|
const data = await res.json();
|
|
1055
|
+
assertValidTokenResponse(data, true);
|
|
1056
|
+
const requestedScopes = (this.config.scopes || "openid profile").split(" ").filter(Boolean);
|
|
1057
|
+
if (requestedScopes.includes("openid") && !data.id_token) {
|
|
1058
|
+
removeOAuthState(this.config.clientId);
|
|
1059
|
+
removeOAuthNonce(this.config.clientId);
|
|
1060
|
+
removePkceVerifier(this.config.clientId);
|
|
1061
|
+
removeSilentProbe(this.config.clientId);
|
|
1062
|
+
removeTokenData(this.config.clientId);
|
|
1063
|
+
throw new SsoError(
|
|
1064
|
+
"id_token_invalid",
|
|
1065
|
+
"Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09"
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
901
1068
|
if (data.id_token) {
|
|
902
1069
|
try {
|
|
903
1070
|
await validateIdToken(
|
|
@@ -927,6 +1094,7 @@ var _SsoClient = class _SsoClient {
|
|
|
927
1094
|
expires_at: now + data.expires_in * 1e3
|
|
928
1095
|
};
|
|
929
1096
|
saveTokenData(tokenData, this.config.clientId);
|
|
1097
|
+
this.debugLog("handleCallback: token \u4EA4\u6362\u6210\u529F", { clientId: this.config.clientId });
|
|
930
1098
|
return tokenData;
|
|
931
1099
|
}
|
|
932
1100
|
/**
|
|
@@ -962,7 +1130,7 @@ var _SsoClient = class _SsoClient {
|
|
|
962
1130
|
let lastErr;
|
|
963
1131
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
964
1132
|
try {
|
|
965
|
-
res = await
|
|
1133
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
966
1134
|
method: "POST",
|
|
967
1135
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
968
1136
|
body: body.toString()
|
|
@@ -984,15 +1152,20 @@ var _SsoClient = class _SsoClient {
|
|
|
984
1152
|
} catch {
|
|
985
1153
|
}
|
|
986
1154
|
const errorCode = errData.error || "";
|
|
987
|
-
if (errorCode === "invalid_grant"
|
|
1155
|
+
if (errorCode === "invalid_grant") {
|
|
988
1156
|
removeTokenData(this.config.clientId);
|
|
989
1157
|
}
|
|
1158
|
+
this.debugLog("refreshToken: \u5237\u65B0\u5931\u8D25", { status: res.status, error: errorCode });
|
|
1159
|
+
if (!errorCode && res.status === 401) {
|
|
1160
|
+
throw new SsoError("sso_server_error", "\u5237\u65B0 Token \u5931\u8D25\uFF08\u7F51\u5173\u5F02\u5E38\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
1161
|
+
}
|
|
990
1162
|
throw new SsoError(
|
|
991
1163
|
mapOAuthErrorToSsoCode(errorCode, "refresh"),
|
|
992
1164
|
errData.error_description || `\u5237\u65B0 Token \u5931\u8D25: HTTP ${res.status}`
|
|
993
1165
|
);
|
|
994
1166
|
}
|
|
995
1167
|
const data = await res.json();
|
|
1168
|
+
assertValidTokenResponse(data, false);
|
|
996
1169
|
if (data.id_token) {
|
|
997
1170
|
try {
|
|
998
1171
|
await validateIdToken(
|
|
@@ -1030,13 +1203,13 @@ var _SsoClient = class _SsoClient {
|
|
|
1030
1203
|
if (!tokenData) {
|
|
1031
1204
|
throw new SsoError("not_authenticated", "\u672A\u767B\u5F55");
|
|
1032
1205
|
}
|
|
1033
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1206
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1034
1207
|
tokenData = await this.refreshToken();
|
|
1035
1208
|
}
|
|
1036
1209
|
const userinfoEndpoint = await this._getUserinfoEndpoint();
|
|
1037
1210
|
let res;
|
|
1038
1211
|
try {
|
|
1039
|
-
res = await
|
|
1212
|
+
res = await fetchWithTimeout(userinfoEndpoint, {
|
|
1040
1213
|
headers: {
|
|
1041
1214
|
Authorization: `Bearer ${tokenData.access_token}`
|
|
1042
1215
|
}
|
|
@@ -1065,7 +1238,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1065
1238
|
async getAccessToken() {
|
|
1066
1239
|
let tokenData = getTokenData(this.config.clientId);
|
|
1067
1240
|
if (!tokenData) return null;
|
|
1068
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1241
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1069
1242
|
tokenData = await this.refreshToken();
|
|
1070
1243
|
}
|
|
1071
1244
|
return tokenData.access_token;
|
|
@@ -1074,6 +1247,9 @@ var _SsoClient = class _SsoClient {
|
|
|
1074
1247
|
* 检查是否已认证(不发起网络请求)
|
|
1075
1248
|
*
|
|
1076
1249
|
* 仅检查本地是否存在未过期的 access_token。
|
|
1250
|
+
* 故意不做 EXPIRY_SKEW_MS 提前量:这是同步的 UI 提示判定(是否显示"已登录"),
|
|
1251
|
+
* 提前 10s 翻转 false 会让调用方在 token 仍有效时误显示未登录;真正发请求的
|
|
1252
|
+
* getAccessToken/getUserInfo 已带提前量。
|
|
1077
1253
|
*/
|
|
1078
1254
|
isAuthenticated() {
|
|
1079
1255
|
const tokenData = getTokenData(this.config.clientId);
|
|
@@ -1093,6 +1269,10 @@ var _SsoClient = class _SsoClient {
|
|
|
1093
1269
|
const refreshToken = tokenData?.refresh_token;
|
|
1094
1270
|
const idTokenHint = tokenData?.id_token;
|
|
1095
1271
|
clearAllSsoData(this.config.clientId);
|
|
1272
|
+
this.debugLog("logout: \u672C\u5730\u6570\u636E\u5DF2\u6E05\u9664", {
|
|
1273
|
+
clientId: this.config.clientId,
|
|
1274
|
+
redirectToSso
|
|
1275
|
+
});
|
|
1096
1276
|
if (refreshToken && this.config.clientId) {
|
|
1097
1277
|
try {
|
|
1098
1278
|
const discovery = this.config.serverBaseUrl ? null : await this._getDiscovery();
|
|
@@ -1105,11 +1285,15 @@ var _SsoClient = class _SsoClient {
|
|
|
1105
1285
|
if (this.config.clientSecret) {
|
|
1106
1286
|
revokeBody.set("client_secret", this.config.clientSecret);
|
|
1107
1287
|
}
|
|
1108
|
-
await
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1288
|
+
await fetchWithTimeout(
|
|
1289
|
+
revokeUrl,
|
|
1290
|
+
{
|
|
1291
|
+
method: "POST",
|
|
1292
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1293
|
+
body: revokeBody.toString()
|
|
1294
|
+
},
|
|
1295
|
+
REVOKE_TIMEOUT_MS
|
|
1296
|
+
);
|
|
1113
1297
|
} catch {
|
|
1114
1298
|
}
|
|
1115
1299
|
}
|
|
@@ -1179,6 +1363,21 @@ var SsoClient = _SsoClient;
|
|
|
1179
1363
|
var SsoContext = createContext(null);
|
|
1180
1364
|
var REFRESH_LOCK_PREFIX = "nihplod_sso_refresh_lock:";
|
|
1181
1365
|
var LOCK_TTL_MS = 5e3;
|
|
1366
|
+
var CHANNEL_PREFIX = "nihplod_sso_events:";
|
|
1367
|
+
var TAB_ID = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
1368
|
+
function broadcastSsoEvent(clientId, type, tokenData) {
|
|
1369
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
1370
|
+
try {
|
|
1371
|
+
const channel = new BroadcastChannel(CHANNEL_PREFIX + clientId);
|
|
1372
|
+
channel.postMessage({
|
|
1373
|
+
type,
|
|
1374
|
+
sourceTabId: TAB_ID,
|
|
1375
|
+
...type === "token" && tokenData ? { tokenData } : {}
|
|
1376
|
+
});
|
|
1377
|
+
channel.close();
|
|
1378
|
+
} catch {
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1182
1381
|
var ownedLocks = /* @__PURE__ */ new Map();
|
|
1183
1382
|
function lockKey(clientId) {
|
|
1184
1383
|
return REFRESH_LOCK_PREFIX + clientId;
|
|
@@ -1237,21 +1436,37 @@ function SsoProvider({
|
|
|
1237
1436
|
config,
|
|
1238
1437
|
children,
|
|
1239
1438
|
refreshThreshold = 60,
|
|
1240
|
-
onTokenRefreshed
|
|
1439
|
+
onTokenRefreshed,
|
|
1440
|
+
onSessionExpired
|
|
1241
1441
|
}) {
|
|
1242
1442
|
const [user, setUser] = useState(null);
|
|
1243
1443
|
const [isLoading, setIsLoading] = useState(true);
|
|
1244
1444
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
1245
1445
|
const [error, setError] = useState(null);
|
|
1446
|
+
const [sessionExpired, setSessionExpired] = useState(false);
|
|
1246
1447
|
const [client] = useState(() => new SsoClient(config));
|
|
1247
1448
|
const refreshTimerRef = useRef(null);
|
|
1248
1449
|
const loadedRef = useRef(false);
|
|
1450
|
+
const onSessionExpiredRef = useRef(onSessionExpired);
|
|
1451
|
+
useEffect(() => {
|
|
1452
|
+
onSessionExpiredRef.current = onSessionExpired;
|
|
1453
|
+
});
|
|
1454
|
+
const sessionExpiredNotifiedRef = useRef(false);
|
|
1455
|
+
const refreshFailureCountRef = useRef(0);
|
|
1456
|
+
const MAX_REFRESH_FAILURES = 3;
|
|
1457
|
+
const markSessionExpired = useCallback((error2) => {
|
|
1458
|
+
setError(error2);
|
|
1459
|
+
setSessionExpired(true);
|
|
1460
|
+
if (!sessionExpiredNotifiedRef.current) {
|
|
1461
|
+
sessionExpiredNotifiedRef.current = true;
|
|
1462
|
+
onSessionExpiredRef.current?.(error2);
|
|
1463
|
+
}
|
|
1464
|
+
}, []);
|
|
1249
1465
|
const loadUser = useCallback(async () => {
|
|
1250
1466
|
const tokenData = getTokenData(client.config.clientId);
|
|
1251
1467
|
if (!tokenData) {
|
|
1252
1468
|
setUser(null);
|
|
1253
1469
|
setIsAuthenticated(false);
|
|
1254
|
-
setError(null);
|
|
1255
1470
|
setIsLoading(false);
|
|
1256
1471
|
return;
|
|
1257
1472
|
}
|
|
@@ -1260,19 +1475,22 @@ function SsoProvider({
|
|
|
1260
1475
|
setUser(u);
|
|
1261
1476
|
setIsAuthenticated(true);
|
|
1262
1477
|
setError(null);
|
|
1478
|
+
setSessionExpired(false);
|
|
1479
|
+
sessionExpiredNotifiedRef.current = false;
|
|
1480
|
+
refreshFailureCountRef.current = 0;
|
|
1263
1481
|
} catch (err) {
|
|
1264
1482
|
setUser(null);
|
|
1265
1483
|
setIsAuthenticated(false);
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
)
|
|
1269
|
-
if (err instanceof SsoError && (err.code === "not_authenticated" || err.code === "session_expired")) {
|
|
1484
|
+
const errorObj = err instanceof SsoError ? err : new SsoError("userinfo_failed", err instanceof Error ? err.message : String(err));
|
|
1485
|
+
setError(errorObj);
|
|
1486
|
+
if (errorObj.code === "not_authenticated" || errorObj.code === "session_expired") {
|
|
1270
1487
|
removeTokenData(client.config.clientId);
|
|
1488
|
+
markSessionExpired(errorObj);
|
|
1271
1489
|
}
|
|
1272
1490
|
} finally {
|
|
1273
1491
|
setIsLoading(false);
|
|
1274
1492
|
}
|
|
1275
|
-
}, [client]);
|
|
1493
|
+
}, [client, markSessionExpired]);
|
|
1276
1494
|
useEffect(() => {
|
|
1277
1495
|
if (loadedRef.current) return;
|
|
1278
1496
|
loadedRef.current = true;
|
|
@@ -1289,13 +1507,28 @@ function SsoProvider({
|
|
|
1289
1507
|
const attemptRefresh = () => withRefreshLock(client.config.clientId, async () => {
|
|
1290
1508
|
try {
|
|
1291
1509
|
const td = await client.refreshToken();
|
|
1510
|
+
refreshFailureCountRef.current = 0;
|
|
1292
1511
|
onTokenRefreshed?.(td.access_token);
|
|
1512
|
+
broadcastSsoEvent(client.config.clientId, "token", td);
|
|
1293
1513
|
loadUser();
|
|
1294
|
-
} catch {
|
|
1514
|
+
} catch (err) {
|
|
1515
|
+
if (err instanceof SsoError && (err.code === "session_expired" || err.code === "no_refresh_token" || err.code === "not_authenticated")) {
|
|
1516
|
+
removeTokenData(client.config.clientId);
|
|
1517
|
+
markSessionExpired(err);
|
|
1518
|
+
} else {
|
|
1519
|
+
refreshFailureCountRef.current += 1;
|
|
1520
|
+
if (refreshFailureCountRef.current >= MAX_REFRESH_FAILURES) {
|
|
1521
|
+
markSessionExpired(
|
|
1522
|
+
err instanceof SsoError ? err : new SsoError("session_expired", "\u591A\u6B21\u5237\u65B0\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55")
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1295
1526
|
setTimeout(() => loadUser(), 500);
|
|
1296
1527
|
}
|
|
1528
|
+
}).finally(() => {
|
|
1529
|
+
if (active) scheduleNextRefresh(5e3);
|
|
1297
1530
|
});
|
|
1298
|
-
const scheduleNextRefresh = () => {
|
|
1531
|
+
const scheduleNextRefresh = (minDelayMs = 0) => {
|
|
1299
1532
|
if (!active) return;
|
|
1300
1533
|
if (refreshTimerRef.current) {
|
|
1301
1534
|
clearTimeout(refreshTimerRef.current);
|
|
@@ -1304,26 +1537,27 @@ function SsoProvider({
|
|
|
1304
1537
|
const tokenData = getTokenData(client.config.clientId);
|
|
1305
1538
|
if (!tokenData) return;
|
|
1306
1539
|
const remainingSec = (tokenData.expires_at - Date.now()) / 1e3;
|
|
1307
|
-
if (remainingSec <= 0) {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1540
|
+
if (remainingSec <= 0 || remainingSec <= refreshThreshold) {
|
|
1541
|
+
refreshTimerRef.current = setTimeout(
|
|
1542
|
+
() => {
|
|
1543
|
+
if (active) void attemptRefresh();
|
|
1544
|
+
},
|
|
1545
|
+
Math.max(minDelayMs, 1e3)
|
|
1546
|
+
);
|
|
1313
1547
|
return;
|
|
1314
1548
|
}
|
|
1315
|
-
const delayMs = (remainingSec - refreshThreshold) * 1e3;
|
|
1549
|
+
const delayMs = Math.max((remainingSec - refreshThreshold) * 1e3, minDelayMs, 1e3);
|
|
1316
1550
|
refreshTimerRef.current = setTimeout(() => {
|
|
1317
1551
|
if (!active) return;
|
|
1318
1552
|
const td = getTokenData(client.config.clientId);
|
|
1319
1553
|
if (!td) return;
|
|
1320
1554
|
const secLeft = (td.expires_at - Date.now()) / 1e3;
|
|
1321
1555
|
if (secLeft <= refreshThreshold) {
|
|
1322
|
-
void attemptRefresh()
|
|
1323
|
-
|
|
1324
|
-
|
|
1556
|
+
void attemptRefresh();
|
|
1557
|
+
} else {
|
|
1558
|
+
scheduleNextRefresh();
|
|
1325
1559
|
}
|
|
1326
|
-
},
|
|
1560
|
+
}, delayMs);
|
|
1327
1561
|
};
|
|
1328
1562
|
scheduleNextRefresh();
|
|
1329
1563
|
return () => {
|
|
@@ -1343,6 +1577,30 @@ function SsoProvider({
|
|
|
1343
1577
|
window.addEventListener("storage", handleStorageChange);
|
|
1344
1578
|
return () => window.removeEventListener("storage", handleStorageChange);
|
|
1345
1579
|
}, [loadUser]);
|
|
1580
|
+
useEffect(() => {
|
|
1581
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
1582
|
+
const channel = new BroadcastChannel(CHANNEL_PREFIX + client.config.clientId);
|
|
1583
|
+
channel.onmessage = (event) => {
|
|
1584
|
+
const data = event.data;
|
|
1585
|
+
if (!data || data.sourceTabId === TAB_ID) return;
|
|
1586
|
+
if (data.type === "logout") {
|
|
1587
|
+
removeTokenData(client.config.clientId);
|
|
1588
|
+
setUser(null);
|
|
1589
|
+
setIsAuthenticated(false);
|
|
1590
|
+
setError(null);
|
|
1591
|
+
setSessionExpired(false);
|
|
1592
|
+
sessionExpiredNotifiedRef.current = false;
|
|
1593
|
+
refreshFailureCountRef.current = 0;
|
|
1594
|
+
} else if (data.type === "token") {
|
|
1595
|
+
const td = data.tokenData;
|
|
1596
|
+
if (td && typeof td.access_token === "string" && typeof td.expires_at === "number" && typeof td.refresh_token === "string") {
|
|
1597
|
+
saveTokenData(td, client.config.clientId);
|
|
1598
|
+
}
|
|
1599
|
+
loadUser();
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
return () => channel.close();
|
|
1603
|
+
}, [client, loadUser]);
|
|
1346
1604
|
const login = useCallback(
|
|
1347
1605
|
async (returnUrl) => {
|
|
1348
1606
|
await client.login(returnUrl);
|
|
@@ -1359,10 +1617,17 @@ function SsoProvider({
|
|
|
1359
1617
|
);
|
|
1360
1618
|
const logout = useCallback(
|
|
1361
1619
|
async (redirectToSso = false) => {
|
|
1362
|
-
await client.logout(redirectToSso);
|
|
1363
1620
|
setUser(null);
|
|
1364
1621
|
setIsAuthenticated(false);
|
|
1365
1622
|
setError(null);
|
|
1623
|
+
setSessionExpired(false);
|
|
1624
|
+
sessionExpiredNotifiedRef.current = false;
|
|
1625
|
+
refreshFailureCountRef.current = 0;
|
|
1626
|
+
broadcastSsoEvent(client.config.clientId, "logout");
|
|
1627
|
+
try {
|
|
1628
|
+
await client.logout(redirectToSso);
|
|
1629
|
+
} catch {
|
|
1630
|
+
}
|
|
1366
1631
|
},
|
|
1367
1632
|
[client]
|
|
1368
1633
|
);
|
|
@@ -1382,6 +1647,7 @@ function SsoProvider({
|
|
|
1382
1647
|
logout,
|
|
1383
1648
|
refreshUser,
|
|
1384
1649
|
getAccessToken,
|
|
1650
|
+
sessionExpired,
|
|
1385
1651
|
client
|
|
1386
1652
|
};
|
|
1387
1653
|
return React.createElement(SsoContext.Provider, { value }, children);
|
|
@@ -1476,6 +1742,36 @@ function withAuth(Component) {
|
|
|
1476
1742
|
|
|
1477
1743
|
// src/react/CallbackPage.tsx
|
|
1478
1744
|
import React3, { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
1745
|
+
var CALLBACK_ERROR_HINTS = {
|
|
1746
|
+
state_mismatch: "\u767B\u5F55\u4F1A\u8BDD\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1747
|
+
pkce_required: "\u767B\u5F55\u4F1A\u8BDD\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1748
|
+
user_denied_authorization: "\u4F60\u5DF2\u53D6\u6D88\u767B\u5F55",
|
|
1749
|
+
session_expired: "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1750
|
+
authorization_code_expired: "\u767B\u5F55\u4FE1\u606F\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1751
|
+
authorization_code_used: "\u767B\u5F55\u4FE1\u606F\u5DF2\u88AB\u4F7F\u7528\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1752
|
+
token_request_failed: "\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1753
|
+
userinfo_failed: "\u767B\u5F55\u4FE1\u606F\u83B7\u53D6\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
|
|
1754
|
+
not_authenticated: "\u8BF7\u5148\u767B\u5F55\u540E\u518D\u8BBF\u95EE",
|
|
1755
|
+
network_error: "\u7F51\u7EDC\u5F02\u5E38\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5",
|
|
1756
|
+
rate_limited: "\u64CD\u4F5C\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
1757
|
+
popup_blocked: "\u767B\u5F55\u7A97\u53E3\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5",
|
|
1758
|
+
popup_closed: "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
|
|
1759
|
+
client_disabled: "\u8BE5\u5E94\u7528\u5DF2\u505C\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
|
|
1760
|
+
account_disabled: "\u8D26\u53F7\u6682\u65F6\u65E0\u6CD5\u4F7F\u7528\uFF0C\u8BF7\u8054\u7CFB\u5BA2\u670D",
|
|
1761
|
+
sso_server_error: "\u767B\u5F55\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
1762
|
+
invalid_config: "\u5E94\u7528\u914D\u7F6E\u6709\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458",
|
|
1763
|
+
no_refresh_token: "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"
|
|
1764
|
+
};
|
|
1765
|
+
function getCallbackErrorMessage(err) {
|
|
1766
|
+
if (err instanceof SsoError) {
|
|
1767
|
+
const hint = CALLBACK_ERROR_HINTS[err.code];
|
|
1768
|
+
if (hint) return hint;
|
|
1769
|
+
if (err.code.startsWith("id_token")) return "\u767B\u5F55\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
|
|
1770
|
+
if (/[\u4e00-\u9fff]/.test(err.description)) return err.description;
|
|
1771
|
+
return "\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
|
|
1772
|
+
}
|
|
1773
|
+
return "\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5";
|
|
1774
|
+
}
|
|
1479
1775
|
function DefaultCallbackError({ error }) {
|
|
1480
1776
|
return React3.createElement(
|
|
1481
1777
|
"div",
|
|
@@ -1486,18 +1782,54 @@ function DefaultCallbackError({ error }) {
|
|
|
1486
1782
|
alignItems: "center",
|
|
1487
1783
|
justifyContent: "center",
|
|
1488
1784
|
minHeight: "100vh",
|
|
1489
|
-
fontFamily: "system-ui, sans-serif"
|
|
1785
|
+
fontFamily: "system-ui, sans-serif",
|
|
1786
|
+
padding: "2rem",
|
|
1787
|
+
textAlign: "center"
|
|
1490
1788
|
}
|
|
1491
1789
|
},
|
|
1492
1790
|
React3.createElement(
|
|
1493
1791
|
"p",
|
|
1494
|
-
{ style: { color: "#dc2626", marginBottom: "
|
|
1792
|
+
{ style: { color: "#dc2626", marginBottom: "1.5rem" } },
|
|
1495
1793
|
error
|
|
1496
1794
|
),
|
|
1497
1795
|
React3.createElement(
|
|
1498
|
-
"
|
|
1499
|
-
{
|
|
1500
|
-
|
|
1796
|
+
"div",
|
|
1797
|
+
{ style: { display: "flex", gap: "0.75rem" } },
|
|
1798
|
+
React3.createElement(
|
|
1799
|
+
"a",
|
|
1800
|
+
{
|
|
1801
|
+
href: "/",
|
|
1802
|
+
style: {
|
|
1803
|
+
display: "inline-block",
|
|
1804
|
+
padding: "0.6rem 1.4rem",
|
|
1805
|
+
background: "#2c2c2c",
|
|
1806
|
+
color: "#fff",
|
|
1807
|
+
fontSize: "0.8125rem",
|
|
1808
|
+
textDecoration: "none"
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
"\u91CD\u65B0\u767B\u5F55"
|
|
1812
|
+
),
|
|
1813
|
+
React3.createElement(
|
|
1814
|
+
"button",
|
|
1815
|
+
{
|
|
1816
|
+
type: "button",
|
|
1817
|
+
onClick: () => {
|
|
1818
|
+
if (window.history.length > 1) window.history.back();
|
|
1819
|
+
else window.location.href = "/";
|
|
1820
|
+
},
|
|
1821
|
+
style: {
|
|
1822
|
+
display: "inline-block",
|
|
1823
|
+
padding: "0.6rem 1.4rem",
|
|
1824
|
+
border: "1px solid rgba(44, 44, 44, 0.25)",
|
|
1825
|
+
background: "transparent",
|
|
1826
|
+
color: "#2c2c2c",
|
|
1827
|
+
fontSize: "0.8125rem",
|
|
1828
|
+
cursor: "pointer"
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
"\u8FD4\u56DE\u4E0A\u4E00\u9875"
|
|
1832
|
+
)
|
|
1501
1833
|
)
|
|
1502
1834
|
);
|
|
1503
1835
|
}
|
|
@@ -1512,9 +1844,13 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1512
1844
|
onSuccessRef.current = onSuccess;
|
|
1513
1845
|
onErrorRef.current = onError;
|
|
1514
1846
|
});
|
|
1847
|
+
const callbackPromiseRef = useRef3(null);
|
|
1848
|
+
const postProcessedRef = useRef3(false);
|
|
1849
|
+
const errorHandledRef = useRef3(false);
|
|
1515
1850
|
useEffect3(() => {
|
|
1516
|
-
|
|
1517
|
-
|
|
1851
|
+
const popupNonce = new URL(window.location.href).searchParams.get("popup_nonce");
|
|
1852
|
+
if (window.opener && !window.opener.closed && popupNonce) {
|
|
1853
|
+
const nonce = popupNonce;
|
|
1518
1854
|
let targetOrigin = window.location.origin;
|
|
1519
1855
|
try {
|
|
1520
1856
|
targetOrigin = window.opener.location.origin;
|
|
@@ -1523,7 +1859,7 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1523
1859
|
const message = {
|
|
1524
1860
|
type: "nihplod_sso_popup_callback",
|
|
1525
1861
|
callbackUrl: window.location.href,
|
|
1526
|
-
nonce
|
|
1862
|
+
nonce
|
|
1527
1863
|
};
|
|
1528
1864
|
let acked = false;
|
|
1529
1865
|
const send = () => {
|
|
@@ -1538,9 +1874,10 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1538
1874
|
setAwaitingManualClose(true);
|
|
1539
1875
|
}, 1e4);
|
|
1540
1876
|
const handleAck = (event) => {
|
|
1877
|
+
if (event.source !== window.opener) return;
|
|
1541
1878
|
if (event.origin !== targetOrigin) return;
|
|
1542
1879
|
if (!event.data || event.data.type !== "nihplod_sso_popup_ack") return;
|
|
1543
|
-
if (
|
|
1880
|
+
if (event.data.nonce !== nonce) return;
|
|
1544
1881
|
acked = true;
|
|
1545
1882
|
clearInterval(resendTimer);
|
|
1546
1883
|
clearTimeout(ackTimeout);
|
|
@@ -1556,8 +1893,12 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1556
1893
|
let cancelled = false;
|
|
1557
1894
|
async function handleCallback() {
|
|
1558
1895
|
try {
|
|
1559
|
-
|
|
1560
|
-
|
|
1896
|
+
if (!callbackPromiseRef.current) {
|
|
1897
|
+
callbackPromiseRef.current = client.handleCallback(window.location.href);
|
|
1898
|
+
}
|
|
1899
|
+
const tokenData = await callbackPromiseRef.current;
|
|
1900
|
+
if (cancelled || postProcessedRef.current) return;
|
|
1901
|
+
postProcessedRef.current = true;
|
|
1561
1902
|
if (tokenData === null) {
|
|
1562
1903
|
const clientId2 = client.config.clientId;
|
|
1563
1904
|
const probeReturnUrl = getReturnUrl(clientId2);
|
|
@@ -1578,14 +1919,12 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1578
1919
|
}
|
|
1579
1920
|
window.location.href = returnUrl && isTrustedReturnUrl(returnUrl, window.location.origin) ? returnUrl : "/";
|
|
1580
1921
|
} catch (err) {
|
|
1581
|
-
if (cancelled) return;
|
|
1922
|
+
if (cancelled || errorHandledRef.current) return;
|
|
1923
|
+
errorHandledRef.current = true;
|
|
1924
|
+
callbackPromiseRef.current = null;
|
|
1582
1925
|
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
1583
1926
|
onErrorRef.current?.(errorObj);
|
|
1584
|
-
|
|
1585
|
-
setError(errorObj.description || `SSO \u9519\u8BEF (${errorObj.code})`);
|
|
1586
|
-
} else {
|
|
1587
|
-
setError(`\u767B\u5F55\u56DE\u8C03\u5904\u7406\u5931\u8D25: ${errorObj.message}`);
|
|
1588
|
-
}
|
|
1927
|
+
setError(getCallbackErrorMessage(errorObj));
|
|
1589
1928
|
setProcessing(false);
|
|
1590
1929
|
}
|
|
1591
1930
|
}
|