@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.js
CHANGED
|
@@ -117,6 +117,7 @@ var STORAGE_PREFIX = "nihplod_sso_";
|
|
|
117
117
|
var TOKEN_KEY = "token";
|
|
118
118
|
var VERIFIER_KEY_PREFIX = "pkce_verifier_";
|
|
119
119
|
var STATE_KEY = "oauth_state";
|
|
120
|
+
var NONCE_KEY = "oidc_nonce";
|
|
120
121
|
var RETURN_URL_KEY = "return_url";
|
|
121
122
|
var LOGOUT_STATE_KEY = "logout_state";
|
|
122
123
|
function buildKey(base, clientId) {
|
|
@@ -199,6 +200,15 @@ function getOAuthState(clientId) {
|
|
|
199
200
|
function removeOAuthState(clientId) {
|
|
200
201
|
_transient.remove(buildKey(STATE_KEY, clientId));
|
|
201
202
|
}
|
|
203
|
+
function saveOAuthNonce(nonce, clientId) {
|
|
204
|
+
_transient.set(buildKey(NONCE_KEY, clientId), nonce);
|
|
205
|
+
}
|
|
206
|
+
function getOAuthNonce(clientId) {
|
|
207
|
+
return _transient.get(buildKey(NONCE_KEY, clientId));
|
|
208
|
+
}
|
|
209
|
+
function removeOAuthNonce(clientId) {
|
|
210
|
+
_transient.remove(buildKey(NONCE_KEY, clientId));
|
|
211
|
+
}
|
|
202
212
|
function saveLogoutState(state, clientId) {
|
|
203
213
|
_transient.set(buildKey(LOGOUT_STATE_KEY, clientId), state);
|
|
204
214
|
}
|
|
@@ -221,6 +231,7 @@ function clearAllSsoData(clientId) {
|
|
|
221
231
|
if (clientId) {
|
|
222
232
|
removeTokenData(clientId);
|
|
223
233
|
removeOAuthState(clientId);
|
|
234
|
+
removeOAuthNonce(clientId);
|
|
224
235
|
removeReturnUrl(clientId);
|
|
225
236
|
removeLogoutState(clientId);
|
|
226
237
|
removePkceVerifier(clientId);
|
|
@@ -229,6 +240,7 @@ function clearAllSsoData(clientId) {
|
|
|
229
240
|
}
|
|
230
241
|
removeTokenData();
|
|
231
242
|
removeOAuthState();
|
|
243
|
+
removeOAuthNonce();
|
|
232
244
|
removeReturnUrl();
|
|
233
245
|
removeLogoutState();
|
|
234
246
|
const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;
|
|
@@ -314,18 +326,21 @@ function normalizeIssuer(url) {
|
|
|
314
326
|
var cachedJwks = null;
|
|
315
327
|
var cachedDiscovery = null;
|
|
316
328
|
var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
329
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
317
330
|
async function fetchDiscoveryDoc(baseUrl) {
|
|
318
331
|
const now = Date.now();
|
|
319
332
|
if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
320
333
|
return cachedDiscovery.doc;
|
|
321
334
|
}
|
|
322
335
|
try {
|
|
323
|
-
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration
|
|
324
|
-
|
|
336
|
+
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
337
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
338
|
+
});
|
|
339
|
+
if (!res.ok) return null;
|
|
340
|
+
const doc = await res.json();
|
|
325
341
|
cachedDiscovery = { baseUrl, doc, fetchedAt: now };
|
|
326
342
|
return doc;
|
|
327
343
|
} catch {
|
|
328
|
-
cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
|
|
329
344
|
return null;
|
|
330
345
|
}
|
|
331
346
|
}
|
|
@@ -337,7 +352,10 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
337
352
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
338
353
|
const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
|
|
339
354
|
try {
|
|
340
|
-
const res = await fetch(jwksUri,
|
|
355
|
+
const res = await fetch(jwksUri, {
|
|
356
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
357
|
+
...options.forceRefresh ? { cache: "no-cache" } : {}
|
|
358
|
+
});
|
|
341
359
|
if (!res.ok) return null;
|
|
342
360
|
const jwks = await res.json();
|
|
343
361
|
cachedJwks = { baseUrl, jwks, fetchedAt: now };
|
|
@@ -384,7 +402,7 @@ async function computeAtHash(accessToken) {
|
|
|
384
402
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
385
403
|
}
|
|
386
404
|
async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
|
|
387
|
-
const { rejectHs256WhenRs256Available = true } = options;
|
|
405
|
+
const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
|
|
388
406
|
const header = decodeJwtHeader(idToken);
|
|
389
407
|
if (!header) {
|
|
390
408
|
throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
|
|
@@ -474,6 +492,12 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
474
492
|
throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
|
|
475
493
|
}
|
|
476
494
|
}
|
|
495
|
+
if (expectedNonce !== void 0) {
|
|
496
|
+
const tokenNonce = typeof payload.nonce === "string" ? payload.nonce : "";
|
|
497
|
+
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
498
|
+
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
477
501
|
return { sub: payload.sub };
|
|
478
502
|
}
|
|
479
503
|
|
|
@@ -580,8 +604,10 @@ var _SsoClient = class _SsoClient {
|
|
|
580
604
|
const verifier = generateCodeVerifier();
|
|
581
605
|
const challenge = await generateCodeChallenge(verifier);
|
|
582
606
|
const state = generateState();
|
|
607
|
+
const nonce = generateState();
|
|
583
608
|
savePkceVerifier(this.config.clientId, verifier);
|
|
584
609
|
saveOAuthState(state, this.config.clientId);
|
|
610
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
585
611
|
if (returnUrl) {
|
|
586
612
|
this._saveReturnUrlIfTrusted(returnUrl);
|
|
587
613
|
}
|
|
@@ -592,6 +618,7 @@ var _SsoClient = class _SsoClient {
|
|
|
592
618
|
params.set("redirect_uri", this.config.redirectUri);
|
|
593
619
|
params.set("scope", this.config.scopes || "openid profile");
|
|
594
620
|
params.set("state", state);
|
|
621
|
+
params.set("nonce", nonce);
|
|
595
622
|
params.set("code_challenge", challenge);
|
|
596
623
|
params.set("code_challenge_method", "S256");
|
|
597
624
|
window.location.href = `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -609,8 +636,10 @@ var _SsoClient = class _SsoClient {
|
|
|
609
636
|
const verifier = generateCodeVerifier();
|
|
610
637
|
const challenge = await generateCodeChallenge(verifier);
|
|
611
638
|
const state = generateState();
|
|
639
|
+
const nonce = generateState();
|
|
612
640
|
savePkceVerifier(this.config.clientId, verifier);
|
|
613
641
|
saveOAuthState(state, this.config.clientId);
|
|
642
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
614
643
|
if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);
|
|
615
644
|
const authorizeEndpoint = await this._getAuthorizeEndpoint();
|
|
616
645
|
const params = new URLSearchParams();
|
|
@@ -619,6 +648,7 @@ var _SsoClient = class _SsoClient {
|
|
|
619
648
|
params.set("redirect_uri", this.config.redirectUri);
|
|
620
649
|
params.set("scope", this.config.scopes || "openid profile");
|
|
621
650
|
params.set("state", state);
|
|
651
|
+
params.set("nonce", nonce);
|
|
622
652
|
params.set("code_challenge", challenge);
|
|
623
653
|
params.set("code_challenge_method", "S256");
|
|
624
654
|
return `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -660,9 +690,11 @@ var _SsoClient = class _SsoClient {
|
|
|
660
690
|
const verifier = generateCodeVerifier();
|
|
661
691
|
const challenge = await generateCodeChallenge(verifier);
|
|
662
692
|
const state = generateState();
|
|
693
|
+
const nonce = generateState();
|
|
663
694
|
const popupNonce = generateState();
|
|
664
695
|
savePkceVerifier(this.config.clientId, verifier);
|
|
665
696
|
saveOAuthState(state, this.config.clientId);
|
|
697
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
666
698
|
savePkceVerifier(`${this.config.clientId}_popup_nonce`, popupNonce);
|
|
667
699
|
if (options.returnUrl) {
|
|
668
700
|
this._saveReturnUrlIfTrusted(options.returnUrl);
|
|
@@ -674,6 +706,7 @@ var _SsoClient = class _SsoClient {
|
|
|
674
706
|
params.set("redirect_uri", this.config.redirectUri);
|
|
675
707
|
params.set("scope", this.config.scopes || "openid profile");
|
|
676
708
|
params.set("state", state);
|
|
709
|
+
params.set("nonce", nonce);
|
|
677
710
|
params.set("code_challenge", challenge);
|
|
678
711
|
params.set("code_challenge_method", "S256");
|
|
679
712
|
params.set("popup_nonce", popupNonce);
|
|
@@ -698,6 +731,7 @@ var _SsoClient = class _SsoClient {
|
|
|
698
731
|
if (!popup) {
|
|
699
732
|
removePkceVerifier(this.config.clientId);
|
|
700
733
|
removeOAuthState(this.config.clientId);
|
|
734
|
+
removeOAuthNonce(this.config.clientId);
|
|
701
735
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
702
736
|
throw new SsoError(
|
|
703
737
|
"popup_blocked",
|
|
@@ -717,6 +751,13 @@ var _SsoClient = class _SsoClient {
|
|
|
717
751
|
const savedNonce = getPkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
718
752
|
if (!savedNonce || !timingSafeEqualString(event.data.nonce ?? "", savedNonce)) return;
|
|
719
753
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
754
|
+
try {
|
|
755
|
+
event.source?.postMessage(
|
|
756
|
+
{ type: "nihplod_sso_popup_ack", nonce: savedNonce },
|
|
757
|
+
event.origin
|
|
758
|
+
);
|
|
759
|
+
} catch {
|
|
760
|
+
}
|
|
720
761
|
completed = true;
|
|
721
762
|
cleanup();
|
|
722
763
|
if (popup && !popup.closed) {
|
|
@@ -748,6 +789,7 @@ var _SsoClient = class _SsoClient {
|
|
|
748
789
|
} catch (err) {
|
|
749
790
|
removePkceVerifier(this.config.clientId);
|
|
750
791
|
removeOAuthState(this.config.clientId);
|
|
792
|
+
removeOAuthNonce(this.config.clientId);
|
|
751
793
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
752
794
|
throw err;
|
|
753
795
|
}
|
|
@@ -767,6 +809,7 @@ var _SsoClient = class _SsoClient {
|
|
|
767
809
|
const error = params.get("error");
|
|
768
810
|
if (error) {
|
|
769
811
|
removeOAuthState(this.config.clientId);
|
|
812
|
+
removeOAuthNonce(this.config.clientId);
|
|
770
813
|
removePkceVerifier(this.config.clientId);
|
|
771
814
|
const desc = params.get("error_description") || error;
|
|
772
815
|
throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
|
|
@@ -775,18 +818,21 @@ var _SsoClient = class _SsoClient {
|
|
|
775
818
|
const returnedState = params.get("state");
|
|
776
819
|
if (!code) {
|
|
777
820
|
removeOAuthState(this.config.clientId);
|
|
821
|
+
removeOAuthNonce(this.config.clientId);
|
|
778
822
|
removePkceVerifier(this.config.clientId);
|
|
779
823
|
throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
|
|
780
824
|
}
|
|
781
825
|
const savedState = getOAuthState(this.config.clientId);
|
|
782
826
|
if (!savedState || !timingSafeEqualString(savedState, returnedState ?? "")) {
|
|
783
827
|
removeOAuthState(this.config.clientId);
|
|
828
|
+
removeOAuthNonce(this.config.clientId);
|
|
784
829
|
removePkceVerifier(this.config.clientId);
|
|
785
830
|
throw new SsoError(
|
|
786
831
|
"state_mismatch",
|
|
787
832
|
"State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
|
|
788
833
|
);
|
|
789
834
|
}
|
|
835
|
+
const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
|
|
790
836
|
const verifier = getPkceVerifier(this.config.clientId);
|
|
791
837
|
if (!verifier) {
|
|
792
838
|
throw new SsoError(
|
|
@@ -833,7 +879,8 @@ var _SsoClient = class _SsoClient {
|
|
|
833
879
|
data.id_token,
|
|
834
880
|
data.access_token,
|
|
835
881
|
this.config.ssoBaseUrl,
|
|
836
|
-
this.config.clientId
|
|
882
|
+
this.config.clientId,
|
|
883
|
+
{ expectedNonce }
|
|
837
884
|
);
|
|
838
885
|
} catch (err) {
|
|
839
886
|
removeTokenData(this.config.clientId);
|
|
@@ -841,6 +888,7 @@ var _SsoClient = class _SsoClient {
|
|
|
841
888
|
}
|
|
842
889
|
}
|
|
843
890
|
removeOAuthState(this.config.clientId);
|
|
891
|
+
removeOAuthNonce(this.config.clientId);
|
|
844
892
|
removePkceVerifier(this.config.clientId);
|
|
845
893
|
const now = Date.now();
|
|
846
894
|
const tokenData = {
|
|
@@ -1192,7 +1240,9 @@ function SsoProvider({
|
|
|
1192
1240
|
setError(
|
|
1193
1241
|
err instanceof SsoError ? err : new SsoError("userinfo_failed", err instanceof Error ? err.message : String(err))
|
|
1194
1242
|
);
|
|
1195
|
-
|
|
1243
|
+
if (err instanceof SsoError && (err.code === "not_authenticated" || err.code === "session_expired")) {
|
|
1244
|
+
removeTokenData(client.config.clientId);
|
|
1245
|
+
}
|
|
1196
1246
|
} finally {
|
|
1197
1247
|
setIsLoading(false);
|
|
1198
1248
|
}
|
|
@@ -1429,6 +1479,7 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1429
1479
|
const { client, refreshUser } = useSso();
|
|
1430
1480
|
const [error, setError] = (0, import_react3.useState)(null);
|
|
1431
1481
|
const [processing, setProcessing] = (0, import_react3.useState)(true);
|
|
1482
|
+
const [awaitingManualClose, setAwaitingManualClose] = (0, import_react3.useState)(false);
|
|
1432
1483
|
const onSuccessRef = (0, import_react3.useRef)(onSuccess);
|
|
1433
1484
|
const onErrorRef = (0, import_react3.useRef)(onError);
|
|
1434
1485
|
(0, import_react3.useEffect)(() => {
|
|
@@ -1443,12 +1494,38 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1443
1494
|
targetOrigin = window.opener.location.origin;
|
|
1444
1495
|
} catch {
|
|
1445
1496
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1497
|
+
const message = {
|
|
1498
|
+
type: "nihplod_sso_popup_callback",
|
|
1499
|
+
callbackUrl: window.location.href,
|
|
1500
|
+
nonce: nonce || void 0
|
|
1501
|
+
};
|
|
1502
|
+
let acked = false;
|
|
1503
|
+
const send = () => {
|
|
1504
|
+
if (acked || !window.opener || window.opener.closed) return;
|
|
1505
|
+
window.opener.postMessage(message, targetOrigin);
|
|
1506
|
+
};
|
|
1507
|
+
send();
|
|
1508
|
+
const resendTimer = setInterval(send, 500);
|
|
1509
|
+
const ackTimeout = setTimeout(() => {
|
|
1510
|
+
if (acked) return;
|
|
1511
|
+
clearInterval(resendTimer);
|
|
1512
|
+
setAwaitingManualClose(true);
|
|
1513
|
+
}, 1e4);
|
|
1514
|
+
const handleAck = (event) => {
|
|
1515
|
+
if (event.origin !== targetOrigin) return;
|
|
1516
|
+
if (!event.data || event.data.type !== "nihplod_sso_popup_ack") return;
|
|
1517
|
+
if (nonce && event.data.nonce !== nonce) return;
|
|
1518
|
+
acked = true;
|
|
1519
|
+
clearInterval(resendTimer);
|
|
1520
|
+
clearTimeout(ackTimeout);
|
|
1521
|
+
};
|
|
1522
|
+
window.addEventListener("message", handleAck);
|
|
1450
1523
|
Promise.resolve().then(() => setProcessing(false));
|
|
1451
|
-
return
|
|
1524
|
+
return () => {
|
|
1525
|
+
clearInterval(resendTimer);
|
|
1526
|
+
clearTimeout(ackTimeout);
|
|
1527
|
+
window.removeEventListener("message", handleAck);
|
|
1528
|
+
};
|
|
1452
1529
|
}
|
|
1453
1530
|
let cancelled = false;
|
|
1454
1531
|
async function handleCallback() {
|
|
@@ -1487,6 +1564,21 @@ function CallbackPage({ onSuccess, onError, renderError } = {}) {
|
|
|
1487
1564
|
if (renderError) return import_react3.default.createElement(import_react3.default.Fragment, null, renderError(error));
|
|
1488
1565
|
return import_react3.default.createElement(DefaultCallbackError, { error });
|
|
1489
1566
|
}
|
|
1567
|
+
if (awaitingManualClose) {
|
|
1568
|
+
return import_react3.default.createElement(
|
|
1569
|
+
"div",
|
|
1570
|
+
{
|
|
1571
|
+
style: {
|
|
1572
|
+
display: "flex",
|
|
1573
|
+
alignItems: "center",
|
|
1574
|
+
justifyContent: "center",
|
|
1575
|
+
minHeight: "100vh",
|
|
1576
|
+
fontFamily: "system-ui, sans-serif"
|
|
1577
|
+
}
|
|
1578
|
+
},
|
|
1579
|
+
import_react3.default.createElement("p", null, "\u767B\u5F55\u5DF2\u5B8C\u6210\uFF0C\u8BF7\u624B\u52A8\u5173\u95ED\u6B64\u7A97\u53E3")
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1490
1582
|
if (processing) {
|
|
1491
1583
|
return import_react3.default.createElement(
|
|
1492
1584
|
"div",
|