@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/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
- const doc = res.ok ? await res.json() : null;
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, options.forceRefresh ? { cache: "no-cache" } : void 0);
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 = {