@nihplod/sso-sdk 1.2.1 → 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 +445 -441
- package/dist/index.d.mts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +62 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -11
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +27 -2
- package/dist/next/index.d.ts +27 -2
- package/dist/next/index.js +118 -41
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +117 -41
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +15 -4
- package/dist/react/index.d.ts +15 -4
- package/dist/react/index.js +112 -17
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +112 -17
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -82,6 +82,7 @@ var STORAGE_PREFIX = "nihplod_sso_";
|
|
|
82
82
|
var TOKEN_KEY = "token";
|
|
83
83
|
var VERIFIER_KEY_PREFIX = "pkce_verifier_";
|
|
84
84
|
var STATE_KEY = "oauth_state";
|
|
85
|
+
var NONCE_KEY = "oidc_nonce";
|
|
85
86
|
var RETURN_URL_KEY = "return_url";
|
|
86
87
|
var LOGOUT_STATE_KEY = "logout_state";
|
|
87
88
|
function buildKey(base, clientId) {
|
|
@@ -193,6 +194,15 @@ function getOAuthState(clientId) {
|
|
|
193
194
|
function removeOAuthState(clientId) {
|
|
194
195
|
_transient.remove(buildKey(STATE_KEY, clientId));
|
|
195
196
|
}
|
|
197
|
+
function saveOAuthNonce(nonce, clientId) {
|
|
198
|
+
_transient.set(buildKey(NONCE_KEY, clientId), nonce);
|
|
199
|
+
}
|
|
200
|
+
function getOAuthNonce(clientId) {
|
|
201
|
+
return _transient.get(buildKey(NONCE_KEY, clientId));
|
|
202
|
+
}
|
|
203
|
+
function removeOAuthNonce(clientId) {
|
|
204
|
+
_transient.remove(buildKey(NONCE_KEY, clientId));
|
|
205
|
+
}
|
|
196
206
|
function saveLogoutState(state, clientId) {
|
|
197
207
|
_transient.set(buildKey(LOGOUT_STATE_KEY, clientId), state);
|
|
198
208
|
}
|
|
@@ -215,6 +225,7 @@ function clearAllSsoData(clientId) {
|
|
|
215
225
|
if (clientId) {
|
|
216
226
|
removeTokenData(clientId);
|
|
217
227
|
removeOAuthState(clientId);
|
|
228
|
+
removeOAuthNonce(clientId);
|
|
218
229
|
removeReturnUrl(clientId);
|
|
219
230
|
removeLogoutState(clientId);
|
|
220
231
|
removePkceVerifier(clientId);
|
|
@@ -223,6 +234,7 @@ function clearAllSsoData(clientId) {
|
|
|
223
234
|
}
|
|
224
235
|
removeTokenData();
|
|
225
236
|
removeOAuthState();
|
|
237
|
+
removeOAuthNonce();
|
|
226
238
|
removeReturnUrl();
|
|
227
239
|
removeLogoutState();
|
|
228
240
|
const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;
|
|
@@ -313,18 +325,21 @@ function normalizeIssuer(url) {
|
|
|
313
325
|
var cachedJwks = null;
|
|
314
326
|
var cachedDiscovery = null;
|
|
315
327
|
var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
328
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
316
329
|
async function fetchDiscoveryDoc(baseUrl) {
|
|
317
330
|
const now = Date.now();
|
|
318
331
|
if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
319
332
|
return cachedDiscovery.doc;
|
|
320
333
|
}
|
|
321
334
|
try {
|
|
322
|
-
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration
|
|
323
|
-
|
|
335
|
+
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
336
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
337
|
+
});
|
|
338
|
+
if (!res.ok) return null;
|
|
339
|
+
const doc = await res.json();
|
|
324
340
|
cachedDiscovery = { baseUrl, doc, fetchedAt: now };
|
|
325
341
|
return doc;
|
|
326
342
|
} catch {
|
|
327
|
-
cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
|
|
328
343
|
return null;
|
|
329
344
|
}
|
|
330
345
|
}
|
|
@@ -336,7 +351,10 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
336
351
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
337
352
|
const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
|
|
338
353
|
try {
|
|
339
|
-
const res = await fetch(jwksUri,
|
|
354
|
+
const res = await fetch(jwksUri, {
|
|
355
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
356
|
+
...options.forceRefresh ? { cache: "no-cache" } : {}
|
|
357
|
+
});
|
|
340
358
|
if (!res.ok) return null;
|
|
341
359
|
const jwks = await res.json();
|
|
342
360
|
cachedJwks = { baseUrl, jwks, fetchedAt: now };
|
|
@@ -383,7 +401,7 @@ async function computeAtHash(accessToken) {
|
|
|
383
401
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
384
402
|
}
|
|
385
403
|
async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
|
|
386
|
-
const { rejectHs256WhenRs256Available = true } = options;
|
|
404
|
+
const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
|
|
387
405
|
const header = decodeJwtHeader(idToken);
|
|
388
406
|
if (!header) {
|
|
389
407
|
throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
|
|
@@ -473,6 +491,12 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
473
491
|
throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
|
|
474
492
|
}
|
|
475
493
|
}
|
|
494
|
+
if (expectedNonce !== void 0) {
|
|
495
|
+
const tokenNonce = typeof payload.nonce === "string" ? payload.nonce : "";
|
|
496
|
+
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
497
|
+
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
476
500
|
return { sub: payload.sub };
|
|
477
501
|
}
|
|
478
502
|
|
|
@@ -487,6 +511,7 @@ var _SsoClient = class _SsoClient {
|
|
|
487
511
|
if (!config.ssoBaseUrl) throw new SsoError("invalid_config", "ssoBaseUrl \u4E0D\u80FD\u4E3A\u7A7A");
|
|
488
512
|
const base = config.ssoBaseUrl.replace(/\/+$/, "");
|
|
489
513
|
this.config = { ...config, ssoBaseUrl: base };
|
|
514
|
+
this._serverBase = (config.serverBaseUrl ?? base).replace(/\/+$/, "");
|
|
490
515
|
}
|
|
491
516
|
// ============================================
|
|
492
517
|
// 内部方法
|
|
@@ -502,7 +527,7 @@ var _SsoClient = class _SsoClient {
|
|
|
502
527
|
if (this._discovery && now - this._discoveryFetchedAt < _SsoClient.DISCOVERY_TTL_MS) {
|
|
503
528
|
return this._discovery;
|
|
504
529
|
}
|
|
505
|
-
const url = `${this.
|
|
530
|
+
const url = `${this._serverBase}/api/oauth/.well-known/openid-configuration`;
|
|
506
531
|
try {
|
|
507
532
|
const controller = new AbortController();
|
|
508
533
|
const timeoutId = setTimeout(
|
|
@@ -533,14 +558,16 @@ var _SsoClient = class _SsoClient {
|
|
|
533
558
|
if (d) return d.authorization_endpoint;
|
|
534
559
|
return `${this.config.ssoBaseUrl}/api/oauth/authorize`;
|
|
535
560
|
}
|
|
536
|
-
/** 获取 token 端点 URL
|
|
561
|
+
/** 获取 token 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */
|
|
537
562
|
async _getTokenEndpoint() {
|
|
563
|
+
if (this.config.serverBaseUrl) return `${this._serverBase}/api/oauth/token`;
|
|
538
564
|
const d = await this._getDiscovery();
|
|
539
565
|
if (d) return d.token_endpoint;
|
|
540
566
|
return `${this.config.ssoBaseUrl}/api/oauth/token`;
|
|
541
567
|
}
|
|
542
|
-
/** 获取 userinfo 端点 URL
|
|
568
|
+
/** 获取 userinfo 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */
|
|
543
569
|
async _getUserinfoEndpoint() {
|
|
570
|
+
if (this.config.serverBaseUrl) return `${this._serverBase}/api/oauth/userinfo`;
|
|
544
571
|
const d = await this._getDiscovery();
|
|
545
572
|
if (d) return d.userinfo_endpoint;
|
|
546
573
|
return `${this.config.ssoBaseUrl}/api/oauth/userinfo`;
|
|
@@ -576,8 +603,10 @@ var _SsoClient = class _SsoClient {
|
|
|
576
603
|
const verifier = generateCodeVerifier();
|
|
577
604
|
const challenge = await generateCodeChallenge(verifier);
|
|
578
605
|
const state = generateState();
|
|
606
|
+
const nonce = generateState();
|
|
579
607
|
savePkceVerifier(this.config.clientId, verifier);
|
|
580
608
|
saveOAuthState(state, this.config.clientId);
|
|
609
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
581
610
|
if (returnUrl) {
|
|
582
611
|
this._saveReturnUrlIfTrusted(returnUrl);
|
|
583
612
|
}
|
|
@@ -588,6 +617,7 @@ var _SsoClient = class _SsoClient {
|
|
|
588
617
|
params.set("redirect_uri", this.config.redirectUri);
|
|
589
618
|
params.set("scope", this.config.scopes || "openid profile");
|
|
590
619
|
params.set("state", state);
|
|
620
|
+
params.set("nonce", nonce);
|
|
591
621
|
params.set("code_challenge", challenge);
|
|
592
622
|
params.set("code_challenge_method", "S256");
|
|
593
623
|
window.location.href = `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -605,8 +635,10 @@ var _SsoClient = class _SsoClient {
|
|
|
605
635
|
const verifier = generateCodeVerifier();
|
|
606
636
|
const challenge = await generateCodeChallenge(verifier);
|
|
607
637
|
const state = generateState();
|
|
638
|
+
const nonce = generateState();
|
|
608
639
|
savePkceVerifier(this.config.clientId, verifier);
|
|
609
640
|
saveOAuthState(state, this.config.clientId);
|
|
641
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
610
642
|
if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);
|
|
611
643
|
const authorizeEndpoint = await this._getAuthorizeEndpoint();
|
|
612
644
|
const params = new URLSearchParams();
|
|
@@ -615,6 +647,7 @@ var _SsoClient = class _SsoClient {
|
|
|
615
647
|
params.set("redirect_uri", this.config.redirectUri);
|
|
616
648
|
params.set("scope", this.config.scopes || "openid profile");
|
|
617
649
|
params.set("state", state);
|
|
650
|
+
params.set("nonce", nonce);
|
|
618
651
|
params.set("code_challenge", challenge);
|
|
619
652
|
params.set("code_challenge_method", "S256");
|
|
620
653
|
return `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -656,9 +689,11 @@ var _SsoClient = class _SsoClient {
|
|
|
656
689
|
const verifier = generateCodeVerifier();
|
|
657
690
|
const challenge = await generateCodeChallenge(verifier);
|
|
658
691
|
const state = generateState();
|
|
692
|
+
const nonce = generateState();
|
|
659
693
|
const popupNonce = generateState();
|
|
660
694
|
savePkceVerifier(this.config.clientId, verifier);
|
|
661
695
|
saveOAuthState(state, this.config.clientId);
|
|
696
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
662
697
|
savePkceVerifier(`${this.config.clientId}_popup_nonce`, popupNonce);
|
|
663
698
|
if (options.returnUrl) {
|
|
664
699
|
this._saveReturnUrlIfTrusted(options.returnUrl);
|
|
@@ -670,6 +705,7 @@ var _SsoClient = class _SsoClient {
|
|
|
670
705
|
params.set("redirect_uri", this.config.redirectUri);
|
|
671
706
|
params.set("scope", this.config.scopes || "openid profile");
|
|
672
707
|
params.set("state", state);
|
|
708
|
+
params.set("nonce", nonce);
|
|
673
709
|
params.set("code_challenge", challenge);
|
|
674
710
|
params.set("code_challenge_method", "S256");
|
|
675
711
|
params.set("popup_nonce", popupNonce);
|
|
@@ -694,6 +730,7 @@ var _SsoClient = class _SsoClient {
|
|
|
694
730
|
if (!popup) {
|
|
695
731
|
removePkceVerifier(this.config.clientId);
|
|
696
732
|
removeOAuthState(this.config.clientId);
|
|
733
|
+
removeOAuthNonce(this.config.clientId);
|
|
697
734
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
698
735
|
throw new SsoError(
|
|
699
736
|
"popup_blocked",
|
|
@@ -713,6 +750,13 @@ var _SsoClient = class _SsoClient {
|
|
|
713
750
|
const savedNonce = getPkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
714
751
|
if (!savedNonce || !timingSafeEqualString(event.data.nonce ?? "", savedNonce)) return;
|
|
715
752
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
753
|
+
try {
|
|
754
|
+
event.source?.postMessage(
|
|
755
|
+
{ type: "nihplod_sso_popup_ack", nonce: savedNonce },
|
|
756
|
+
event.origin
|
|
757
|
+
);
|
|
758
|
+
} catch {
|
|
759
|
+
}
|
|
716
760
|
completed = true;
|
|
717
761
|
cleanup();
|
|
718
762
|
if (popup && !popup.closed) {
|
|
@@ -744,6 +788,7 @@ var _SsoClient = class _SsoClient {
|
|
|
744
788
|
} catch (err) {
|
|
745
789
|
removePkceVerifier(this.config.clientId);
|
|
746
790
|
removeOAuthState(this.config.clientId);
|
|
791
|
+
removeOAuthNonce(this.config.clientId);
|
|
747
792
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
748
793
|
throw err;
|
|
749
794
|
}
|
|
@@ -763,6 +808,7 @@ var _SsoClient = class _SsoClient {
|
|
|
763
808
|
const error = params.get("error");
|
|
764
809
|
if (error) {
|
|
765
810
|
removeOAuthState(this.config.clientId);
|
|
811
|
+
removeOAuthNonce(this.config.clientId);
|
|
766
812
|
removePkceVerifier(this.config.clientId);
|
|
767
813
|
const desc = params.get("error_description") || error;
|
|
768
814
|
throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
|
|
@@ -771,18 +817,21 @@ var _SsoClient = class _SsoClient {
|
|
|
771
817
|
const returnedState = params.get("state");
|
|
772
818
|
if (!code) {
|
|
773
819
|
removeOAuthState(this.config.clientId);
|
|
820
|
+
removeOAuthNonce(this.config.clientId);
|
|
774
821
|
removePkceVerifier(this.config.clientId);
|
|
775
822
|
throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
|
|
776
823
|
}
|
|
777
824
|
const savedState = getOAuthState(this.config.clientId);
|
|
778
825
|
if (!savedState || !timingSafeEqualString(savedState, returnedState ?? "")) {
|
|
779
826
|
removeOAuthState(this.config.clientId);
|
|
827
|
+
removeOAuthNonce(this.config.clientId);
|
|
780
828
|
removePkceVerifier(this.config.clientId);
|
|
781
829
|
throw new SsoError(
|
|
782
830
|
"state_mismatch",
|
|
783
831
|
"State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
|
|
784
832
|
);
|
|
785
833
|
}
|
|
834
|
+
const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
|
|
786
835
|
const verifier = getPkceVerifier(this.config.clientId);
|
|
787
836
|
if (!verifier) {
|
|
788
837
|
throw new SsoError(
|
|
@@ -829,7 +878,8 @@ var _SsoClient = class _SsoClient {
|
|
|
829
878
|
data.id_token,
|
|
830
879
|
data.access_token,
|
|
831
880
|
this.config.ssoBaseUrl,
|
|
832
|
-
this.config.clientId
|
|
881
|
+
this.config.clientId,
|
|
882
|
+
{ expectedNonce }
|
|
833
883
|
);
|
|
834
884
|
} catch (err) {
|
|
835
885
|
removeTokenData(this.config.clientId);
|
|
@@ -837,6 +887,7 @@ var _SsoClient = class _SsoClient {
|
|
|
837
887
|
}
|
|
838
888
|
}
|
|
839
889
|
removeOAuthState(this.config.clientId);
|
|
890
|
+
removeOAuthNonce(this.config.clientId);
|
|
840
891
|
removePkceVerifier(this.config.clientId);
|
|
841
892
|
const now = Date.now();
|
|
842
893
|
const tokenData = {
|
|
@@ -1017,8 +1068,8 @@ var _SsoClient = class _SsoClient {
|
|
|
1017
1068
|
clearAllSsoData(this.config.clientId);
|
|
1018
1069
|
if (refreshToken && this.config.clientId) {
|
|
1019
1070
|
try {
|
|
1020
|
-
const discovery = await this._getDiscovery();
|
|
1021
|
-
const revokeUrl = discovery?.revocation_endpoint || `${this.config.ssoBaseUrl}/api/oauth/revoke`;
|
|
1071
|
+
const discovery = this.config.serverBaseUrl ? null : await this._getDiscovery();
|
|
1072
|
+
const revokeUrl = this.config.serverBaseUrl ? `${this._serverBase}/api/oauth/revoke` : discovery?.revocation_endpoint || `${this.config.ssoBaseUrl}/api/oauth/revoke`;
|
|
1022
1073
|
const revokeBody = new URLSearchParams({
|
|
1023
1074
|
token: refreshToken,
|
|
1024
1075
|
token_type_hint: "refresh_token",
|