@nihplod/sso-sdk 1.3.0 → 1.3.1
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 +6 -4
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +54 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +54 -6
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +9 -2
- package/dist/next/index.d.ts +9 -2
- package/dist/next/index.js +74 -21
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +73 -21
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +104 -12
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +104 -12
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react/index.mjs
CHANGED
|
@@ -83,6 +83,7 @@ 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";
|
|
88
89
|
function buildKey(base, clientId) {
|
|
@@ -165,6 +166,15 @@ function getOAuthState(clientId) {
|
|
|
165
166
|
function removeOAuthState(clientId) {
|
|
166
167
|
_transient.remove(buildKey(STATE_KEY, clientId));
|
|
167
168
|
}
|
|
169
|
+
function saveOAuthNonce(nonce, clientId) {
|
|
170
|
+
_transient.set(buildKey(NONCE_KEY, clientId), nonce);
|
|
171
|
+
}
|
|
172
|
+
function getOAuthNonce(clientId) {
|
|
173
|
+
return _transient.get(buildKey(NONCE_KEY, clientId));
|
|
174
|
+
}
|
|
175
|
+
function removeOAuthNonce(clientId) {
|
|
176
|
+
_transient.remove(buildKey(NONCE_KEY, clientId));
|
|
177
|
+
}
|
|
168
178
|
function saveLogoutState(state, clientId) {
|
|
169
179
|
_transient.set(buildKey(LOGOUT_STATE_KEY, clientId), state);
|
|
170
180
|
}
|
|
@@ -187,6 +197,7 @@ function clearAllSsoData(clientId) {
|
|
|
187
197
|
if (clientId) {
|
|
188
198
|
removeTokenData(clientId);
|
|
189
199
|
removeOAuthState(clientId);
|
|
200
|
+
removeOAuthNonce(clientId);
|
|
190
201
|
removeReturnUrl(clientId);
|
|
191
202
|
removeLogoutState(clientId);
|
|
192
203
|
removePkceVerifier(clientId);
|
|
@@ -195,6 +206,7 @@ function clearAllSsoData(clientId) {
|
|
|
195
206
|
}
|
|
196
207
|
removeTokenData();
|
|
197
208
|
removeOAuthState();
|
|
209
|
+
removeOAuthNonce();
|
|
198
210
|
removeReturnUrl();
|
|
199
211
|
removeLogoutState();
|
|
200
212
|
const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;
|
|
@@ -280,18 +292,21 @@ function normalizeIssuer(url) {
|
|
|
280
292
|
var cachedJwks = null;
|
|
281
293
|
var cachedDiscovery = null;
|
|
282
294
|
var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
295
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
283
296
|
async function fetchDiscoveryDoc(baseUrl) {
|
|
284
297
|
const now = Date.now();
|
|
285
298
|
if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
286
299
|
return cachedDiscovery.doc;
|
|
287
300
|
}
|
|
288
301
|
try {
|
|
289
|
-
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration
|
|
290
|
-
|
|
302
|
+
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
303
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
304
|
+
});
|
|
305
|
+
if (!res.ok) return null;
|
|
306
|
+
const doc = await res.json();
|
|
291
307
|
cachedDiscovery = { baseUrl, doc, fetchedAt: now };
|
|
292
308
|
return doc;
|
|
293
309
|
} catch {
|
|
294
|
-
cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
|
|
295
310
|
return null;
|
|
296
311
|
}
|
|
297
312
|
}
|
|
@@ -303,7 +318,10 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
303
318
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
304
319
|
const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
|
|
305
320
|
try {
|
|
306
|
-
const res = await fetch(jwksUri,
|
|
321
|
+
const res = await fetch(jwksUri, {
|
|
322
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
323
|
+
...options.forceRefresh ? { cache: "no-cache" } : {}
|
|
324
|
+
});
|
|
307
325
|
if (!res.ok) return null;
|
|
308
326
|
const jwks = await res.json();
|
|
309
327
|
cachedJwks = { baseUrl, jwks, fetchedAt: now };
|
|
@@ -350,7 +368,7 @@ async function computeAtHash(accessToken) {
|
|
|
350
368
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
351
369
|
}
|
|
352
370
|
async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
|
|
353
|
-
const { rejectHs256WhenRs256Available = true } = options;
|
|
371
|
+
const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
|
|
354
372
|
const header = decodeJwtHeader(idToken);
|
|
355
373
|
if (!header) {
|
|
356
374
|
throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
|
|
@@ -440,6 +458,12 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
440
458
|
throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
|
|
441
459
|
}
|
|
442
460
|
}
|
|
461
|
+
if (expectedNonce !== void 0) {
|
|
462
|
+
const tokenNonce = typeof payload.nonce === "string" ? payload.nonce : "";
|
|
463
|
+
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
464
|
+
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
443
467
|
return { sub: payload.sub };
|
|
444
468
|
}
|
|
445
469
|
|
|
@@ -546,8 +570,10 @@ var _SsoClient = class _SsoClient {
|
|
|
546
570
|
const verifier = generateCodeVerifier();
|
|
547
571
|
const challenge = await generateCodeChallenge(verifier);
|
|
548
572
|
const state = generateState();
|
|
573
|
+
const nonce = generateState();
|
|
549
574
|
savePkceVerifier(this.config.clientId, verifier);
|
|
550
575
|
saveOAuthState(state, this.config.clientId);
|
|
576
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
551
577
|
if (returnUrl) {
|
|
552
578
|
this._saveReturnUrlIfTrusted(returnUrl);
|
|
553
579
|
}
|
|
@@ -558,6 +584,7 @@ var _SsoClient = class _SsoClient {
|
|
|
558
584
|
params.set("redirect_uri", this.config.redirectUri);
|
|
559
585
|
params.set("scope", this.config.scopes || "openid profile");
|
|
560
586
|
params.set("state", state);
|
|
587
|
+
params.set("nonce", nonce);
|
|
561
588
|
params.set("code_challenge", challenge);
|
|
562
589
|
params.set("code_challenge_method", "S256");
|
|
563
590
|
window.location.href = `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -575,8 +602,10 @@ var _SsoClient = class _SsoClient {
|
|
|
575
602
|
const verifier = generateCodeVerifier();
|
|
576
603
|
const challenge = await generateCodeChallenge(verifier);
|
|
577
604
|
const state = generateState();
|
|
605
|
+
const nonce = generateState();
|
|
578
606
|
savePkceVerifier(this.config.clientId, verifier);
|
|
579
607
|
saveOAuthState(state, this.config.clientId);
|
|
608
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
580
609
|
if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);
|
|
581
610
|
const authorizeEndpoint = await this._getAuthorizeEndpoint();
|
|
582
611
|
const params = new URLSearchParams();
|
|
@@ -585,6 +614,7 @@ var _SsoClient = class _SsoClient {
|
|
|
585
614
|
params.set("redirect_uri", this.config.redirectUri);
|
|
586
615
|
params.set("scope", this.config.scopes || "openid profile");
|
|
587
616
|
params.set("state", state);
|
|
617
|
+
params.set("nonce", nonce);
|
|
588
618
|
params.set("code_challenge", challenge);
|
|
589
619
|
params.set("code_challenge_method", "S256");
|
|
590
620
|
return `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -626,9 +656,11 @@ var _SsoClient = class _SsoClient {
|
|
|
626
656
|
const verifier = generateCodeVerifier();
|
|
627
657
|
const challenge = await generateCodeChallenge(verifier);
|
|
628
658
|
const state = generateState();
|
|
659
|
+
const nonce = generateState();
|
|
629
660
|
const popupNonce = generateState();
|
|
630
661
|
savePkceVerifier(this.config.clientId, verifier);
|
|
631
662
|
saveOAuthState(state, this.config.clientId);
|
|
663
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
632
664
|
savePkceVerifier(`${this.config.clientId}_popup_nonce`, popupNonce);
|
|
633
665
|
if (options.returnUrl) {
|
|
634
666
|
this._saveReturnUrlIfTrusted(options.returnUrl);
|
|
@@ -640,6 +672,7 @@ var _SsoClient = class _SsoClient {
|
|
|
640
672
|
params.set("redirect_uri", this.config.redirectUri);
|
|
641
673
|
params.set("scope", this.config.scopes || "openid profile");
|
|
642
674
|
params.set("state", state);
|
|
675
|
+
params.set("nonce", nonce);
|
|
643
676
|
params.set("code_challenge", challenge);
|
|
644
677
|
params.set("code_challenge_method", "S256");
|
|
645
678
|
params.set("popup_nonce", popupNonce);
|
|
@@ -664,6 +697,7 @@ var _SsoClient = class _SsoClient {
|
|
|
664
697
|
if (!popup) {
|
|
665
698
|
removePkceVerifier(this.config.clientId);
|
|
666
699
|
removeOAuthState(this.config.clientId);
|
|
700
|
+
removeOAuthNonce(this.config.clientId);
|
|
667
701
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
668
702
|
throw new SsoError(
|
|
669
703
|
"popup_blocked",
|
|
@@ -683,6 +717,13 @@ var _SsoClient = class _SsoClient {
|
|
|
683
717
|
const savedNonce = getPkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
684
718
|
if (!savedNonce || !timingSafeEqualString(event.data.nonce ?? "", savedNonce)) return;
|
|
685
719
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
720
|
+
try {
|
|
721
|
+
event.source?.postMessage(
|
|
722
|
+
{ type: "nihplod_sso_popup_ack", nonce: savedNonce },
|
|
723
|
+
event.origin
|
|
724
|
+
);
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
686
727
|
completed = true;
|
|
687
728
|
cleanup();
|
|
688
729
|
if (popup && !popup.closed) {
|
|
@@ -714,6 +755,7 @@ var _SsoClient = class _SsoClient {
|
|
|
714
755
|
} catch (err) {
|
|
715
756
|
removePkceVerifier(this.config.clientId);
|
|
716
757
|
removeOAuthState(this.config.clientId);
|
|
758
|
+
removeOAuthNonce(this.config.clientId);
|
|
717
759
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
718
760
|
throw err;
|
|
719
761
|
}
|
|
@@ -733,6 +775,7 @@ var _SsoClient = class _SsoClient {
|
|
|
733
775
|
const error = params.get("error");
|
|
734
776
|
if (error) {
|
|
735
777
|
removeOAuthState(this.config.clientId);
|
|
778
|
+
removeOAuthNonce(this.config.clientId);
|
|
736
779
|
removePkceVerifier(this.config.clientId);
|
|
737
780
|
const desc = params.get("error_description") || error;
|
|
738
781
|
throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
|
|
@@ -741,18 +784,21 @@ var _SsoClient = class _SsoClient {
|
|
|
741
784
|
const returnedState = params.get("state");
|
|
742
785
|
if (!code) {
|
|
743
786
|
removeOAuthState(this.config.clientId);
|
|
787
|
+
removeOAuthNonce(this.config.clientId);
|
|
744
788
|
removePkceVerifier(this.config.clientId);
|
|
745
789
|
throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
|
|
746
790
|
}
|
|
747
791
|
const savedState = getOAuthState(this.config.clientId);
|
|
748
792
|
if (!savedState || !timingSafeEqualString(savedState, returnedState ?? "")) {
|
|
749
793
|
removeOAuthState(this.config.clientId);
|
|
794
|
+
removeOAuthNonce(this.config.clientId);
|
|
750
795
|
removePkceVerifier(this.config.clientId);
|
|
751
796
|
throw new SsoError(
|
|
752
797
|
"state_mismatch",
|
|
753
798
|
"State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
|
|
754
799
|
);
|
|
755
800
|
}
|
|
801
|
+
const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
|
|
756
802
|
const verifier = getPkceVerifier(this.config.clientId);
|
|
757
803
|
if (!verifier) {
|
|
758
804
|
throw new SsoError(
|
|
@@ -799,7 +845,8 @@ var _SsoClient = class _SsoClient {
|
|
|
799
845
|
data.id_token,
|
|
800
846
|
data.access_token,
|
|
801
847
|
this.config.ssoBaseUrl,
|
|
802
|
-
this.config.clientId
|
|
848
|
+
this.config.clientId,
|
|
849
|
+
{ expectedNonce }
|
|
803
850
|
);
|
|
804
851
|
} catch (err) {
|
|
805
852
|
removeTokenData(this.config.clientId);
|
|
@@ -807,6 +854,7 @@ var _SsoClient = class _SsoClient {
|
|
|
807
854
|
}
|
|
808
855
|
}
|
|
809
856
|
removeOAuthState(this.config.clientId);
|
|
857
|
+
removeOAuthNonce(this.config.clientId);
|
|
810
858
|
removePkceVerifier(this.config.clientId);
|
|
811
859
|
const now = Date.now();
|
|
812
860
|
const tokenData = {
|
|
@@ -1158,7 +1206,9 @@ function SsoProvider({
|
|
|
1158
1206
|
setError(
|
|
1159
1207
|
err instanceof SsoError ? err : new SsoError("userinfo_failed", err instanceof Error ? err.message : String(err))
|
|
1160
1208
|
);
|
|
1161
|
-
|
|
1209
|
+
if (err instanceof SsoError && (err.code === "not_authenticated" || err.code === "session_expired")) {
|
|
1210
|
+
removeTokenData(client.config.clientId);
|
|
1211
|
+
}
|
|
1162
1212
|
} finally {
|
|
1163
1213
|
setIsLoading(false);
|
|
1164
1214
|
}
|
|
@@ -1395,6 +1445,7 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1395
1445
|
const { client, refreshUser } = useSso();
|
|
1396
1446
|
const [error, setError] = useState3(null);
|
|
1397
1447
|
const [processing, setProcessing] = useState3(true);
|
|
1448
|
+
const [awaitingManualClose, setAwaitingManualClose] = useState3(false);
|
|
1398
1449
|
const onSuccessRef = useRef3(onSuccess);
|
|
1399
1450
|
const onErrorRef = useRef3(onError);
|
|
1400
1451
|
useEffect3(() => {
|
|
@@ -1409,12 +1460,38 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1409
1460
|
targetOrigin = window.opener.location.origin;
|
|
1410
1461
|
} catch {
|
|
1411
1462
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1463
|
+
const message = {
|
|
1464
|
+
type: "nihplod_sso_popup_callback",
|
|
1465
|
+
callbackUrl: window.location.href,
|
|
1466
|
+
nonce: nonce || void 0
|
|
1467
|
+
};
|
|
1468
|
+
let acked = false;
|
|
1469
|
+
const send = () => {
|
|
1470
|
+
if (acked || !window.opener || window.opener.closed) return;
|
|
1471
|
+
window.opener.postMessage(message, targetOrigin);
|
|
1472
|
+
};
|
|
1473
|
+
send();
|
|
1474
|
+
const resendTimer = setInterval(send, 500);
|
|
1475
|
+
const ackTimeout = setTimeout(() => {
|
|
1476
|
+
if (acked) return;
|
|
1477
|
+
clearInterval(resendTimer);
|
|
1478
|
+
setAwaitingManualClose(true);
|
|
1479
|
+
}, 1e4);
|
|
1480
|
+
const handleAck = (event) => {
|
|
1481
|
+
if (event.origin !== targetOrigin) return;
|
|
1482
|
+
if (!event.data || event.data.type !== "nihplod_sso_popup_ack") return;
|
|
1483
|
+
if (nonce && event.data.nonce !== nonce) return;
|
|
1484
|
+
acked = true;
|
|
1485
|
+
clearInterval(resendTimer);
|
|
1486
|
+
clearTimeout(ackTimeout);
|
|
1487
|
+
};
|
|
1488
|
+
window.addEventListener("message", handleAck);
|
|
1416
1489
|
Promise.resolve().then(() => setProcessing(false));
|
|
1417
|
-
return
|
|
1490
|
+
return () => {
|
|
1491
|
+
clearInterval(resendTimer);
|
|
1492
|
+
clearTimeout(ackTimeout);
|
|
1493
|
+
window.removeEventListener("message", handleAck);
|
|
1494
|
+
};
|
|
1418
1495
|
}
|
|
1419
1496
|
let cancelled = false;
|
|
1420
1497
|
async function handleCallback() {
|
|
@@ -1453,6 +1530,21 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1453
1530
|
if (renderError) return React3.createElement(React3.Fragment, null, renderError(error));
|
|
1454
1531
|
return React3.createElement(DefaultCallbackError, { error });
|
|
1455
1532
|
}
|
|
1533
|
+
if (awaitingManualClose) {
|
|
1534
|
+
return React3.createElement(
|
|
1535
|
+
"div",
|
|
1536
|
+
{
|
|
1537
|
+
style: {
|
|
1538
|
+
display: "flex",
|
|
1539
|
+
alignItems: "center",
|
|
1540
|
+
justifyContent: "center",
|
|
1541
|
+
minHeight: "100vh",
|
|
1542
|
+
fontFamily: "system-ui, sans-serif"
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
React3.createElement("p", null, "\u767B\u5F55\u5DF2\u5B8C\u6210\uFF0C\u8BF7\u624B\u52A8\u5173\u95ED\u6B64\u7A97\u53E3")
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1456
1548
|
if (processing) {
|
|
1457
1549
|
return React3.createElement(
|
|
1458
1550
|
"div",
|