@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/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Build the login URL string without redirecting. Returns `Promise<string>`.
|
|
|
66
66
|
|
|
67
67
|
### `sso.handleCallback(callbackUrl)`
|
|
68
68
|
|
|
69
|
-
Handle the OAuth callback. Parses `code` and `state` from the URL, validates state, and exchanges the code for tokens.
|
|
69
|
+
Handle the OAuth callback. Parses `code` and `state` from the URL, validates state, and exchanges the code for tokens. When an `id_token` is present, the SDK verifies its signature, issuer, audience, expiry and `at_hash`, and — when the login was initiated via `login()` / `getLoginUrl()` / `loginPopup()` — also validates the OIDC `nonce` claim against the value generated at login time (constant-time comparison, fail-closed, protecting against ID Token replay).
|
|
70
70
|
|
|
71
71
|
| Parameter | Type | Description |
|
|
72
72
|
|------|------|------|
|
|
@@ -365,7 +365,8 @@ const handler = createLogoutRouteHandler({
|
|
|
365
365
|
});
|
|
366
366
|
|
|
367
367
|
// Prefer POST to trigger logout (prevents logout CSRF via cross-site GET);
|
|
368
|
-
// GET
|
|
368
|
+
// a GET without a valid logout state does NOT log out — it returns a
|
|
369
|
+
// confirmation page (HTML) whose form POSTs to the same endpoint.
|
|
369
370
|
export const GET = handler;
|
|
370
371
|
export const POST = handler;
|
|
371
372
|
```
|
|
@@ -378,7 +379,7 @@ Trigger the logout endpoint with a POST request (recommended):
|
|
|
378
379
|
</button>
|
|
379
380
|
```
|
|
380
381
|
|
|
381
|
-
A plain `<a href="/api/auth/logout">`
|
|
382
|
+
A plain `<a href="/api/auth/logout">` navigation (GET) renders the built-in confirmation page instead of logging out directly, so cross-site image/prefetch requests can no longer trigger a logout (logout CSRF protection). Only a POST — or a GET carrying a valid RP-Initiated Logout `state` — performs the logout.
|
|
382
383
|
|
|
383
384
|
### Cookie Configuration
|
|
384
385
|
|
|
@@ -389,6 +390,7 @@ Default cookie names:
|
|
|
389
390
|
| access_token | `__Host-nihplod_sso_at` | Requires Secure + Path=/ + no Domain |
|
|
390
391
|
| refresh_token | `__Host-nihplod_sso_rt` | Requires Secure + Path=/ + no Domain |
|
|
391
392
|
| state | `__Host-nihplod_sso_state` | Requires Secure + Path=/ + no Domain |
|
|
393
|
+
| nonce | `__Host-nihplod_sso_nonce` | OIDC nonce for ID Token replay protection (validated fail-closed in the callback); Requires Secure + Path=/ + no Domain |
|
|
392
394
|
| return_url | `__Host-nihplod_sso_return` | Requires Secure + Path=/ + no Domain |
|
|
393
395
|
| verifier | `__Secure-nihplod_sso_verifier` | Requires Secure + no Domain; Path is the callback path, therefore uses `__Secure-` prefix |
|
|
394
396
|
|
|
@@ -402,7 +404,7 @@ By default, the SDK stores tokens in **sessionStorage** (tab-scoped persistence)
|
|
|
402
404
|
|
|
403
405
|
> ⚠️ XSS note: any token readable by JavaScript can be stolen by XSS. sessionStorage narrows the exposure compared with `localStorage` (tab-scoped, auto-cleared), but a `refresh_token` readable by JS is still exfiltratable while the tab is open. If you need stronger guarantees, use the Next.js BFF pattern below (tokens in `httpOnly` cookies) or keep the refresh token inside a Service Worker.
|
|
404
406
|
|
|
405
|
-
Transient OAuth data (PKCE `code_verifier`, `state`, `returnUrl`, popup nonce) is stored separately in **sessionStorage**, because it must survive the full-page redirect to the SSO center and back; it is cleared automatically when the tab closes. In SSR environments without `sessionStorage`, it falls back to an in-memory map.
|
|
407
|
+
Transient OAuth data (PKCE `code_verifier`, `state`, OIDC `nonce`, `returnUrl`, popup nonce) is stored separately in **sessionStorage**, because it must survive the full-page redirect to the SSO center and back; it is cleared automatically when the tab closes. In SSR environments without `sessionStorage`, it falls back to an in-memory map.
|
|
406
408
|
|
|
407
409
|
If the sub-project is a **Next.js BFF / Confidential Client**, you can store tokens in `localStorage` for multi-tab sharing:
|
|
408
410
|
|
package/dist/index.d.mts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 整页跳转与刷新后登录态保留,关闭标签页自动清除;SSR / 隐私模式写入失败时
|
|
8
8
|
* 降级为内存 Map。对需要多 Tab 共享 token 或 BFF/Confidential Client 场景,
|
|
9
9
|
* 可通过 setTokenStorage() 注入 localStorage 实现(如 createSecureStorage({ persist: true }))。
|
|
10
|
-
* - 临时数据(PKCE verifier / state / returnUrl / popup nonce):必须跨整页重定向存活
|
|
10
|
+
* - 临时数据(PKCE verifier / state / OIDC nonce / returnUrl / popup nonce):必须跨整页重定向存活
|
|
11
11
|
* (login() 会 302 跳转到 SSO 中心再回来),因此默认写入 sessionStorage;
|
|
12
12
|
* SSR 等无 sessionStorage 环境自动降级为内存 Map。
|
|
13
13
|
*
|
|
@@ -359,7 +359,7 @@ declare function generateState(): string;
|
|
|
359
359
|
* SSO SDK 错误类型
|
|
360
360
|
*/
|
|
361
361
|
/** SSO 错误码 */
|
|
362
|
-
type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch";
|
|
362
|
+
type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch" | "id_token_nonce_mismatch";
|
|
363
363
|
/**
|
|
364
364
|
* 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode
|
|
365
365
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 整页跳转与刷新后登录态保留,关闭标签页自动清除;SSR / 隐私模式写入失败时
|
|
8
8
|
* 降级为内存 Map。对需要多 Tab 共享 token 或 BFF/Confidential Client 场景,
|
|
9
9
|
* 可通过 setTokenStorage() 注入 localStorage 实现(如 createSecureStorage({ persist: true }))。
|
|
10
|
-
* - 临时数据(PKCE verifier / state / returnUrl / popup nonce):必须跨整页重定向存活
|
|
10
|
+
* - 临时数据(PKCE verifier / state / OIDC nonce / returnUrl / popup nonce):必须跨整页重定向存活
|
|
11
11
|
* (login() 会 302 跳转到 SSO 中心再回来),因此默认写入 sessionStorage;
|
|
12
12
|
* SSR 等无 sessionStorage 环境自动降级为内存 Map。
|
|
13
13
|
*
|
|
@@ -359,7 +359,7 @@ declare function generateState(): string;
|
|
|
359
359
|
* SSO SDK 错误类型
|
|
360
360
|
*/
|
|
361
361
|
/** SSO 错误码 */
|
|
362
|
-
type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch";
|
|
362
|
+
type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch" | "id_token_nonce_mismatch";
|
|
363
363
|
/**
|
|
364
364
|
* 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode
|
|
365
365
|
*
|
package/dist/index.js
CHANGED
|
@@ -136,6 +136,7 @@ var STORAGE_PREFIX = "nihplod_sso_";
|
|
|
136
136
|
var TOKEN_KEY = "token";
|
|
137
137
|
var VERIFIER_KEY_PREFIX = "pkce_verifier_";
|
|
138
138
|
var STATE_KEY = "oauth_state";
|
|
139
|
+
var NONCE_KEY = "oidc_nonce";
|
|
139
140
|
var RETURN_URL_KEY = "return_url";
|
|
140
141
|
var LOGOUT_STATE_KEY = "logout_state";
|
|
141
142
|
function buildKey(base, clientId) {
|
|
@@ -247,6 +248,15 @@ function getOAuthState(clientId) {
|
|
|
247
248
|
function removeOAuthState(clientId) {
|
|
248
249
|
_transient.remove(buildKey(STATE_KEY, clientId));
|
|
249
250
|
}
|
|
251
|
+
function saveOAuthNonce(nonce, clientId) {
|
|
252
|
+
_transient.set(buildKey(NONCE_KEY, clientId), nonce);
|
|
253
|
+
}
|
|
254
|
+
function getOAuthNonce(clientId) {
|
|
255
|
+
return _transient.get(buildKey(NONCE_KEY, clientId));
|
|
256
|
+
}
|
|
257
|
+
function removeOAuthNonce(clientId) {
|
|
258
|
+
_transient.remove(buildKey(NONCE_KEY, clientId));
|
|
259
|
+
}
|
|
250
260
|
function saveLogoutState(state, clientId) {
|
|
251
261
|
_transient.set(buildKey(LOGOUT_STATE_KEY, clientId), state);
|
|
252
262
|
}
|
|
@@ -269,6 +279,7 @@ function clearAllSsoData(clientId) {
|
|
|
269
279
|
if (clientId) {
|
|
270
280
|
removeTokenData(clientId);
|
|
271
281
|
removeOAuthState(clientId);
|
|
282
|
+
removeOAuthNonce(clientId);
|
|
272
283
|
removeReturnUrl(clientId);
|
|
273
284
|
removeLogoutState(clientId);
|
|
274
285
|
removePkceVerifier(clientId);
|
|
@@ -277,6 +288,7 @@ function clearAllSsoData(clientId) {
|
|
|
277
288
|
}
|
|
278
289
|
removeTokenData();
|
|
279
290
|
removeOAuthState();
|
|
291
|
+
removeOAuthNonce();
|
|
280
292
|
removeReturnUrl();
|
|
281
293
|
removeLogoutState();
|
|
282
294
|
const prefix = STORAGE_PREFIX + VERIFIER_KEY_PREFIX;
|
|
@@ -367,18 +379,21 @@ function normalizeIssuer(url) {
|
|
|
367
379
|
var cachedJwks = null;
|
|
368
380
|
var cachedDiscovery = null;
|
|
369
381
|
var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
382
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
370
383
|
async function fetchDiscoveryDoc(baseUrl) {
|
|
371
384
|
const now = Date.now();
|
|
372
385
|
if (cachedDiscovery && cachedDiscovery.baseUrl === baseUrl && now - cachedDiscovery.fetchedAt < JWKS_CACHE_TTL_MS) {
|
|
373
386
|
return cachedDiscovery.doc;
|
|
374
387
|
}
|
|
375
388
|
try {
|
|
376
|
-
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration
|
|
377
|
-
|
|
389
|
+
const res = await fetch(`${baseUrl}/api/oauth/.well-known/openid-configuration`, {
|
|
390
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
391
|
+
});
|
|
392
|
+
if (!res.ok) return null;
|
|
393
|
+
const doc = await res.json();
|
|
378
394
|
cachedDiscovery = { baseUrl, doc, fetchedAt: now };
|
|
379
395
|
return doc;
|
|
380
396
|
} catch {
|
|
381
|
-
cachedDiscovery = { baseUrl, doc: null, fetchedAt: now };
|
|
382
397
|
return null;
|
|
383
398
|
}
|
|
384
399
|
}
|
|
@@ -390,7 +405,10 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
390
405
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
391
406
|
const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
|
|
392
407
|
try {
|
|
393
|
-
const res = await fetch(jwksUri,
|
|
408
|
+
const res = await fetch(jwksUri, {
|
|
409
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
410
|
+
...options.forceRefresh ? { cache: "no-cache" } : {}
|
|
411
|
+
});
|
|
394
412
|
if (!res.ok) return null;
|
|
395
413
|
const jwks = await res.json();
|
|
396
414
|
cachedJwks = { baseUrl, jwks, fetchedAt: now };
|
|
@@ -437,7 +455,7 @@ async function computeAtHash(accessToken) {
|
|
|
437
455
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
438
456
|
}
|
|
439
457
|
async function validateIdToken(idToken, accessToken, expectedIssuer, expectedClientId, options = {}) {
|
|
440
|
-
const { rejectHs256WhenRs256Available = true } = options;
|
|
458
|
+
const { rejectHs256WhenRs256Available = true, expectedNonce } = options;
|
|
441
459
|
const header = decodeJwtHeader(idToken);
|
|
442
460
|
if (!header) {
|
|
443
461
|
throw new SsoError("id_token_invalid", "ID Token \u683C\u5F0F\u9519\u8BEF");
|
|
@@ -527,6 +545,12 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
527
545
|
throw new SsoError("id_token_at_hash_mismatch", "ID Token at_hash \u4E0D\u5339\u914D");
|
|
528
546
|
}
|
|
529
547
|
}
|
|
548
|
+
if (expectedNonce !== void 0) {
|
|
549
|
+
const tokenNonce = typeof payload.nonce === "string" ? payload.nonce : "";
|
|
550
|
+
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
551
|
+
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
530
554
|
return { sub: payload.sub };
|
|
531
555
|
}
|
|
532
556
|
|
|
@@ -633,8 +657,10 @@ var _SsoClient = class _SsoClient {
|
|
|
633
657
|
const verifier = generateCodeVerifier();
|
|
634
658
|
const challenge = await generateCodeChallenge(verifier);
|
|
635
659
|
const state = generateState();
|
|
660
|
+
const nonce = generateState();
|
|
636
661
|
savePkceVerifier(this.config.clientId, verifier);
|
|
637
662
|
saveOAuthState(state, this.config.clientId);
|
|
663
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
638
664
|
if (returnUrl) {
|
|
639
665
|
this._saveReturnUrlIfTrusted(returnUrl);
|
|
640
666
|
}
|
|
@@ -645,6 +671,7 @@ var _SsoClient = class _SsoClient {
|
|
|
645
671
|
params.set("redirect_uri", this.config.redirectUri);
|
|
646
672
|
params.set("scope", this.config.scopes || "openid profile");
|
|
647
673
|
params.set("state", state);
|
|
674
|
+
params.set("nonce", nonce);
|
|
648
675
|
params.set("code_challenge", challenge);
|
|
649
676
|
params.set("code_challenge_method", "S256");
|
|
650
677
|
window.location.href = `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -662,8 +689,10 @@ var _SsoClient = class _SsoClient {
|
|
|
662
689
|
const verifier = generateCodeVerifier();
|
|
663
690
|
const challenge = await generateCodeChallenge(verifier);
|
|
664
691
|
const state = generateState();
|
|
692
|
+
const nonce = generateState();
|
|
665
693
|
savePkceVerifier(this.config.clientId, verifier);
|
|
666
694
|
saveOAuthState(state, this.config.clientId);
|
|
695
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
667
696
|
if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);
|
|
668
697
|
const authorizeEndpoint = await this._getAuthorizeEndpoint();
|
|
669
698
|
const params = new URLSearchParams();
|
|
@@ -672,6 +701,7 @@ var _SsoClient = class _SsoClient {
|
|
|
672
701
|
params.set("redirect_uri", this.config.redirectUri);
|
|
673
702
|
params.set("scope", this.config.scopes || "openid profile");
|
|
674
703
|
params.set("state", state);
|
|
704
|
+
params.set("nonce", nonce);
|
|
675
705
|
params.set("code_challenge", challenge);
|
|
676
706
|
params.set("code_challenge_method", "S256");
|
|
677
707
|
return `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -713,9 +743,11 @@ var _SsoClient = class _SsoClient {
|
|
|
713
743
|
const verifier = generateCodeVerifier();
|
|
714
744
|
const challenge = await generateCodeChallenge(verifier);
|
|
715
745
|
const state = generateState();
|
|
746
|
+
const nonce = generateState();
|
|
716
747
|
const popupNonce = generateState();
|
|
717
748
|
savePkceVerifier(this.config.clientId, verifier);
|
|
718
749
|
saveOAuthState(state, this.config.clientId);
|
|
750
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
719
751
|
savePkceVerifier(`${this.config.clientId}_popup_nonce`, popupNonce);
|
|
720
752
|
if (options.returnUrl) {
|
|
721
753
|
this._saveReturnUrlIfTrusted(options.returnUrl);
|
|
@@ -727,6 +759,7 @@ var _SsoClient = class _SsoClient {
|
|
|
727
759
|
params.set("redirect_uri", this.config.redirectUri);
|
|
728
760
|
params.set("scope", this.config.scopes || "openid profile");
|
|
729
761
|
params.set("state", state);
|
|
762
|
+
params.set("nonce", nonce);
|
|
730
763
|
params.set("code_challenge", challenge);
|
|
731
764
|
params.set("code_challenge_method", "S256");
|
|
732
765
|
params.set("popup_nonce", popupNonce);
|
|
@@ -751,6 +784,7 @@ var _SsoClient = class _SsoClient {
|
|
|
751
784
|
if (!popup) {
|
|
752
785
|
removePkceVerifier(this.config.clientId);
|
|
753
786
|
removeOAuthState(this.config.clientId);
|
|
787
|
+
removeOAuthNonce(this.config.clientId);
|
|
754
788
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
755
789
|
throw new SsoError(
|
|
756
790
|
"popup_blocked",
|
|
@@ -770,6 +804,13 @@ var _SsoClient = class _SsoClient {
|
|
|
770
804
|
const savedNonce = getPkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
771
805
|
if (!savedNonce || !timingSafeEqualString(event.data.nonce ?? "", savedNonce)) return;
|
|
772
806
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
807
|
+
try {
|
|
808
|
+
event.source?.postMessage(
|
|
809
|
+
{ type: "nihplod_sso_popup_ack", nonce: savedNonce },
|
|
810
|
+
event.origin
|
|
811
|
+
);
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
773
814
|
completed = true;
|
|
774
815
|
cleanup();
|
|
775
816
|
if (popup && !popup.closed) {
|
|
@@ -801,6 +842,7 @@ var _SsoClient = class _SsoClient {
|
|
|
801
842
|
} catch (err) {
|
|
802
843
|
removePkceVerifier(this.config.clientId);
|
|
803
844
|
removeOAuthState(this.config.clientId);
|
|
845
|
+
removeOAuthNonce(this.config.clientId);
|
|
804
846
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
805
847
|
throw err;
|
|
806
848
|
}
|
|
@@ -820,6 +862,7 @@ var _SsoClient = class _SsoClient {
|
|
|
820
862
|
const error = params.get("error");
|
|
821
863
|
if (error) {
|
|
822
864
|
removeOAuthState(this.config.clientId);
|
|
865
|
+
removeOAuthNonce(this.config.clientId);
|
|
823
866
|
removePkceVerifier(this.config.clientId);
|
|
824
867
|
const desc = params.get("error_description") || error;
|
|
825
868
|
throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
|
|
@@ -828,18 +871,21 @@ var _SsoClient = class _SsoClient {
|
|
|
828
871
|
const returnedState = params.get("state");
|
|
829
872
|
if (!code) {
|
|
830
873
|
removeOAuthState(this.config.clientId);
|
|
874
|
+
removeOAuthNonce(this.config.clientId);
|
|
831
875
|
removePkceVerifier(this.config.clientId);
|
|
832
876
|
throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
|
|
833
877
|
}
|
|
834
878
|
const savedState = getOAuthState(this.config.clientId);
|
|
835
879
|
if (!savedState || !timingSafeEqualString(savedState, returnedState ?? "")) {
|
|
836
880
|
removeOAuthState(this.config.clientId);
|
|
881
|
+
removeOAuthNonce(this.config.clientId);
|
|
837
882
|
removePkceVerifier(this.config.clientId);
|
|
838
883
|
throw new SsoError(
|
|
839
884
|
"state_mismatch",
|
|
840
885
|
"State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
|
|
841
886
|
);
|
|
842
887
|
}
|
|
888
|
+
const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
|
|
843
889
|
const verifier = getPkceVerifier(this.config.clientId);
|
|
844
890
|
if (!verifier) {
|
|
845
891
|
throw new SsoError(
|
|
@@ -886,7 +932,8 @@ var _SsoClient = class _SsoClient {
|
|
|
886
932
|
data.id_token,
|
|
887
933
|
data.access_token,
|
|
888
934
|
this.config.ssoBaseUrl,
|
|
889
|
-
this.config.clientId
|
|
935
|
+
this.config.clientId,
|
|
936
|
+
{ expectedNonce }
|
|
890
937
|
);
|
|
891
938
|
} catch (err) {
|
|
892
939
|
removeTokenData(this.config.clientId);
|
|
@@ -894,6 +941,7 @@ var _SsoClient = class _SsoClient {
|
|
|
894
941
|
}
|
|
895
942
|
}
|
|
896
943
|
removeOAuthState(this.config.clientId);
|
|
944
|
+
removeOAuthNonce(this.config.clientId);
|
|
897
945
|
removePkceVerifier(this.config.clientId);
|
|
898
946
|
const now = Date.now();
|
|
899
947
|
const tokenData = {
|