@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.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
|
*
|
|
@@ -110,6 +110,13 @@ interface SsoClientConfig {
|
|
|
110
110
|
scopes?: string;
|
|
111
111
|
/** RP-Initiated Logout 返回地址(可选)。不传时回退到 redirectUri */
|
|
112
112
|
postLogoutRedirectUri?: string;
|
|
113
|
+
/**
|
|
114
|
+
* 服务端到服务端调用的内网地址(可选,如 http://127.0.0.1:3000)。
|
|
115
|
+
* 仅用于 discovery / token / userinfo / revocation 等服务器间请求;
|
|
116
|
+
* 浏览器跳转(authorize、end-session)始终使用 ssoBaseUrl 公网地址。
|
|
117
|
+
* 仅在 BFF/服务端场景配置;浏览器端(Public Client)不要配置。
|
|
118
|
+
*/
|
|
119
|
+
serverBaseUrl?: string;
|
|
113
120
|
}
|
|
114
121
|
/** 用户信息 */
|
|
115
122
|
interface SsoUser {
|
|
@@ -123,6 +130,8 @@ interface SsoUser {
|
|
|
123
130
|
/** 生日(ISO 8601,需 birthday scope,未设置时为 null) */
|
|
124
131
|
birthday?: string | null;
|
|
125
132
|
membership_level?: string;
|
|
133
|
+
/** 累计消费金额(单位:元,需 membership scope,与 membership_level 同源下发) */
|
|
134
|
+
total_spent?: number;
|
|
126
135
|
total_points?: number;
|
|
127
136
|
}
|
|
128
137
|
/** Token 响应 */
|
|
@@ -150,6 +159,8 @@ interface OidcDiscovery {
|
|
|
150
159
|
}
|
|
151
160
|
declare class SsoClient {
|
|
152
161
|
readonly config: SsoClientConfig;
|
|
162
|
+
/** 服务器间调用的基准地址(未配置 serverBaseUrl 时等于 ssoBaseUrl) */
|
|
163
|
+
private readonly _serverBase;
|
|
153
164
|
private _discovery;
|
|
154
165
|
private _discoveryFetchedAt;
|
|
155
166
|
private _refreshLock;
|
|
@@ -167,9 +178,9 @@ declare class SsoClient {
|
|
|
167
178
|
private _getDiscovery;
|
|
168
179
|
/** 获取 authorize 端点 URL(优先 Discovery,回退默认) */
|
|
169
180
|
private _getAuthorizeEndpoint;
|
|
170
|
-
/** 获取 token 端点 URL
|
|
181
|
+
/** 获取 token 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */
|
|
171
182
|
private _getTokenEndpoint;
|
|
172
|
-
/** 获取 userinfo 端点 URL
|
|
183
|
+
/** 获取 userinfo 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */
|
|
173
184
|
private _getUserinfoEndpoint;
|
|
174
185
|
/**
|
|
175
186
|
* 开放重定向防护:仅保存相对路径或与当前页面同源的 returnUrl,
|
|
@@ -348,7 +359,7 @@ declare function generateState(): string;
|
|
|
348
359
|
* SSO SDK 错误类型
|
|
349
360
|
*/
|
|
350
361
|
/** SSO 错误码 */
|
|
351
|
-
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";
|
|
352
363
|
/**
|
|
353
364
|
* 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode
|
|
354
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
|
*
|
|
@@ -110,6 +110,13 @@ interface SsoClientConfig {
|
|
|
110
110
|
scopes?: string;
|
|
111
111
|
/** RP-Initiated Logout 返回地址(可选)。不传时回退到 redirectUri */
|
|
112
112
|
postLogoutRedirectUri?: string;
|
|
113
|
+
/**
|
|
114
|
+
* 服务端到服务端调用的内网地址(可选,如 http://127.0.0.1:3000)。
|
|
115
|
+
* 仅用于 discovery / token / userinfo / revocation 等服务器间请求;
|
|
116
|
+
* 浏览器跳转(authorize、end-session)始终使用 ssoBaseUrl 公网地址。
|
|
117
|
+
* 仅在 BFF/服务端场景配置;浏览器端(Public Client)不要配置。
|
|
118
|
+
*/
|
|
119
|
+
serverBaseUrl?: string;
|
|
113
120
|
}
|
|
114
121
|
/** 用户信息 */
|
|
115
122
|
interface SsoUser {
|
|
@@ -123,6 +130,8 @@ interface SsoUser {
|
|
|
123
130
|
/** 生日(ISO 8601,需 birthday scope,未设置时为 null) */
|
|
124
131
|
birthday?: string | null;
|
|
125
132
|
membership_level?: string;
|
|
133
|
+
/** 累计消费金额(单位:元,需 membership scope,与 membership_level 同源下发) */
|
|
134
|
+
total_spent?: number;
|
|
126
135
|
total_points?: number;
|
|
127
136
|
}
|
|
128
137
|
/** Token 响应 */
|
|
@@ -150,6 +159,8 @@ interface OidcDiscovery {
|
|
|
150
159
|
}
|
|
151
160
|
declare class SsoClient {
|
|
152
161
|
readonly config: SsoClientConfig;
|
|
162
|
+
/** 服务器间调用的基准地址(未配置 serverBaseUrl 时等于 ssoBaseUrl) */
|
|
163
|
+
private readonly _serverBase;
|
|
153
164
|
private _discovery;
|
|
154
165
|
private _discoveryFetchedAt;
|
|
155
166
|
private _refreshLock;
|
|
@@ -167,9 +178,9 @@ declare class SsoClient {
|
|
|
167
178
|
private _getDiscovery;
|
|
168
179
|
/** 获取 authorize 端点 URL(优先 Discovery,回退默认) */
|
|
169
180
|
private _getAuthorizeEndpoint;
|
|
170
|
-
/** 获取 token 端点 URL
|
|
181
|
+
/** 获取 token 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */
|
|
171
182
|
private _getTokenEndpoint;
|
|
172
|
-
/** 获取 userinfo 端点 URL
|
|
183
|
+
/** 获取 userinfo 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */
|
|
173
184
|
private _getUserinfoEndpoint;
|
|
174
185
|
/**
|
|
175
186
|
* 开放重定向防护:仅保存相对路径或与当前页面同源的 returnUrl,
|
|
@@ -348,7 +359,7 @@ declare function generateState(): string;
|
|
|
348
359
|
* SSO SDK 错误类型
|
|
349
360
|
*/
|
|
350
361
|
/** SSO 错误码 */
|
|
351
|
-
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";
|
|
352
363
|
/**
|
|
353
364
|
* 将 OAuth 2.0 服务端 error 字段映射到 SsoErrorCode
|
|
354
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
|
|
|
@@ -541,6 +565,7 @@ var _SsoClient = class _SsoClient {
|
|
|
541
565
|
if (!config.ssoBaseUrl) throw new SsoError("invalid_config", "ssoBaseUrl \u4E0D\u80FD\u4E3A\u7A7A");
|
|
542
566
|
const base = config.ssoBaseUrl.replace(/\/+$/, "");
|
|
543
567
|
this.config = { ...config, ssoBaseUrl: base };
|
|
568
|
+
this._serverBase = (config.serverBaseUrl ?? base).replace(/\/+$/, "");
|
|
544
569
|
}
|
|
545
570
|
// ============================================
|
|
546
571
|
// 内部方法
|
|
@@ -556,7 +581,7 @@ var _SsoClient = class _SsoClient {
|
|
|
556
581
|
if (this._discovery && now - this._discoveryFetchedAt < _SsoClient.DISCOVERY_TTL_MS) {
|
|
557
582
|
return this._discovery;
|
|
558
583
|
}
|
|
559
|
-
const url = `${this.
|
|
584
|
+
const url = `${this._serverBase}/api/oauth/.well-known/openid-configuration`;
|
|
560
585
|
try {
|
|
561
586
|
const controller = new AbortController();
|
|
562
587
|
const timeoutId = setTimeout(
|
|
@@ -587,14 +612,16 @@ var _SsoClient = class _SsoClient {
|
|
|
587
612
|
if (d) return d.authorization_endpoint;
|
|
588
613
|
return `${this.config.ssoBaseUrl}/api/oauth/authorize`;
|
|
589
614
|
}
|
|
590
|
-
/** 获取 token 端点 URL
|
|
615
|
+
/** 获取 token 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */
|
|
591
616
|
async _getTokenEndpoint() {
|
|
617
|
+
if (this.config.serverBaseUrl) return `${this._serverBase}/api/oauth/token`;
|
|
592
618
|
const d = await this._getDiscovery();
|
|
593
619
|
if (d) return d.token_endpoint;
|
|
594
620
|
return `${this.config.ssoBaseUrl}/api/oauth/token`;
|
|
595
621
|
}
|
|
596
|
-
/** 获取 userinfo 端点 URL
|
|
622
|
+
/** 获取 userinfo 端点 URL(服务器间调用:serverBaseUrl 直连优先,其次 Discovery,回退默认) */
|
|
597
623
|
async _getUserinfoEndpoint() {
|
|
624
|
+
if (this.config.serverBaseUrl) return `${this._serverBase}/api/oauth/userinfo`;
|
|
598
625
|
const d = await this._getDiscovery();
|
|
599
626
|
if (d) return d.userinfo_endpoint;
|
|
600
627
|
return `${this.config.ssoBaseUrl}/api/oauth/userinfo`;
|
|
@@ -630,8 +657,10 @@ var _SsoClient = class _SsoClient {
|
|
|
630
657
|
const verifier = generateCodeVerifier();
|
|
631
658
|
const challenge = await generateCodeChallenge(verifier);
|
|
632
659
|
const state = generateState();
|
|
660
|
+
const nonce = generateState();
|
|
633
661
|
savePkceVerifier(this.config.clientId, verifier);
|
|
634
662
|
saveOAuthState(state, this.config.clientId);
|
|
663
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
635
664
|
if (returnUrl) {
|
|
636
665
|
this._saveReturnUrlIfTrusted(returnUrl);
|
|
637
666
|
}
|
|
@@ -642,6 +671,7 @@ var _SsoClient = class _SsoClient {
|
|
|
642
671
|
params.set("redirect_uri", this.config.redirectUri);
|
|
643
672
|
params.set("scope", this.config.scopes || "openid profile");
|
|
644
673
|
params.set("state", state);
|
|
674
|
+
params.set("nonce", nonce);
|
|
645
675
|
params.set("code_challenge", challenge);
|
|
646
676
|
params.set("code_challenge_method", "S256");
|
|
647
677
|
window.location.href = `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -659,8 +689,10 @@ 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
|
savePkceVerifier(this.config.clientId, verifier);
|
|
663
694
|
saveOAuthState(state, this.config.clientId);
|
|
695
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
664
696
|
if (returnUrl) this._saveReturnUrlIfTrusted(returnUrl);
|
|
665
697
|
const authorizeEndpoint = await this._getAuthorizeEndpoint();
|
|
666
698
|
const params = new URLSearchParams();
|
|
@@ -669,6 +701,7 @@ var _SsoClient = class _SsoClient {
|
|
|
669
701
|
params.set("redirect_uri", this.config.redirectUri);
|
|
670
702
|
params.set("scope", this.config.scopes || "openid profile");
|
|
671
703
|
params.set("state", state);
|
|
704
|
+
params.set("nonce", nonce);
|
|
672
705
|
params.set("code_challenge", challenge);
|
|
673
706
|
params.set("code_challenge_method", "S256");
|
|
674
707
|
return `${authorizeEndpoint}?${params.toString()}`;
|
|
@@ -710,9 +743,11 @@ var _SsoClient = class _SsoClient {
|
|
|
710
743
|
const verifier = generateCodeVerifier();
|
|
711
744
|
const challenge = await generateCodeChallenge(verifier);
|
|
712
745
|
const state = generateState();
|
|
746
|
+
const nonce = generateState();
|
|
713
747
|
const popupNonce = generateState();
|
|
714
748
|
savePkceVerifier(this.config.clientId, verifier);
|
|
715
749
|
saveOAuthState(state, this.config.clientId);
|
|
750
|
+
saveOAuthNonce(nonce, this.config.clientId);
|
|
716
751
|
savePkceVerifier(`${this.config.clientId}_popup_nonce`, popupNonce);
|
|
717
752
|
if (options.returnUrl) {
|
|
718
753
|
this._saveReturnUrlIfTrusted(options.returnUrl);
|
|
@@ -724,6 +759,7 @@ var _SsoClient = class _SsoClient {
|
|
|
724
759
|
params.set("redirect_uri", this.config.redirectUri);
|
|
725
760
|
params.set("scope", this.config.scopes || "openid profile");
|
|
726
761
|
params.set("state", state);
|
|
762
|
+
params.set("nonce", nonce);
|
|
727
763
|
params.set("code_challenge", challenge);
|
|
728
764
|
params.set("code_challenge_method", "S256");
|
|
729
765
|
params.set("popup_nonce", popupNonce);
|
|
@@ -748,6 +784,7 @@ var _SsoClient = class _SsoClient {
|
|
|
748
784
|
if (!popup) {
|
|
749
785
|
removePkceVerifier(this.config.clientId);
|
|
750
786
|
removeOAuthState(this.config.clientId);
|
|
787
|
+
removeOAuthNonce(this.config.clientId);
|
|
751
788
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
752
789
|
throw new SsoError(
|
|
753
790
|
"popup_blocked",
|
|
@@ -767,6 +804,13 @@ var _SsoClient = class _SsoClient {
|
|
|
767
804
|
const savedNonce = getPkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
768
805
|
if (!savedNonce || !timingSafeEqualString(event.data.nonce ?? "", savedNonce)) return;
|
|
769
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
|
+
}
|
|
770
814
|
completed = true;
|
|
771
815
|
cleanup();
|
|
772
816
|
if (popup && !popup.closed) {
|
|
@@ -798,6 +842,7 @@ var _SsoClient = class _SsoClient {
|
|
|
798
842
|
} catch (err) {
|
|
799
843
|
removePkceVerifier(this.config.clientId);
|
|
800
844
|
removeOAuthState(this.config.clientId);
|
|
845
|
+
removeOAuthNonce(this.config.clientId);
|
|
801
846
|
removePkceVerifier(`${this.config.clientId}_popup_nonce`);
|
|
802
847
|
throw err;
|
|
803
848
|
}
|
|
@@ -817,6 +862,7 @@ var _SsoClient = class _SsoClient {
|
|
|
817
862
|
const error = params.get("error");
|
|
818
863
|
if (error) {
|
|
819
864
|
removeOAuthState(this.config.clientId);
|
|
865
|
+
removeOAuthNonce(this.config.clientId);
|
|
820
866
|
removePkceVerifier(this.config.clientId);
|
|
821
867
|
const desc = params.get("error_description") || error;
|
|
822
868
|
throw new SsoError(mapOAuthErrorToSsoCode(error), `\u6388\u6743\u5931\u8D25: ${desc}`);
|
|
@@ -825,18 +871,21 @@ var _SsoClient = class _SsoClient {
|
|
|
825
871
|
const returnedState = params.get("state");
|
|
826
872
|
if (!code) {
|
|
827
873
|
removeOAuthState(this.config.clientId);
|
|
874
|
+
removeOAuthNonce(this.config.clientId);
|
|
828
875
|
removePkceVerifier(this.config.clientId);
|
|
829
876
|
throw new SsoError("token_request_failed", "\u56DE\u8C03 URL \u4E2D\u7F3A\u5C11 authorization code");
|
|
830
877
|
}
|
|
831
878
|
const savedState = getOAuthState(this.config.clientId);
|
|
832
879
|
if (!savedState || !timingSafeEqualString(savedState, returnedState ?? "")) {
|
|
833
880
|
removeOAuthState(this.config.clientId);
|
|
881
|
+
removeOAuthNonce(this.config.clientId);
|
|
834
882
|
removePkceVerifier(this.config.clientId);
|
|
835
883
|
throw new SsoError(
|
|
836
884
|
"state_mismatch",
|
|
837
885
|
"State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
|
|
838
886
|
);
|
|
839
887
|
}
|
|
888
|
+
const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
|
|
840
889
|
const verifier = getPkceVerifier(this.config.clientId);
|
|
841
890
|
if (!verifier) {
|
|
842
891
|
throw new SsoError(
|
|
@@ -883,7 +932,8 @@ var _SsoClient = class _SsoClient {
|
|
|
883
932
|
data.id_token,
|
|
884
933
|
data.access_token,
|
|
885
934
|
this.config.ssoBaseUrl,
|
|
886
|
-
this.config.clientId
|
|
935
|
+
this.config.clientId,
|
|
936
|
+
{ expectedNonce }
|
|
887
937
|
);
|
|
888
938
|
} catch (err) {
|
|
889
939
|
removeTokenData(this.config.clientId);
|
|
@@ -891,6 +941,7 @@ var _SsoClient = class _SsoClient {
|
|
|
891
941
|
}
|
|
892
942
|
}
|
|
893
943
|
removeOAuthState(this.config.clientId);
|
|
944
|
+
removeOAuthNonce(this.config.clientId);
|
|
894
945
|
removePkceVerifier(this.config.clientId);
|
|
895
946
|
const now = Date.now();
|
|
896
947
|
const tokenData = {
|
|
@@ -1071,8 +1122,8 @@ var _SsoClient = class _SsoClient {
|
|
|
1071
1122
|
clearAllSsoData(this.config.clientId);
|
|
1072
1123
|
if (refreshToken && this.config.clientId) {
|
|
1073
1124
|
try {
|
|
1074
|
-
const discovery = await this._getDiscovery();
|
|
1075
|
-
const revokeUrl = discovery?.revocation_endpoint || `${this.config.ssoBaseUrl}/api/oauth/revoke`;
|
|
1125
|
+
const discovery = this.config.serverBaseUrl ? null : await this._getDiscovery();
|
|
1126
|
+
const revokeUrl = this.config.serverBaseUrl ? `${this._serverBase}/api/oauth/revoke` : discovery?.revocation_endpoint || `${this.config.ssoBaseUrl}/api/oauth/revoke`;
|
|
1076
1127
|
const revokeBody = new URLSearchParams({
|
|
1077
1128
|
token: refreshToken,
|
|
1078
1129
|
token_type_hint: "refresh_token",
|