@nihplod/sso-sdk 1.3.0 → 1.4.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 +52 -9
- package/dist/index.d.mts +26 -6
- package/dist/index.d.ts +26 -6
- package/dist/index.js +118 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +118 -10
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +81 -4
- package/dist/next/index.d.ts +81 -4
- package/dist/next/index.js +287 -23
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +285 -23
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +26 -6
- package/dist/react/index.d.ts +26 -6
- package/dist/react/index.js +176 -16
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +176 -16
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react/index.mjs
CHANGED
|
@@ -83,8 +83,10 @@ var STORAGE_PREFIX = "nihplod_sso_";
|
|
|
83
83
|
var TOKEN_KEY = "token";
|
|
84
84
|
var VERIFIER_KEY_PREFIX = "pkce_verifier_";
|
|
85
85
|
var STATE_KEY = "oauth_state";
|
|
86
|
+
var NONCE_KEY = "oidc_nonce";
|
|
86
87
|
var RETURN_URL_KEY = "return_url";
|
|
87
88
|
var LOGOUT_STATE_KEY = "logout_state";
|
|
89
|
+
var SILENT_PROBE_KEY = "silent_probe";
|
|
88
90
|
function buildKey(base, clientId) {
|
|
89
91
|
return clientId ? `${base}:${clientId}` : base;
|
|
90
92
|
}
|
|
@@ -165,6 +167,15 @@ function getOAuthState(clientId) {
|
|
|
165
167
|
function removeOAuthState(clientId) {
|
|
166
168
|
_transient.remove(buildKey(STATE_KEY, clientId));
|
|
167
169
|
}
|
|
170
|
+
function saveOAuthNonce(nonce, clientId) {
|
|
171
|
+
_transient.set(buildKey(NONCE_KEY, clientId), nonce);
|
|
172
|
+
}
|
|
173
|
+
function getOAuthNonce(clientId) {
|
|
174
|
+
return _transient.get(buildKey(NONCE_KEY, clientId));
|
|
175
|
+
}
|
|
176
|
+
function removeOAuthNonce(clientId) {
|
|
177
|
+
_transient.remove(buildKey(NONCE_KEY, clientId));
|
|
178
|
+
}
|
|
168
179
|
function saveLogoutState(state, clientId) {
|
|
169
180
|
_transient.set(buildKey(LOGOUT_STATE_KEY, clientId), state);
|
|
170
181
|
}
|
|
@@ -183,20 +194,33 @@ function getReturnUrl(clientId) {
|
|
|
183
194
|
function removeReturnUrl(clientId) {
|
|
184
195
|
_transient.remove(buildKey(RETURN_URL_KEY, clientId));
|
|
185
196
|
}
|
|
197
|
+
function saveSilentProbe(state, clientId) {
|
|
198
|
+
_transient.set(buildKey(SILENT_PROBE_KEY, clientId), state);
|
|
199
|
+
}
|
|
200
|
+
function getSilentProbe(clientId) {
|
|
201
|
+
return _transient.get(buildKey(SILENT_PROBE_KEY, clientId));
|
|
202
|
+
}
|
|
203
|
+
function removeSilentProbe(clientId) {
|
|
204
|
+
_transient.remove(buildKey(SILENT_PROBE_KEY, clientId));
|
|
205
|
+
}
|
|
186
206
|
function clearAllSsoData(clientId) {
|
|
187
207
|
if (clientId) {
|
|
188
208
|
removeTokenData(clientId);
|
|
189
209
|
removeOAuthState(clientId);
|
|
210
|
+
removeOAuthNonce(clientId);
|
|
190
211
|
removeReturnUrl(clientId);
|
|
191
212
|
removeLogoutState(clientId);
|
|
213
|
+
removeSilentProbe(clientId);
|
|
192
214
|
removePkceVerifier(clientId);
|
|
193
215
|
removePkceVerifier(`${clientId}_popup_nonce`);
|
|
194
216
|
return;
|
|
195
217
|
}
|
|
196
218
|
removeTokenData();
|
|
197
219
|
removeOAuthState();
|
|
220
|
+
removeOAuthNonce();
|
|
198
221
|
removeReturnUrl();
|
|
199
222
|
removeLogoutState();
|
|
223
|
+
removeSilentProbe();
|
|
200
224
|
const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;
|
|
201
225
|
const stores = [
|
|
202
226
|
typeof sessionStorage !== "undefined" ? sessionStorage : null,
|
|
@@ -280,18 +304,21 @@ function normalizeIssuer(url) {
|
|
|
280
304
|
var cachedJwks = null;
|
|
281
305
|
var cachedDiscovery = null;
|
|
282
306
|
var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
307
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
283
308
|
async function fetchDiscoveryDoc(baseUrl) {
|
|
284
309
|
const now = Date.now();
|
|
285
310
|
if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
286
311
|
return cachedDiscovery.doc;
|
|
287
312
|
}
|
|
288
313
|
try {
|
|
289
|
-
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration
|
|
290
|
-
|
|
314
|
+
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
315
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
316
|
+
});
|
|
317
|
+
if (!res.ok) return null;
|
|
318
|
+
const doc = await res.json();
|
|
291
319
|
cachedDiscovery = { baseUrl, doc, fetchedAt: now };
|
|
292
320
|
return doc;
|
|
293
321
|
} catch {
|
|
294
|
-
cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
|
|
295
322
|
return null;
|
|
296
323
|
}
|
|
297
324
|
}
|
|
@@ -303,7 +330,10 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
303
330
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
304
331
|
const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
|
|
305
332
|
try {
|
|
306
|
-
const res = await fetch(jwksUri,
|
|
333
|
+
const res = await fetch(jwksUri, {
|
|
334
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
335
|
+
...options.forceRefresh ? { cache: "no-cache" } : {}
|
|
336
|
+
});
|
|
307
337
|
if (!res.ok) return null;
|
|
308
338
|
const jwks = await res.json();
|
|
309
339
|
cachedJwks = { baseUrl, jwks, fetchedAt: now };
|
|
@@ -350,7 +380,7 @@ async function computeAtHash(accessToken) {
|
|
|
350
380
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
351
381
|
}
|
|
352
382
|
async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
|
|
353
|
-
const { rejectHs256WhenRs256Available = true } = options;
|
|
383
|
+
const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
|
|
354
384
|
const header = decodeJwtHeader(idToken);
|
|
355
385
|
if (!header) {
|
|
356
386
|
throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
|
|
@@ -440,10 +470,21 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
440
470
|
throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
|
|
441
471
|
}
|
|
442
472
|
}
|
|
473
|
+
if (expectedNonce !== void 0) {
|
|
474
|
+
const tokenNonce = typeof payload.nonce === "string" ? payload.nonce : "";
|
|
475
|
+
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
476
|
+
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
443
479
|
return { sub: payload.sub };
|
|
444
480
|
}
|
|
445
481
|
|
|
446
482
|
// src/core/SsoClient.ts
|
|
483
|
+
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
|
|
484
|
+
"login_required",
|
|
485
|
+
"consent_required",
|
|
486
|
+
"interaction_required"
|
|
487
|
+
]);
|
|
447
488
|
var _SsoClient = class _SsoClient {
|
|
448
489
|
constructor(config) {
|
|
449
490
|
this._discovery = null;
|
|
@@ -537,17 +578,27 @@ var _SsoClient = class _SsoClient {
|
|
|
537
578
|
*
|
|
538
579
|
* @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;
|
|
539
580
|
* 仅允许相对路径或同源绝对 URL,否则忽略并告警)
|
|
581
|
+
* @param options.prompt - OIDC prompt 参数。传 "none" 为静默探测:
|
|
582
|
+
* 额外在存储中记录探测标记(该次请求的 state),回调无会话时
|
|
583
|
+
* handleCallback() 返回 null 而不抛错。
|
|
540
584
|
*
|
|
541
585
|
* ⚠️ 不要与 getLoginUrl() 混用:两者都会重新生成并覆盖 sessionStorage 中的
|
|
542
586
|
* state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
|
|
543
587
|
* 同一次登录只使用其中一个入口。
|
|
544
588
|
*/
|
|
545
|
-
async login(returnUrl) {
|
|
589
|
+
async login(returnUrl, options = {}) {
|
|
546
590
|
const verifier = generateCodeVerifier();
|
|
547
591
|
const challenge = await generateCodeChallenge(verifier);
|
|
548
592
|
const state = generateState();
|
|
593
|
+
const nonce = generateState();
|
|
549
594
|
savePkceVerifier(this.config.clientId, verifier);
|
|
550
595
|
saveOAuthState(state, this.config.clientId);
|
|
596
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
597
|
+
if (options.prompt === "none") {
|
|
598
|
+
saveSilentProbe(state, this.config.clientId);
|
|
599
|
+
} else {
|
|
600
|
+
removeSilentProbe(this.config.clientId);
|
|
601
|
+
}
|
|
551
602
|
if (returnUrl) {
|
|
552
603
|
this._saveReturnUrlIfTrusted(returnUrl);
|
|
553
604
|
}
|
|
@@ -558,8 +609,12 @@ var _SsoClient = class _SsoClient {
|
|
|
558
609
|
params.set("redirect_uri", this.config.redirectUri);
|
|
559
610
|
params.set("scope", this.config.scopes || "openid profile");
|
|
560
611
|
params.set("state", state);
|
|
612
|
+
params.set("nonce", nonce);
|
|
561
613
|
params.set("code_challenge", challenge);
|
|
562
614
|
params.set("code_challenge_method", "S256");
|
|
615
|
+
if (options.prompt) {
|
|
616
|
+
params.set("prompt", options.prompt);
|
|
617
|
+
}
|
|
563
618
|
window.location.href = `${authorizeEndpoint}?${params.toString()}`;
|
|
564
619
|
}
|
|
565
620
|
/**
|
|
@@ -571,12 +626,19 @@ var _SsoClient = class _SsoClient {
|
|
|
571
626
|
* state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
|
|
572
627
|
* 同一次登录只使用其中一个入口。
|
|
573
628
|
*/
|
|
574
|
-
async getLoginUrl(returnUrl) {
|
|
629
|
+
async getLoginUrl(returnUrl, options = {}) {
|
|
575
630
|
const verifier = generateCodeVerifier();
|
|
576
631
|
const challenge = await generateCodeChallenge(verifier);
|
|
577
632
|
const state = generateState();
|
|
633
|
+
const nonce = generateState();
|
|
578
634
|
savePkceVerifier(this.config.clientId, verifier);
|
|
579
635
|
saveOAuthState(state, this.config.clientId);
|
|
636
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
637
|
+
if (options.prompt === "none") {
|
|
638
|
+
saveSilentProbe(state, this.config.clientId);
|
|
639
|
+
} else {
|
|
640
|
+
removeSilentProbe(this.config.clientId);
|
|
641
|
+
}
|
|
580
642
|
if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);
|
|
581
643
|
const authorizeEndpoint = await this._getAuthorizeEndpoint();
|
|
582
644
|
const params = new URLSearchParams();
|
|
@@ -585,8 +647,12 @@ var _SsoClient = class _SsoClient {
|
|
|
585
647
|
params.set("redirect_uri", this.config.redirectUri);
|
|
586
648
|
params.set("scope", this.config.scopes || "openid profile");
|
|
587
649
|
params.set("state", state);
|
|
650
|
+
params.set("nonce", nonce);
|
|
588
651
|
params.set("code_challenge", challenge);
|
|
589
652
|
params.set("code_challenge_method", "S256");
|
|
653
|
+
if (options.prompt) {
|
|
654
|
+
params.set("prompt", options.prompt);
|
|
655
|
+
}
|
|
590
656
|
return `${authorizeEndpoint}?${params.toString()}`;
|
|
591
657
|
}
|
|
592
658
|
/**
|
|
@@ -626,9 +692,11 @@ var _SsoClient = class _SsoClient {
|
|
|
626
692
|
const verifier = generateCodeVerifier();
|
|
627
693
|
const challenge = await generateCodeChallenge(verifier);
|
|
628
694
|
const state = generateState();
|
|
695
|
+
const nonce = generateState();
|
|
629
696
|
const popupNonce = generateState();
|
|
630
697
|
savePkceVerifier(this.config.clientId, verifier);
|
|
631
698
|
saveOAuthState(state, this.config.clientId);
|
|
699
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
632
700
|
savePkceVerifier(`${this.config.clientId}_popup_nonce`, popupNonce);
|
|
633
701
|
if (options.returnUrl) {
|
|
634
702
|
this._saveReturnUrlIfTrusted(options.returnUrl);
|
|
@@ -640,6 +708,7 @@ var _SsoClient = class _SsoClient {
|
|
|
640
708
|
params.set("redirect_uri", this.config.redirectUri);
|
|
641
709
|
params.set("scope", this.config.scopes || "openid profile");
|
|
642
710
|
params.set("state", state);
|
|
711
|
+
params.set("nonce", nonce);
|
|
643
712
|
params.set("code_challenge", challenge);
|
|
644
713
|
params.set("code_challenge_method", "S256");
|
|
645
714
|
params.set("popup_nonce", popupNonce);
|
|
@@ -664,6 +733,7 @@ var _SsoClient = class _SsoClient {
|
|
|
664
733
|
if (!popup) {
|
|
665
734
|
removePkceVerifier(this.config.clientId);
|
|
666
735
|
removeOAuthState(this.config.clientId);
|
|
736
|
+
removeOAuthNonce(this.config.clientId);
|
|
667
737
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
668
738
|
throw new SsoError(
|
|
669
739
|
"popup_blocked",
|
|
@@ -683,12 +753,27 @@ var _SsoClient = class _SsoClient {
|
|
|
683
753
|
const savedNonce = getPkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
684
754
|
if (!savedNonce || !timingSafeEqualString(event.data.nonce ?? "", savedNonce)) return;
|
|
685
755
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
756
|
+
try {
|
|
757
|
+
event.source?.postMessage(
|
|
758
|
+
{ type: "nihplod_sso_popup_ack", nonce: savedNonce },
|
|
759
|
+
event.origin
|
|
760
|
+
);
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
686
763
|
completed = true;
|
|
687
764
|
cleanup();
|
|
688
765
|
if (popup && !popup.closed) {
|
|
689
766
|
popup.close();
|
|
690
767
|
}
|
|
691
|
-
this.handleCallback(event.data.callbackUrl).then(
|
|
768
|
+
this.handleCallback(event.data.callbackUrl).then((tokenData) => {
|
|
769
|
+
if (tokenData === null) {
|
|
770
|
+
reject(
|
|
771
|
+
new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09")
|
|
772
|
+
);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
resolve(tokenData);
|
|
776
|
+
}).catch(reject);
|
|
692
777
|
};
|
|
693
778
|
const pollTimer = setInterval(() => {
|
|
694
779
|
if (popup.closed) {
|
|
@@ -714,6 +799,7 @@ var _SsoClient = class _SsoClient {
|
|
|
714
799
|
} catch (err) {
|
|
715
800
|
removePkceVerifier(this.config.clientId);
|
|
716
801
|
removeOAuthState(this.config.clientId);
|
|
802
|
+
removeOAuthNonce(this.config.clientId);
|
|
717
803
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
718
804
|
throw err;
|
|
719
805
|
}
|
|
@@ -725,15 +811,29 @@ var _SsoClient = class _SsoClient {
|
|
|
725
811
|
* 成功后 token 自动保存到 token 存储(默认 sessionStorage,可通过 setTokenStorage 定制)。
|
|
726
812
|
*
|
|
727
813
|
* @param callbackUrl - 完整的回调 URL(window.location.href)
|
|
728
|
-
* @returns TokenData
|
|
814
|
+
* @returns TokenData;prompt=none 静默探测且无 SSO 会话时返回 null
|
|
815
|
+
* (IdP 回跳 login_required / consent_required / interaction_required
|
|
816
|
+
* 且回调 state 与探测标记匹配),此时临时数据已清理,调用方按"未登录"处理即可
|
|
729
817
|
*/
|
|
730
818
|
async handleCallback(callbackUrl) {
|
|
731
819
|
const url = new URL(callbackUrl);
|
|
732
820
|
const params = url.searchParams;
|
|
733
821
|
const error = params.get("error");
|
|
734
822
|
if (error) {
|
|
823
|
+
const returnedErrorState = params.get("state");
|
|
824
|
+
const probeState = getSilentProbe(this.config.clientId);
|
|
825
|
+
const savedStateForProbe = getOAuthState(this.config.clientId);
|
|
826
|
+
if (SILENT_PROBE_ERRORS.has(error) && probeState && savedStateForProbe && timingSafeEqualString(savedStateForProbe, returnedErrorState ?? "") && timingSafeEqualString(probeState, returnedErrorState ?? "")) {
|
|
827
|
+
removeOAuthState(this.config.clientId);
|
|
828
|
+
removeOAuthNonce(this.config.clientId);
|
|
829
|
+
removePkceVerifier(this.config.clientId);
|
|
830
|
+
removeSilentProbe(this.config.clientId);
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
735
833
|
removeOAuthState(this.config.clientId);
|
|
834
|
+
removeOAuthNonce(this.config.clientId);
|
|
736
835
|
removePkceVerifier(this.config.clientId);
|
|
836
|
+
removeSilentProbe(this.config.clientId);
|
|
737
837
|
const desc = params.get("error_description") || error;
|
|
738
838
|
throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
|
|
739
839
|
}
|
|
@@ -741,18 +841,23 @@ var _SsoClient = class _SsoClient {
|
|
|
741
841
|
const returnedState = params.get("state");
|
|
742
842
|
if (!code) {
|
|
743
843
|
removeOAuthState(this.config.clientId);
|
|
844
|
+
removeOAuthNonce(this.config.clientId);
|
|
744
845
|
removePkceVerifier(this.config.clientId);
|
|
846
|
+
removeSilentProbe(this.config.clientId);
|
|
745
847
|
throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
|
|
746
848
|
}
|
|
747
849
|
const savedState = getOAuthState(this.config.clientId);
|
|
748
850
|
if (!savedState || !timingSafeEqualString(savedState, returnedState ?? "")) {
|
|
749
851
|
removeOAuthState(this.config.clientId);
|
|
852
|
+
removeOAuthNonce(this.config.clientId);
|
|
750
853
|
removePkceVerifier(this.config.clientId);
|
|
854
|
+
removeSilentProbe(this.config.clientId);
|
|
751
855
|
throw new SsoError(
|
|
752
856
|
"state_mismatch",
|
|
753
857
|
"State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
|
|
754
858
|
);
|
|
755
859
|
}
|
|
860
|
+
const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
|
|
756
861
|
const verifier = getPkceVerifier(this.config.clientId);
|
|
757
862
|
if (!verifier) {
|
|
758
863
|
throw new SsoError(
|
|
@@ -799,7 +904,8 @@ var _SsoClient = class _SsoClient {
|
|
|
799
904
|
data.id_token,
|
|
800
905
|
data.access_token,
|
|
801
906
|
this.config.ssoBaseUrl,
|
|
802
|
-
this.config.clientId
|
|
907
|
+
this.config.clientId,
|
|
908
|
+
{ expectedNonce }
|
|
803
909
|
);
|
|
804
910
|
} catch (err) {
|
|
805
911
|
removeTokenData(this.config.clientId);
|
|
@@ -807,7 +913,9 @@ var _SsoClient = class _SsoClient {
|
|
|
807
913
|
}
|
|
808
914
|
}
|
|
809
915
|
removeOAuthState(this.config.clientId);
|
|
916
|
+
removeOAuthNonce(this.config.clientId);
|
|
810
917
|
removePkceVerifier(this.config.clientId);
|
|
918
|
+
removeSilentProbe(this.config.clientId);
|
|
811
919
|
const now = Date.now();
|
|
812
920
|
const tokenData = {
|
|
813
921
|
access_token: data.access_token,
|
|
@@ -1158,7 +1266,9 @@ function SsoProvider({
|
|
|
1158
1266
|
setError(
|
|
1159
1267
|
err instanceof SsoError ? err : new SsoError("userinfo_failed", err instanceof Error ? err.message : String(err))
|
|
1160
1268
|
);
|
|
1161
|
-
|
|
1269
|
+
if (err instanceof SsoError && (err.code === "not_authenticated" || err.code === "session_expired")) {
|
|
1270
|
+
removeTokenData(client.config.clientId);
|
|
1271
|
+
}
|
|
1162
1272
|
} finally {
|
|
1163
1273
|
setIsLoading(false);
|
|
1164
1274
|
}
|
|
@@ -1395,6 +1505,7 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1395
1505
|
const { client, refreshUser } = useSso();
|
|
1396
1506
|
const [error, setError] = useState3(null);
|
|
1397
1507
|
const [processing, setProcessing] = useState3(true);
|
|
1508
|
+
const [awaitingManualClose, setAwaitingManualClose] = useState3(false);
|
|
1398
1509
|
const onSuccessRef = useRef3(onSuccess);
|
|
1399
1510
|
const onErrorRef = useRef3(onError);
|
|
1400
1511
|
useEffect3(() => {
|
|
@@ -1409,18 +1520,52 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1409
1520
|
targetOrigin = window.opener.location.origin;
|
|
1410
1521
|
} catch {
|
|
1411
1522
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1523
|
+
const message = {
|
|
1524
|
+
type: "nihplod_sso_popup_callback",
|
|
1525
|
+
callbackUrl: window.location.href,
|
|
1526
|
+
nonce: nonce || void 0
|
|
1527
|
+
};
|
|
1528
|
+
let acked = false;
|
|
1529
|
+
const send = () => {
|
|
1530
|
+
if (acked || !window.opener || window.opener.closed) return;
|
|
1531
|
+
window.opener.postMessage(message, targetOrigin);
|
|
1532
|
+
};
|
|
1533
|
+
send();
|
|
1534
|
+
const resendTimer = setInterval(send, 500);
|
|
1535
|
+
const ackTimeout = setTimeout(() => {
|
|
1536
|
+
if (acked) return;
|
|
1537
|
+
clearInterval(resendTimer);
|
|
1538
|
+
setAwaitingManualClose(true);
|
|
1539
|
+
}, 1e4);
|
|
1540
|
+
const handleAck = (event) => {
|
|
1541
|
+
if (event.origin !== targetOrigin) return;
|
|
1542
|
+
if (!event.data || event.data.type !== "nihplod_sso_popup_ack") return;
|
|
1543
|
+
if (nonce && event.data.nonce !== nonce) return;
|
|
1544
|
+
acked = true;
|
|
1545
|
+
clearInterval(resendTimer);
|
|
1546
|
+
clearTimeout(ackTimeout);
|
|
1547
|
+
};
|
|
1548
|
+
window.addEventListener("message", handleAck);
|
|
1416
1549
|
Promise.resolve().then(() => setProcessing(false));
|
|
1417
|
-
return
|
|
1550
|
+
return () => {
|
|
1551
|
+
clearInterval(resendTimer);
|
|
1552
|
+
clearTimeout(ackTimeout);
|
|
1553
|
+
window.removeEventListener("message", handleAck);
|
|
1554
|
+
};
|
|
1418
1555
|
}
|
|
1419
1556
|
let cancelled = false;
|
|
1420
1557
|
async function handleCallback() {
|
|
1421
1558
|
try {
|
|
1422
1559
|
const tokenData = await client.handleCallback(window.location.href);
|
|
1423
1560
|
if (cancelled) return;
|
|
1561
|
+
if (tokenData === null) {
|
|
1562
|
+
const clientId2 = client.config.clientId;
|
|
1563
|
+
const probeReturnUrl = getReturnUrl(clientId2);
|
|
1564
|
+
removeReturnUrl(clientId2);
|
|
1565
|
+
const target = probeReturnUrl && isTrustedReturnUrl(probeReturnUrl, window.location.origin) ? probeReturnUrl : "/";
|
|
1566
|
+
window.location.href = target + (target.includes("?") ? "&" : "?") + "sso_probe=no_session";
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1424
1569
|
await refreshUser();
|
|
1425
1570
|
if (cancelled) return;
|
|
1426
1571
|
const clientId = client.config.clientId;
|
|
@@ -1453,6 +1598,21 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1453
1598
|
if (renderError) return React3.createElement(React3.Fragment, null, renderError(error));
|
|
1454
1599
|
return React3.createElement(DefaultCallbackError, { error });
|
|
1455
1600
|
}
|
|
1601
|
+
if (awaitingManualClose) {
|
|
1602
|
+
return React3.createElement(
|
|
1603
|
+
"div",
|
|
1604
|
+
{
|
|
1605
|
+
style: {
|
|
1606
|
+
display: "flex",
|
|
1607
|
+
alignItems: "center",
|
|
1608
|
+
justifyContent: "center",
|
|
1609
|
+
minHeight: "100vh",
|
|
1610
|
+
fontFamily: "system-ui, sans-serif"
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
React3.createElement("p", null, "\u767B\u5F55\u5DF2\u5B8C\u6210\uFF0C\u8BF7\u624B\u52A8\u5173\u95ED\u6B64\u7A97\u53E3")
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1456
1616
|
if (processing) {
|
|
1457
1617
|
return React3.createElement(
|
|
1458
1618
|
"div",
|