@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/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
|
|
|
@@ -579,8 +603,10 @@ var _SsoClient = class _SsoClient {
|
|
|
579
603
|
const verifier = generateCodeVerifier();
|
|
580
604
|
const challenge = await generateCodeChallenge(verifier);
|
|
581
605
|
const state = generateState();
|
|
606
|
+
const nonce = generateState();
|
|
582
607
|
savePkceVerifier(this.config.clientId, verifier);
|
|
583
608
|
saveOAuthState(state, this.config.clientId);
|
|
609
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
584
610
|
if (returnUrl) {
|
|
585
611
|
this._saveReturnUrlIfTrusted(returnUrl);
|
|
586
612
|
}
|
|
@@ -591,6 +617,7 @@ var _SsoClient = class _SsoClient {
|
|
|
591
617
|
params.set("redirect_uri", this.config.redirectUri);
|
|
592
618
|
params.set("scope", this.config.scopes || "openid profile");
|
|
593
619
|
params.set("state", state);
|
|
620
|
+
params.set("nonce", nonce);
|
|
594
621
|
params.set("code_challenge", challenge);
|
|
595
622
|
params.set("code_challenge_method", "S256");
|
|
596
623
|
window.location.href = `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -608,8 +635,10 @@ var _SsoClient = class _SsoClient {
|
|
|
608
635
|
const verifier = generateCodeVerifier();
|
|
609
636
|
const challenge = await generateCodeChallenge(verifier);
|
|
610
637
|
const state = generateState();
|
|
638
|
+
const nonce = generateState();
|
|
611
639
|
savePkceVerifier(this.config.clientId, verifier);
|
|
612
640
|
saveOAuthState(state, this.config.clientId);
|
|
641
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
613
642
|
if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);
|
|
614
643
|
const authorizeEndpoint = await this._getAuthorizeEndpoint();
|
|
615
644
|
const params = new URLSearchParams();
|
|
@@ -618,6 +647,7 @@ var _SsoClient = class _SsoClient {
|
|
|
618
647
|
params.set("redirect_uri", this.config.redirectUri);
|
|
619
648
|
params.set("scope", this.config.scopes || "openid profile");
|
|
620
649
|
params.set("state", state);
|
|
650
|
+
params.set("nonce", nonce);
|
|
621
651
|
params.set("code_challenge", challenge);
|
|
622
652
|
params.set("code_challenge_method", "S256");
|
|
623
653
|
return `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -659,9 +689,11 @@ var _SsoClient = class _SsoClient {
|
|
|
659
689
|
const verifier = generateCodeVerifier();
|
|
660
690
|
const challenge = await generateCodeChallenge(verifier);
|
|
661
691
|
const state = generateState();
|
|
692
|
+
const nonce = generateState();
|
|
662
693
|
const popupNonce = generateState();
|
|
663
694
|
savePkceVerifier(this.config.clientId, verifier);
|
|
664
695
|
saveOAuthState(state, this.config.clientId);
|
|
696
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
665
697
|
savePkceVerifier(`${this.config.clientId}_popup_nonce`, popupNonce);
|
|
666
698
|
if (options.returnUrl) {
|
|
667
699
|
this._saveReturnUrlIfTrusted(options.returnUrl);
|
|
@@ -673,6 +705,7 @@ var _SsoClient = class _SsoClient {
|
|
|
673
705
|
params.set("redirect_uri", this.config.redirectUri);
|
|
674
706
|
params.set("scope", this.config.scopes || "openid profile");
|
|
675
707
|
params.set("state", state);
|
|
708
|
+
params.set("nonce", nonce);
|
|
676
709
|
params.set("code_challenge", challenge);
|
|
677
710
|
params.set("code_challenge_method", "S256");
|
|
678
711
|
params.set("popup_nonce", popupNonce);
|
|
@@ -697,6 +730,7 @@ var _SsoClient = class _SsoClient {
|
|
|
697
730
|
if (!popup) {
|
|
698
731
|
removePkceVerifier(this.config.clientId);
|
|
699
732
|
removeOAuthState(this.config.clientId);
|
|
733
|
+
removeOAuthNonce(this.config.clientId);
|
|
700
734
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
701
735
|
throw new SsoError(
|
|
702
736
|
"popup_blocked",
|
|
@@ -716,6 +750,13 @@ var _SsoClient = class _SsoClient {
|
|
|
716
750
|
const savedNonce = getPkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
717
751
|
if (!savedNonce || !timingSafeEqualString(event.data.nonce ?? "", savedNonce)) return;
|
|
718
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
|
+
}
|
|
719
760
|
completed = true;
|
|
720
761
|
cleanup();
|
|
721
762
|
if (popup && !popup.closed) {
|
|
@@ -747,6 +788,7 @@ var _SsoClient = class _SsoClient {
|
|
|
747
788
|
} catch (err) {
|
|
748
789
|
removePkceVerifier(this.config.clientId);
|
|
749
790
|
removeOAuthState(this.config.clientId);
|
|
791
|
+
removeOAuthNonce(this.config.clientId);
|
|
750
792
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
751
793
|
throw err;
|
|
752
794
|
}
|
|
@@ -766,6 +808,7 @@ var _SsoClient = class _SsoClient {
|
|
|
766
808
|
const error = params.get("error");
|
|
767
809
|
if (error) {
|
|
768
810
|
removeOAuthState(this.config.clientId);
|
|
811
|
+
removeOAuthNonce(this.config.clientId);
|
|
769
812
|
removePkceVerifier(this.config.clientId);
|
|
770
813
|
const desc = params.get("error_description") || error;
|
|
771
814
|
throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
|
|
@@ -774,18 +817,21 @@ var _SsoClient = class _SsoClient {
|
|
|
774
817
|
const returnedState = params.get("state");
|
|
775
818
|
if (!code) {
|
|
776
819
|
removeOAuthState(this.config.clientId);
|
|
820
|
+
removeOAuthNonce(this.config.clientId);
|
|
777
821
|
removePkceVerifier(this.config.clientId);
|
|
778
822
|
throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
|
|
779
823
|
}
|
|
780
824
|
const savedState = getOAuthState(this.config.clientId);
|
|
781
825
|
if (!savedState || !timingSafeEqualString(savedState, returnedState ?? "")) {
|
|
782
826
|
removeOAuthState(this.config.clientId);
|
|
827
|
+
removeOAuthNonce(this.config.clientId);
|
|
783
828
|
removePkceVerifier(this.config.clientId);
|
|
784
829
|
throw new SsoError(
|
|
785
830
|
"state_mismatch",
|
|
786
831
|
"State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
|
|
787
832
|
);
|
|
788
833
|
}
|
|
834
|
+
const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
|
|
789
835
|
const verifier = getPkceVerifier(this.config.clientId);
|
|
790
836
|
if (!verifier) {
|
|
791
837
|
throw new SsoError(
|
|
@@ -832,7 +878,8 @@ var _SsoClient = class _SsoClient {
|
|
|
832
878
|
data.id_token,
|
|
833
879
|
data.access_token,
|
|
834
880
|
this.config.ssoBaseUrl,
|
|
835
|
-
this.config.clientId
|
|
881
|
+
this.config.clientId,
|
|
882
|
+
{ expectedNonce }
|
|
836
883
|
);
|
|
837
884
|
} catch (err) {
|
|
838
885
|
removeTokenData(this.config.clientId);
|
|
@@ -840,6 +887,7 @@ var _SsoClient = class _SsoClient {
|
|
|
840
887
|
}
|
|
841
888
|
}
|
|
842
889
|
removeOAuthState(this.config.clientId);
|
|
890
|
+
removeOAuthNonce(this.config.clientId);
|
|
843
891
|
removePkceVerifier(this.config.clientId);
|
|
844
892
|
const now = Date.now();
|
|
845
893
|
const tokenData = {
|