@nihplod/sso-sdk 1.5.0 → 1.7.0
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 +21 -2
- package/dist/index.d.mts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +148 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +148 -84
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +87 -2
- package/dist/next/index.d.ts +87 -2
- package/dist/next/index.js +330 -64
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +329 -64
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +32 -2
- package/dist/react/index.d.ts +32 -2
- package/dist/react/index.js +196 -105
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +196 -105
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ if (params.get("sso_probe") === "no_session") {
|
|
|
84
84
|
|
|
85
85
|
### `sso.handleCallback(callbackUrl)`
|
|
86
86
|
|
|
87
|
-
Handle the OAuth callback. Parses `code` and `state` from the URL, validates state, and exchanges the code for tokens. When the requested scope includes `openid`, the token response **must** contain an `id_token` (fail-closed: a missing `id_token` is rejected instead of silently skipping validation). The SDK verifies the ID Token's 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, protecting against ID Token replay
|
|
87
|
+
Handle the OAuth callback. Parses `code` and `state` from the URL, validates state, and exchanges the code for tokens. When the requested scope includes `openid`, the token response **must** contain an `id_token` (fail-closed: a missing `id_token` is rejected instead of silently skipping validation). The SDK verifies the ID Token's 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, protecting against ID Token replay). The check is fail-closed both ways: a stored nonce requires a matching token nonce, and (since 1.6.0) a token carrying a `nonce` claim while no expected nonce is stored locally (e.g. the nonce cookie/sessionStorage entry was evicted) is rejected as well.
|
|
88
88
|
|
|
89
89
|
| Parameter | Type | Description |
|
|
90
90
|
|------|------|------|
|
|
@@ -450,6 +450,8 @@ export const POST = createBackchannelLogoutRouteHandler({
|
|
|
450
450
|
|
|
451
451
|
The backchannel handler receives `logout_token` pushes from the SSO center (OIDC Back-Channel Logout) when the user signs out globally or revokes consent, verifies the token (RS256 signature via JWKS, issuer/audience/expiry, `events` claim, `jti` replay protection), clears the SSO cookies, and calls your `onLogout` hook. If the hook throws, it responds 500 so the IdP retries delivery. **Remember to register this route's public URL as `backchannelLogoutUri` in the SSO admin console for your client.**
|
|
452
452
|
|
|
453
|
+
> **jti replay storage (since 1.6.0):** the default replay cache is an in-memory per-instance Map. Multi-instance / serverless deployments MUST inject shared storage via the `jtiStore` option (e.g. Redis: `addIfAbsent` = `SET key 1 NX EX ttl`, `remove` = `DEL key` — `remove` lets the SDK release the jti so the IdP can redeliver after an `onLogout` failure; without it, redeliveries are rejected as replays).
|
|
454
|
+
|
|
453
455
|
Trigger the logout endpoint with a POST request (recommended):
|
|
454
456
|
|
|
455
457
|
```tsx
|
|
@@ -469,7 +471,7 @@ Default cookie names:
|
|
|
469
471
|
| access_token | `__Host-nihplod_sso_at` | Requires Secure + Path=/ + no Domain |
|
|
470
472
|
| refresh_token | `__Host-nihplod_sso_rt` | Requires Secure + Path=/ + no Domain |
|
|
471
473
|
| state | `__Host-nihplod_sso_state` | Requires Secure + Path=/ + no Domain |
|
|
472
|
-
| nonce | `__Host-nihplod_sso_nonce` | OIDC nonce for ID Token replay protection. Validated when the nonce cookie is present (cookie TTL 10 minutes);
|
|
474
|
+
| nonce | `__Host-nihplod_sso_nonce` | OIDC nonce for ID Token replay protection. Validated fail-closed when the nonce cookie is present (cookie TTL 10 minutes); since 1.6.0, an id_token that carries a `nonce` while the cookie has expired is also rejected (restart the login flow). Requires Secure + Path=/ + no Domain |
|
|
473
475
|
| id_token | `__Host-nihplod_sso_id` | ID token cookie used as `id_token_hint` for RP-Initiated Logout. Requires Secure + Path=/ + no Domain |
|
|
474
476
|
| logout_state | `__Host-nihplod_sso_logout_state` | One-time logout state for CSRF protection on the RP-Initiated Logout callback. Requires Secure + Path=/ + no Domain |
|
|
475
477
|
| return_url | `__Host-nihplod_sso_return` | Requires Secure + Path=/ + no Domain |
|
|
@@ -481,6 +483,23 @@ Default cookie names:
|
|
|
481
483
|
|
|
482
484
|
---
|
|
483
485
|
|
|
486
|
+
## Changelog
|
|
487
|
+
|
|
488
|
+
### 1.6.0
|
|
489
|
+
|
|
490
|
+
Security and correctness hardening. Migration notes:
|
|
491
|
+
|
|
492
|
+
- **ID Token must carry `iat`.** `validateIdToken` now rejects tokens without an `iat` claim (`id_token_invalid`). Any spec-compliant IdP already includes it.
|
|
493
|
+
- **Nonce check fails closed.** An `id_token` that carries a `nonce` claim while no expected nonce is stored locally (e.g. the nonce cookie/sessionStorage entry was evicted) is now rejected with `id_token_nonce_mismatch` (OIDC Core §12.2), instead of silently skipping the check. Refresh-issued ID tokens must not carry `nonce` per spec, so the refresh flow is unaffected. If login fails with this error after upgrading, restart the login flow — do not downgrade the check.
|
|
494
|
+
- **New `jtiStore` option** for `createBackchannelLogoutRouteHandler` / `verifyLogoutToken(Detailed)`. The default jti replay cache is an in-memory per-instance Map; multi-instance / serverless deployments MUST inject shared storage (e.g. Redis `SET NX EX` for `addIfAbsent`, `DEL` for `remove` — `remove` lets the SDK release the jti so the IdP can redeliver after your `onLogout` hook fails). Replay keys are now prefixed with the token issuer to prevent cross-issuer jti collisions.
|
|
495
|
+
- **New `allowCrossOriginDiscoveryEndpoints` option** on `SsoClientConfig` (default `false`). Discovery documents whose `authorization_endpoint` is not same-origin with `ssoBaseUrl`, or whose `token_endpoint` / `userinfo_endpoint` is not same-origin with `ssoBaseUrl` (or `serverBaseUrl` when configured), are rejected with a warning and the SDK falls back to the default endpoints. Set this flag only if your SSO center genuinely hosts endpoints on another origin.
|
|
496
|
+
- **Multi-tab token sync**: after a silent refresh, the new token data is broadcast to other tabs via `BroadcastChannel` and persisted directly (light shape check), so other tabs no longer attempt their own refresh with an already-rotated refresh_token. Older SDK tabs (no payload) fall back to the previous `loadUser()`-only behavior.
|
|
497
|
+
- `handleCallback` now clears the one-time transient data (state/nonce/PKCE verifier/silent-probe marker) when the token endpoint returns an error (e.g. `invalid_grant`) or when the required `id_token` is missing — the authorization code is already consumed and retrying cannot succeed. Network failures and JWKS validation failures still retain the data for retry.
|
|
498
|
+
- `getAccessToken()` / `getUserInfo()` refresh when the token has less than 10s of validity left (expiry skew), instead of returning a token that may expire in flight. `isAuthenticated()` intentionally keeps the exact check (it's a synchronous UI hint).
|
|
499
|
+
- Next.js callback: the token exchange request now has a 10s timeout (previously unbounded) and still gets one retry before the 502 error page. RP-Initiated Logout state comparison is now constant-time. The middleware never asset-bypasses paths under `/api/` (e.g. `/api/app.js` no longer skips authentication; list public API paths in `publicPaths` explicitly).
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
484
503
|
## Security Recommendations and Token Storage
|
|
485
504
|
|
|
486
505
|
> ⚠️ **The Next.js middleware is only a UX gate.** It decides whether to redirect
|
package/dist/index.d.mts
CHANGED
|
@@ -112,6 +112,17 @@ interface SsoClientConfig {
|
|
|
112
112
|
redirectUri: string;
|
|
113
113
|
/** SSO 中心地址,如 "https://nihplod.cn" */
|
|
114
114
|
ssoBaseUrl: string;
|
|
115
|
+
/**
|
|
116
|
+
* 允许 Discovery 文档声明跨源端点(默认 false)。
|
|
117
|
+
*
|
|
118
|
+
* ⚠️ 安全警告:Discovery 文档若被篡改(如中间人/反向代理劫持),攻击者可把
|
|
119
|
+
* token_endpoint 指向自己的服务器窃取 authorization code / refresh_token。
|
|
120
|
+
* SDK 默认校验 authorization_endpoint 必须与 ssoBaseUrl 同源、
|
|
121
|
+
* token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl(或 serverBaseUrl)同源,
|
|
122
|
+
* 不匹配时告警并回退到默认端点。仅当 SSO 中心确实把端点托管在其他域名时
|
|
123
|
+
* 才设置此开关,并自行确保 Discovery 通道可信。
|
|
124
|
+
*/
|
|
125
|
+
allowCrossOriginDiscoveryEndpoints?: boolean;
|
|
115
126
|
/** 请求的 scope(空格分隔),如 "openid profile phone" */
|
|
116
127
|
scopes?: string;
|
|
117
128
|
/** RP-Initiated Logout 返回地址(可选)。不传时回退到 redirectUri */
|
|
@@ -195,6 +206,8 @@ declare class SsoClient {
|
|
|
195
206
|
private _discovery;
|
|
196
207
|
private _discoveryFetchedAt;
|
|
197
208
|
private _refreshLock;
|
|
209
|
+
/** Discovery 端点 origin 校验失败只告警一次(拒绝的文档不缓存,避免每次调用刷屏) */
|
|
210
|
+
private _discoveryOriginWarned;
|
|
198
211
|
/** Discovery 文档缓存 TTL(5 分钟) */
|
|
199
212
|
private static readonly DISCOVERY_TTL_MS;
|
|
200
213
|
/** Discovery fetch 超时(10 秒) */
|
|
@@ -206,6 +219,15 @@ declare class SsoClient {
|
|
|
206
219
|
* 不会输出 token/授权码等敏感值。
|
|
207
220
|
*/
|
|
208
221
|
debugLog(...args: unknown[]): void;
|
|
222
|
+
/**
|
|
223
|
+
* 校验 Discovery 文档端点 origin(防文档被篡改后 token/code 被导向攻击者服务器):
|
|
224
|
+
* - authorization_endpoint 必须与 ssoBaseUrl 同源(浏览器跳转目标);
|
|
225
|
+
* - token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl 同源,
|
|
226
|
+
* 或(配置了 serverBaseUrl 时)与内网地址同源。
|
|
227
|
+
* 任一不匹配:告警并返回 null(上层回退到默认端点,fail-safe)。
|
|
228
|
+
* 可通过 config.allowCrossOriginDiscoveryEndpoints 关闭(见配置项警告)。
|
|
229
|
+
*/
|
|
230
|
+
private _sanitizeDiscovery;
|
|
209
231
|
/**
|
|
210
232
|
* 获取 OIDC Discovery 文档(带缓存 + 超时)
|
|
211
233
|
*
|
|
@@ -327,6 +349,9 @@ declare class SsoClient {
|
|
|
327
349
|
* 检查是否已认证(不发起网络请求)
|
|
328
350
|
*
|
|
329
351
|
* 仅检查本地是否存在未过期的 access_token。
|
|
352
|
+
* 故意不做 EXPIRY_SKEW_MS 提前量:这是同步的 UI 提示判定(是否显示"已登录"),
|
|
353
|
+
* 提前 10s 翻转 false 会让调用方在 token 仍有效时误显示未登录;真正发请求的
|
|
354
|
+
* getAccessToken/getUserInfo 已带提前量。
|
|
330
355
|
*/
|
|
331
356
|
isAuthenticated(): boolean;
|
|
332
357
|
/**
|
|
@@ -401,7 +426,7 @@ declare function generateState(): string;
|
|
|
401
426
|
* SSO SDK 错误类型
|
|
402
427
|
*/
|
|
403
428
|
/** SSO 错误码 */
|
|
404
|
-
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" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
|
|
429
|
+
type SsoErrorCode = "invalid_config" | "state_mismatch" | "issuer_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" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
|
|
405
430
|
/**
|
|
406
431
|
* 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode
|
|
407
432
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -112,6 +112,17 @@ interface SsoClientConfig {
|
|
|
112
112
|
redirectUri: string;
|
|
113
113
|
/** SSO 中心地址,如 "https://nihplod.cn" */
|
|
114
114
|
ssoBaseUrl: string;
|
|
115
|
+
/**
|
|
116
|
+
* 允许 Discovery 文档声明跨源端点(默认 false)。
|
|
117
|
+
*
|
|
118
|
+
* ⚠️ 安全警告:Discovery 文档若被篡改(如中间人/反向代理劫持),攻击者可把
|
|
119
|
+
* token_endpoint 指向自己的服务器窃取 authorization code / refresh_token。
|
|
120
|
+
* SDK 默认校验 authorization_endpoint 必须与 ssoBaseUrl 同源、
|
|
121
|
+
* token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl(或 serverBaseUrl)同源,
|
|
122
|
+
* 不匹配时告警并回退到默认端点。仅当 SSO 中心确实把端点托管在其他域名时
|
|
123
|
+
* 才设置此开关,并自行确保 Discovery 通道可信。
|
|
124
|
+
*/
|
|
125
|
+
allowCrossOriginDiscoveryEndpoints?: boolean;
|
|
115
126
|
/** 请求的 scope(空格分隔),如 "openid profile phone" */
|
|
116
127
|
scopes?: string;
|
|
117
128
|
/** RP-Initiated Logout 返回地址(可选)。不传时回退到 redirectUri */
|
|
@@ -195,6 +206,8 @@ declare class SsoClient {
|
|
|
195
206
|
private _discovery;
|
|
196
207
|
private _discoveryFetchedAt;
|
|
197
208
|
private _refreshLock;
|
|
209
|
+
/** Discovery 端点 origin 校验失败只告警一次(拒绝的文档不缓存,避免每次调用刷屏) */
|
|
210
|
+
private _discoveryOriginWarned;
|
|
198
211
|
/** Discovery 文档缓存 TTL(5 分钟) */
|
|
199
212
|
private static readonly DISCOVERY_TTL_MS;
|
|
200
213
|
/** Discovery fetch 超时(10 秒) */
|
|
@@ -206,6 +219,15 @@ declare class SsoClient {
|
|
|
206
219
|
* 不会输出 token/授权码等敏感值。
|
|
207
220
|
*/
|
|
208
221
|
debugLog(...args: unknown[]): void;
|
|
222
|
+
/**
|
|
223
|
+
* 校验 Discovery 文档端点 origin(防文档被篡改后 token/code 被导向攻击者服务器):
|
|
224
|
+
* - authorization_endpoint 必须与 ssoBaseUrl 同源(浏览器跳转目标);
|
|
225
|
+
* - token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl 同源,
|
|
226
|
+
* 或(配置了 serverBaseUrl 时)与内网地址同源。
|
|
227
|
+
* 任一不匹配:告警并返回 null(上层回退到默认端点,fail-safe)。
|
|
228
|
+
* 可通过 config.allowCrossOriginDiscoveryEndpoints 关闭(见配置项警告)。
|
|
229
|
+
*/
|
|
230
|
+
private _sanitizeDiscovery;
|
|
209
231
|
/**
|
|
210
232
|
* 获取 OIDC Discovery 文档(带缓存 + 超时)
|
|
211
233
|
*
|
|
@@ -327,6 +349,9 @@ declare class SsoClient {
|
|
|
327
349
|
* 检查是否已认证(不发起网络请求)
|
|
328
350
|
*
|
|
329
351
|
* 仅检查本地是否存在未过期的 access_token。
|
|
352
|
+
* 故意不做 EXPIRY_SKEW_MS 提前量:这是同步的 UI 提示判定(是否显示"已登录"),
|
|
353
|
+
* 提前 10s 翻转 false 会让调用方在 token 仍有效时误显示未登录;真正发请求的
|
|
354
|
+
* getAccessToken/getUserInfo 已带提前量。
|
|
330
355
|
*/
|
|
331
356
|
isAuthenticated(): boolean;
|
|
332
357
|
/**
|
|
@@ -401,7 +426,7 @@ declare function generateState(): string;
|
|
|
401
426
|
* SSO SDK 错误类型
|
|
402
427
|
*/
|
|
403
428
|
/** SSO 错误码 */
|
|
404
|
-
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" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
|
|
429
|
+
type SsoErrorCode = "invalid_config" | "state_mismatch" | "issuer_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" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
|
|
405
430
|
/**
|
|
406
431
|
* 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode
|
|
407
432
|
*
|
package/dist/index.js
CHANGED
|
@@ -143,24 +143,6 @@ var SILENT_PROBE_KEY = "silent_probe";
|
|
|
143
143
|
function buildKey(base, clientId) {
|
|
144
144
|
return clientId ? `${base}:${clientId}` : base;
|
|
145
145
|
}
|
|
146
|
-
function createMemoryStorageAdapter() {
|
|
147
|
-
const store = /* @__PURE__ */ new Map();
|
|
148
|
-
return {
|
|
149
|
-
get(key) {
|
|
150
|
-
return store.get(key) ?? null;
|
|
151
|
-
},
|
|
152
|
-
set(key, value) {
|
|
153
|
-
store.set(key, value);
|
|
154
|
-
},
|
|
155
|
-
remove(key) {
|
|
156
|
-
store.delete(key);
|
|
157
|
-
},
|
|
158
|
-
keys() {
|
|
159
|
-
return [...store.keys()];
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
var memoryStorageAdapter = createMemoryStorageAdapter();
|
|
164
146
|
var localStorageAdapter = {
|
|
165
147
|
get(key) {
|
|
166
148
|
try {
|
|
@@ -482,7 +464,19 @@ async function fetchJwks(baseUrl, options = {}) {
|
|
|
482
464
|
return cachedJwks.jwks;
|
|
483
465
|
}
|
|
484
466
|
const discovery = await fetchDiscoveryDoc(baseUrl);
|
|
485
|
-
|
|
467
|
+
let jwksUri = `${baseUrl}/api/oauth/jwks`;
|
|
468
|
+
if (discovery?.jwks_uri) {
|
|
469
|
+
try {
|
|
470
|
+
if (new URL(discovery.jwks_uri).origin === new URL(baseUrl).origin) {
|
|
471
|
+
jwksUri = discovery.jwks_uri;
|
|
472
|
+
} else {
|
|
473
|
+
console.warn(
|
|
474
|
+
"[SSO SDK] Discovery \u7684 jwks_uri \u4E0E ssoBaseUrl \u4E0D\u540C\u6E90\uFF0C\u5DF2\u5FFD\u7565\u5E76\u4F7F\u7528\u9ED8\u8BA4 JWKS \u7AEF\u70B9"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
486
480
|
try {
|
|
487
481
|
const res = await fetch(jwksUri, {
|
|
488
482
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
@@ -612,7 +606,10 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
612
606
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
613
607
|
throw new SsoError("id_token_expired", "ID Token \u5DF2\u8FC7\u671F");
|
|
614
608
|
}
|
|
615
|
-
if (typeof payload.iat
|
|
609
|
+
if (typeof payload.iat !== "number") {
|
|
610
|
+
throw new SsoError("id_token_invalid", "ID Token \u7F3A\u5C11 iat \u58F0\u660E");
|
|
611
|
+
}
|
|
612
|
+
if (payload.iat * 1e3 > Date.now() + 6e4) {
|
|
616
613
|
throw new SsoError("id_token_invalid", "ID Token iat \u5728\u672A\u6765\uFF0C\u7591\u4F3C\u4F2A\u9020\u6216\u65F6\u949F\u5F02\u5E38");
|
|
617
614
|
}
|
|
618
615
|
if (typeof payload.sub !== "string" || !payload.sub) {
|
|
@@ -629,13 +626,17 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
629
626
|
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
630
627
|
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
631
628
|
}
|
|
629
|
+
} else if (typeof payload.nonce === "string" && payload.nonce) {
|
|
630
|
+
throw new SsoError(
|
|
631
|
+
"id_token_nonce_mismatch",
|
|
632
|
+
"ID Token \u643A\u5E26 nonce \u4F46\u672C\u5730\u65E0\u671F\u671B\u503C\uFF08\u53EF\u80FD\u975E\u672C\u6B21\u767B\u5F55\u4F1A\u8BDD\u7B7E\u53D1\uFF09"
|
|
633
|
+
);
|
|
632
634
|
}
|
|
633
635
|
return { sub: payload.sub };
|
|
634
636
|
}
|
|
635
637
|
|
|
636
|
-
// src/core/
|
|
638
|
+
// src/core/http.ts
|
|
637
639
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
638
|
-
var REVOKE_TIMEOUT_MS = 3e3;
|
|
639
640
|
async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
640
641
|
const controller = new AbortController();
|
|
641
642
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -645,6 +646,10 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS
|
|
|
645
646
|
clearTimeout(timer);
|
|
646
647
|
}
|
|
647
648
|
}
|
|
649
|
+
|
|
650
|
+
// src/core/SsoClient.ts
|
|
651
|
+
var REVOKE_TIMEOUT_MS = 3e3;
|
|
652
|
+
var EXPIRY_SKEW_MS = 1e4;
|
|
648
653
|
function assertValidTokenResponse(data, requireRefreshToken) {
|
|
649
654
|
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
650
655
|
throw new SsoError("token_request_failed", "Token \u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
@@ -662,16 +667,14 @@ function assertValidTokenResponse(data, requireRefreshToken) {
|
|
|
662
667
|
);
|
|
663
668
|
}
|
|
664
669
|
}
|
|
665
|
-
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
|
|
666
|
-
"login_required",
|
|
667
|
-
"consent_required",
|
|
668
|
-
"interaction_required"
|
|
669
|
-
]);
|
|
670
|
+
var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set(["login_required", "consent_required", "interaction_required"]);
|
|
670
671
|
var _SsoClient = class _SsoClient {
|
|
671
672
|
constructor(config) {
|
|
672
673
|
this._discovery = null;
|
|
673
674
|
this._discoveryFetchedAt = 0;
|
|
674
675
|
this._refreshLock = null;
|
|
676
|
+
/** Discovery 端点 origin 校验失败只告警一次(拒绝的文档不缓存,避免每次调用刷屏) */
|
|
677
|
+
this._discoveryOriginWarned = false;
|
|
675
678
|
if (!config.clientId) throw new SsoError("invalid_config", "clientId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
676
679
|
if (!config.redirectUri) throw new SsoError("invalid_config", "redirectUri \u4E0D\u80FD\u4E3A\u7A7A");
|
|
677
680
|
if (!config.ssoBaseUrl) throw new SsoError("invalid_config", "ssoBaseUrl \u4E0D\u80FD\u4E3A\u7A7A");
|
|
@@ -695,6 +698,68 @@ var _SsoClient = class _SsoClient {
|
|
|
695
698
|
// ============================================
|
|
696
699
|
// 内部方法
|
|
697
700
|
// ============================================
|
|
701
|
+
/**
|
|
702
|
+
* 校验 Discovery 文档端点 origin(防文档被篡改后 token/code 被导向攻击者服务器):
|
|
703
|
+
* - authorization_endpoint 必须与 ssoBaseUrl 同源(浏览器跳转目标);
|
|
704
|
+
* - token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl 同源,
|
|
705
|
+
* 或(配置了 serverBaseUrl 时)与内网地址同源。
|
|
706
|
+
* 任一不匹配:告警并返回 null(上层回退到默认端点,fail-safe)。
|
|
707
|
+
* 可通过 config.allowCrossOriginDiscoveryEndpoints 关闭(见配置项警告)。
|
|
708
|
+
*/
|
|
709
|
+
_sanitizeDiscovery(d) {
|
|
710
|
+
if (this.config.allowCrossOriginDiscoveryEndpoints) return d;
|
|
711
|
+
const allowedOrigins = /* @__PURE__ */ new Set();
|
|
712
|
+
try {
|
|
713
|
+
allowedOrigins.add(new URL(this.config.ssoBaseUrl).origin);
|
|
714
|
+
allowedOrigins.add(new URL(this._serverBase).origin);
|
|
715
|
+
} catch {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
const sameOrigin = (endpoint) => {
|
|
719
|
+
try {
|
|
720
|
+
return allowedOrigins.has(new URL(endpoint).origin);
|
|
721
|
+
} catch {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
const publicOrigin = new URL(this.config.ssoBaseUrl).origin;
|
|
726
|
+
let authorizationOk = false;
|
|
727
|
+
try {
|
|
728
|
+
authorizationOk = new URL(d.authorization_endpoint).origin === publicOrigin;
|
|
729
|
+
} catch {
|
|
730
|
+
authorizationOk = false;
|
|
731
|
+
}
|
|
732
|
+
const bad = [];
|
|
733
|
+
if (!authorizationOk) bad.push("authorization_endpoint");
|
|
734
|
+
if (!sameOrigin(d.token_endpoint)) bad.push("token_endpoint");
|
|
735
|
+
if (!sameOrigin(d.userinfo_endpoint)) bad.push("userinfo_endpoint");
|
|
736
|
+
if (d.jwks_uri && !sameOrigin(d.jwks_uri)) bad.push("jwks_uri");
|
|
737
|
+
if (d.revocation_endpoint && !sameOrigin(d.revocation_endpoint)) {
|
|
738
|
+
bad.push("revocation_endpoint");
|
|
739
|
+
}
|
|
740
|
+
if (d.introspection_endpoint && !sameOrigin(d.introspection_endpoint)) {
|
|
741
|
+
bad.push("introspection_endpoint");
|
|
742
|
+
}
|
|
743
|
+
if (d.end_session_endpoint) {
|
|
744
|
+
try {
|
|
745
|
+
if (new URL(d.end_session_endpoint).origin !== publicOrigin) {
|
|
746
|
+
bad.push("end_session_endpoint");
|
|
747
|
+
}
|
|
748
|
+
} catch {
|
|
749
|
+
bad.push("end_session_endpoint");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (bad.length > 0) {
|
|
753
|
+
if (!this._discoveryOriginWarned) {
|
|
754
|
+
this._discoveryOriginWarned = true;
|
|
755
|
+
console.warn(
|
|
756
|
+
`[SSO SDK] OIDC Discovery \u7AEF\u70B9 origin \u4E0E ssoBaseUrl/serverBaseUrl \u4E0D\u5339\u914D\uFF08${bad.join(", ")}\uFF09\uFF0C\u5DF2\u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9\u3002\u82E5 SSO \u4E2D\u5FC3\u786E\u5B9E\u6258\u7BA1\u8DE8\u6E90\u7AEF\u70B9\uFF0C\u8BF7\u8BBE\u7F6E allowCrossOriginDiscoveryEndpoints: true`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
return d;
|
|
762
|
+
}
|
|
698
763
|
/**
|
|
699
764
|
* 获取 OIDC Discovery 文档(带缓存 + 超时)
|
|
700
765
|
*
|
|
@@ -709,19 +774,17 @@ var _SsoClient = class _SsoClient {
|
|
|
709
774
|
const url = `${this._serverBase}/api/oauth/.well-known/openid-configuration`;
|
|
710
775
|
try {
|
|
711
776
|
const controller = new AbortController();
|
|
712
|
-
const timeoutId = setTimeout(
|
|
713
|
-
() => controller.abort(),
|
|
714
|
-
_SsoClient.DISCOVERY_TIMEOUT_MS
|
|
715
|
-
);
|
|
777
|
+
const timeoutId = setTimeout(() => controller.abort(), _SsoClient.DISCOVERY_TIMEOUT_MS);
|
|
716
778
|
const res = await fetch(url, { signal: controller.signal });
|
|
717
779
|
clearTimeout(timeoutId);
|
|
718
780
|
if (!res.ok) {
|
|
719
|
-
console.warn(
|
|
720
|
-
`[SSO SDK] OIDC Discovery \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}, \u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9`
|
|
721
|
-
);
|
|
781
|
+
console.warn(`[SSO SDK] OIDC Discovery \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}, \u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9`);
|
|
722
782
|
return null;
|
|
723
783
|
}
|
|
724
|
-
|
|
784
|
+
const doc = await res.json();
|
|
785
|
+
const sanitized = this._sanitizeDiscovery(doc);
|
|
786
|
+
if (!sanitized) return null;
|
|
787
|
+
this._discovery = sanitized;
|
|
725
788
|
this._discoveryFetchedAt = now;
|
|
726
789
|
return this._discovery;
|
|
727
790
|
} catch (err) {
|
|
@@ -930,10 +993,7 @@ var _SsoClient = class _SsoClient {
|
|
|
930
993
|
removeOAuthState(this.config.clientId);
|
|
931
994
|
removeOAuthNonce(this.config.clientId);
|
|
932
995
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
933
|
-
throw new SsoError(
|
|
934
|
-
"popup_blocked",
|
|
935
|
-
"\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5"
|
|
936
|
-
);
|
|
996
|
+
throw new SsoError("popup_blocked", "\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5");
|
|
937
997
|
}
|
|
938
998
|
const redirectUriOrigin = new URL(this.config.redirectUri).origin;
|
|
939
999
|
try {
|
|
@@ -962,9 +1022,7 @@ var _SsoClient = class _SsoClient {
|
|
|
962
1022
|
}
|
|
963
1023
|
this.handleCallback(event.data.callbackUrl).then((tokenData) => {
|
|
964
1024
|
if (tokenData === null) {
|
|
965
|
-
reject(
|
|
966
|
-
new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09")
|
|
967
|
-
);
|
|
1025
|
+
reject(new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09"));
|
|
968
1026
|
return;
|
|
969
1027
|
}
|
|
970
1028
|
resolve(tokenData);
|
|
@@ -975,12 +1033,7 @@ var _SsoClient = class _SsoClient {
|
|
|
975
1033
|
if (!completed) {
|
|
976
1034
|
completed = true;
|
|
977
1035
|
cleanup();
|
|
978
|
-
reject(
|
|
979
|
-
new SsoError(
|
|
980
|
-
"popup_closed",
|
|
981
|
-
"\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED"
|
|
982
|
-
)
|
|
983
|
-
);
|
|
1036
|
+
reject(new SsoError("popup_closed", "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED"));
|
|
984
1037
|
}
|
|
985
1038
|
}
|
|
986
1039
|
}, 500);
|
|
@@ -1047,18 +1100,24 @@ var _SsoClient = class _SsoClient {
|
|
|
1047
1100
|
removeOAuthNonce(this.config.clientId);
|
|
1048
1101
|
removePkceVerifier(this.config.clientId);
|
|
1049
1102
|
removeSilentProbe(this.config.clientId);
|
|
1050
|
-
throw new SsoError(
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1103
|
+
throw new SsoError("state_mismatch", "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB");
|
|
1104
|
+
}
|
|
1105
|
+
const returnedIss = params.get("iss");
|
|
1106
|
+
if (returnedIss) {
|
|
1107
|
+
const discovery = await this._getDiscovery();
|
|
1108
|
+
const expectedIssuer = (discovery?.issuer || this.config.ssoBaseUrl).replace(/\/+$/, "");
|
|
1109
|
+
if (returnedIss.replace(/\/+$/, "") !== expectedIssuer) {
|
|
1110
|
+
removeOAuthState(this.config.clientId);
|
|
1111
|
+
removeOAuthNonce(this.config.clientId);
|
|
1112
|
+
removePkceVerifier(this.config.clientId);
|
|
1113
|
+
removeSilentProbe(this.config.clientId);
|
|
1114
|
+
throw new SsoError("issuer_mismatch", "\u6388\u6743\u54CD\u5E94 issuer \u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 IdP \u6DF7\u53E0\u653B\u51FB");
|
|
1115
|
+
}
|
|
1054
1116
|
}
|
|
1055
1117
|
const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
|
|
1056
1118
|
const verifier = getPkceVerifier(this.config.clientId);
|
|
1057
1119
|
if (!verifier) {
|
|
1058
|
-
throw new SsoError(
|
|
1059
|
-
"pkce_required",
|
|
1060
|
-
"code_verifier \u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u6765\u81EA\u5176\u4ED6\u6807\u7B7E\u9875\uFF09"
|
|
1061
|
-
);
|
|
1120
|
+
throw new SsoError("pkce_required", "code_verifier \u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u6765\u81EA\u5176\u4ED6\u6807\u7B7E\u9875\uFF09");
|
|
1062
1121
|
}
|
|
1063
1122
|
const tokenEndpoint = await this._getTokenEndpoint();
|
|
1064
1123
|
const body = new URLSearchParams();
|
|
@@ -1081,6 +1140,10 @@ var _SsoClient = class _SsoClient {
|
|
|
1081
1140
|
throw new SsoError("network_error", "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", err);
|
|
1082
1141
|
}
|
|
1083
1142
|
if (!res.ok) {
|
|
1143
|
+
removeOAuthState(this.config.clientId);
|
|
1144
|
+
removeOAuthNonce(this.config.clientId);
|
|
1145
|
+
removePkceVerifier(this.config.clientId);
|
|
1146
|
+
removeSilentProbe(this.config.clientId);
|
|
1084
1147
|
let errData = {};
|
|
1085
1148
|
try {
|
|
1086
1149
|
errData = await res.json();
|
|
@@ -1096,11 +1159,12 @@ var _SsoClient = class _SsoClient {
|
|
|
1096
1159
|
assertValidTokenResponse(data, true);
|
|
1097
1160
|
const requestedScopes = (this.config.scopes || "openid profile").split(" ").filter(Boolean);
|
|
1098
1161
|
if (requestedScopes.includes("openid") && !data.id_token) {
|
|
1162
|
+
removeOAuthState(this.config.clientId);
|
|
1163
|
+
removeOAuthNonce(this.config.clientId);
|
|
1164
|
+
removePkceVerifier(this.config.clientId);
|
|
1165
|
+
removeSilentProbe(this.config.clientId);
|
|
1099
1166
|
removeTokenData(this.config.clientId);
|
|
1100
|
-
throw new SsoError(
|
|
1101
|
-
"id_token_invalid",
|
|
1102
|
-
"Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09"
|
|
1103
|
-
);
|
|
1167
|
+
throw new SsoError("id_token_invalid", "Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09");
|
|
1104
1168
|
}
|
|
1105
1169
|
if (data.id_token) {
|
|
1106
1170
|
try {
|
|
@@ -1165,19 +1229,14 @@ var _SsoClient = class _SsoClient {
|
|
|
1165
1229
|
}
|
|
1166
1230
|
let res = null;
|
|
1167
1231
|
let lastErr;
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
break;
|
|
1177
|
-
} catch (err) {
|
|
1178
|
-
lastErr = err;
|
|
1179
|
-
if (attempt < 1) await new Promise((r) => setTimeout(r, 1e3));
|
|
1180
|
-
}
|
|
1232
|
+
try {
|
|
1233
|
+
res = await fetchWithTimeout(tokenEndpoint, {
|
|
1234
|
+
method: "POST",
|
|
1235
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1236
|
+
body: body.toString()
|
|
1237
|
+
});
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
lastErr = err;
|
|
1181
1240
|
}
|
|
1182
1241
|
if (lastErr || !res) {
|
|
1183
1242
|
throw new SsoError("network_error", "\u5237\u65B0 Token \u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", lastErr);
|
|
@@ -1240,7 +1299,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1240
1299
|
if (!tokenData) {
|
|
1241
1300
|
throw new SsoError("not_authenticated", "\u672A\u767B\u5F55");
|
|
1242
1301
|
}
|
|
1243
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1302
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1244
1303
|
tokenData = await this.refreshToken();
|
|
1245
1304
|
}
|
|
1246
1305
|
const userinfoEndpoint = await this._getUserinfoEndpoint();
|
|
@@ -1259,10 +1318,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1259
1318
|
removeTokenData(this.config.clientId);
|
|
1260
1319
|
throw new SsoError("not_authenticated", "Token \u5DF2\u5931\u6548");
|
|
1261
1320
|
}
|
|
1262
|
-
throw new SsoError(
|
|
1263
|
-
"userinfo_failed",
|
|
1264
|
-
`\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: HTTP ${res.status}`
|
|
1265
|
-
);
|
|
1321
|
+
throw new SsoError("userinfo_failed", `\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: HTTP ${res.status}`);
|
|
1266
1322
|
}
|
|
1267
1323
|
return await res.json();
|
|
1268
1324
|
}
|
|
@@ -1275,7 +1331,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1275
1331
|
async getAccessToken() {
|
|
1276
1332
|
let tokenData = getTokenData(this.config.clientId);
|
|
1277
1333
|
if (!tokenData) return null;
|
|
1278
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1334
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1279
1335
|
tokenData = await this.refreshToken();
|
|
1280
1336
|
}
|
|
1281
1337
|
return tokenData.access_token;
|
|
@@ -1284,6 +1340,9 @@ var _SsoClient = class _SsoClient {
|
|
|
1284
1340
|
* 检查是否已认证(不发起网络请求)
|
|
1285
1341
|
*
|
|
1286
1342
|
* 仅检查本地是否存在未过期的 access_token。
|
|
1343
|
+
* 故意不做 EXPIRY_SKEW_MS 提前量:这是同步的 UI 提示判定(是否显示"已登录"),
|
|
1344
|
+
* 提前 10s 翻转 false 会让调用方在 token 仍有效时误显示未登录;真正发请求的
|
|
1345
|
+
* getAccessToken/getUserInfo 已带提前量。
|
|
1287
1346
|
*/
|
|
1288
1347
|
isAuthenticated() {
|
|
1289
1348
|
const tokenData = getTokenData(this.config.clientId);
|
|
@@ -1319,7 +1378,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1319
1378
|
if (this.config.clientSecret) {
|
|
1320
1379
|
revokeBody.set("client_secret", this.config.clientSecret);
|
|
1321
1380
|
}
|
|
1322
|
-
|
|
1381
|
+
const doRevoke = () => fetchWithTimeout(
|
|
1323
1382
|
revokeUrl,
|
|
1324
1383
|
{
|
|
1325
1384
|
method: "POST",
|
|
@@ -1328,6 +1387,14 @@ var _SsoClient = class _SsoClient {
|
|
|
1328
1387
|
},
|
|
1329
1388
|
REVOKE_TIMEOUT_MS
|
|
1330
1389
|
);
|
|
1390
|
+
let res = await doRevoke();
|
|
1391
|
+
if (!res.ok && (res.status >= 500 || res.status === 429)) {
|
|
1392
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1393
|
+
res = await doRevoke();
|
|
1394
|
+
}
|
|
1395
|
+
if (!res.ok) {
|
|
1396
|
+
this.debugLog("logout: \u670D\u52A1\u7AEF\u64A4\u9500 refresh_token \u5931\u8D25", { status: res.status });
|
|
1397
|
+
}
|
|
1331
1398
|
} catch {
|
|
1332
1399
|
}
|
|
1333
1400
|
}
|
|
@@ -1338,10 +1405,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1338
1405
|
logoutUrl.searchParams.set("client_id", this.config.clientId);
|
|
1339
1406
|
const postLogoutUri = this.config.postLogoutRedirectUri || this.config.redirectUri;
|
|
1340
1407
|
if (postLogoutUri) {
|
|
1341
|
-
logoutUrl.searchParams.set(
|
|
1342
|
-
"post_logout_redirect_uri",
|
|
1343
|
-
postLogoutUri
|
|
1344
|
-
);
|
|
1408
|
+
logoutUrl.searchParams.set("post_logout_redirect_uri", postLogoutUri);
|
|
1345
1409
|
}
|
|
1346
1410
|
if (idTokenHint) {
|
|
1347
1411
|
logoutUrl.searchParams.set("id_token_hint", idTokenHint);
|