@nihplod/sso-sdk 1.5.0 → 1.6.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 +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +79 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +79 -6
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +38 -2
- package/dist/next/index.d.ts +38 -2
- package/dist/next/index.js +82 -20
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +82 -20
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +25 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/index.js +90 -9
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +90 -9
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -558,7 +558,10 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
558
558
|
if (Date.now() >= payload.exp * 1e3 + 6e4) {
|
|
559
559
|
throw new SsoError("id_token_expired", "ID Token \u5DF2\u8FC7\u671F");
|
|
560
560
|
}
|
|
561
|
-
if (typeof payload.iat
|
|
561
|
+
if (typeof payload.iat !== "number") {
|
|
562
|
+
throw new SsoError("id_token_invalid", "ID Token \u7F3A\u5C11 iat \u58F0\u660E");
|
|
563
|
+
}
|
|
564
|
+
if (payload.iat * 1e3 > Date.now() + 6e4) {
|
|
562
565
|
throw new SsoError("id_token_invalid", "ID Token iat \u5728\u672A\u6765\uFF0C\u7591\u4F3C\u4F2A\u9020\u6216\u65F6\u949F\u5F02\u5E38");
|
|
563
566
|
}
|
|
564
567
|
if (typeof payload.sub !== "string" || !payload.sub) {
|
|
@@ -575,13 +578,17 @@ async function validateIdToken(idToken, accessToken, expectedIssuer, expectedCli
|
|
|
575
578
|
if (!tokenNonce || !timingSafeEqualString(expectedNonce, tokenNonce)) {
|
|
576
579
|
throw new SsoError("id_token_nonce_mismatch", "ID Token nonce \u4E0D\u5339\u914D");
|
|
577
580
|
}
|
|
581
|
+
} else if (typeof payload.nonce === "string" && payload.nonce) {
|
|
582
|
+
throw new SsoError(
|
|
583
|
+
"id_token_nonce_mismatch",
|
|
584
|
+
"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"
|
|
585
|
+
);
|
|
578
586
|
}
|
|
579
587
|
return { sub: payload.sub };
|
|
580
588
|
}
|
|
581
589
|
|
|
582
|
-
// src/core/
|
|
590
|
+
// src/core/http.ts
|
|
583
591
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
584
|
-
var REVOKE_TIMEOUT_MS = 3e3;
|
|
585
592
|
async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
586
593
|
const controller = new AbortController();
|
|
587
594
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -591,6 +598,10 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = REQUEST_TIMEOUT_MS
|
|
|
591
598
|
clearTimeout(timer);
|
|
592
599
|
}
|
|
593
600
|
}
|
|
601
|
+
|
|
602
|
+
// src/core/SsoClient.ts
|
|
603
|
+
var REVOKE_TIMEOUT_MS = 3e3;
|
|
604
|
+
var EXPIRY_SKEW_MS = 1e4;
|
|
594
605
|
function assertValidTokenResponse(data, requireRefreshToken) {
|
|
595
606
|
if (typeof data.access_token !== "string" || !data.access_token) {
|
|
596
607
|
throw new SsoError("token_request_failed", "Token \u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
@@ -618,6 +629,8 @@ var _SsoClient = class _SsoClient {
|
|
|
618
629
|
this._discovery = null;
|
|
619
630
|
this._discoveryFetchedAt = 0;
|
|
620
631
|
this._refreshLock = null;
|
|
632
|
+
/** Discovery 端点 origin 校验失败只告警一次(拒绝的文档不缓存,避免每次调用刷屏) */
|
|
633
|
+
this._discoveryOriginWarned = false;
|
|
621
634
|
if (!config.clientId) throw new SsoError("invalid_config", "clientId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
622
635
|
if (!config.redirectUri) throw new SsoError("invalid_config", "redirectUri \u4E0D\u80FD\u4E3A\u7A7A");
|
|
623
636
|
if (!config.ssoBaseUrl) throw new SsoError("invalid_config", "ssoBaseUrl \u4E0D\u80FD\u4E3A\u7A7A");
|
|
@@ -641,6 +654,52 @@ var _SsoClient = class _SsoClient {
|
|
|
641
654
|
// ============================================
|
|
642
655
|
// 内部方法
|
|
643
656
|
// ============================================
|
|
657
|
+
/**
|
|
658
|
+
* 校验 Discovery 文档端点 origin(防文档被篡改后 token/code 被导向攻击者服务器):
|
|
659
|
+
* - authorization_endpoint 必须与 ssoBaseUrl 同源(浏览器跳转目标);
|
|
660
|
+
* - token_endpoint / userinfo_endpoint 必须与 ssoBaseUrl 同源,
|
|
661
|
+
* 或(配置了 serverBaseUrl 时)与内网地址同源。
|
|
662
|
+
* 任一不匹配:告警并返回 null(上层回退到默认端点,fail-safe)。
|
|
663
|
+
* 可通过 config.allowCrossOriginDiscoveryEndpoints 关闭(见配置项警告)。
|
|
664
|
+
*/
|
|
665
|
+
_sanitizeDiscovery(d) {
|
|
666
|
+
if (this.config.allowCrossOriginDiscoveryEndpoints) return d;
|
|
667
|
+
const allowedOrigins = /* @__PURE__ */ new Set();
|
|
668
|
+
try {
|
|
669
|
+
allowedOrigins.add(new URL(this.config.ssoBaseUrl).origin);
|
|
670
|
+
allowedOrigins.add(new URL(this._serverBase).origin);
|
|
671
|
+
} catch {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
const sameOrigin = (endpoint) => {
|
|
675
|
+
try {
|
|
676
|
+
return allowedOrigins.has(new URL(endpoint).origin);
|
|
677
|
+
} catch {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const publicOrigin = new URL(this.config.ssoBaseUrl).origin;
|
|
682
|
+
let authorizationOk = false;
|
|
683
|
+
try {
|
|
684
|
+
authorizationOk = new URL(d.authorization_endpoint).origin === publicOrigin;
|
|
685
|
+
} catch {
|
|
686
|
+
authorizationOk = false;
|
|
687
|
+
}
|
|
688
|
+
const bad = [];
|
|
689
|
+
if (!authorizationOk) bad.push("authorization_endpoint");
|
|
690
|
+
if (!sameOrigin(d.token_endpoint)) bad.push("token_endpoint");
|
|
691
|
+
if (!sameOrigin(d.userinfo_endpoint)) bad.push("userinfo_endpoint");
|
|
692
|
+
if (bad.length > 0) {
|
|
693
|
+
if (!this._discoveryOriginWarned) {
|
|
694
|
+
this._discoveryOriginWarned = true;
|
|
695
|
+
console.warn(
|
|
696
|
+
`[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`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
return d;
|
|
702
|
+
}
|
|
644
703
|
/**
|
|
645
704
|
* 获取 OIDC Discovery 文档(带缓存 + 超时)
|
|
646
705
|
*
|
|
@@ -667,7 +726,10 @@ var _SsoClient = class _SsoClient {
|
|
|
667
726
|
);
|
|
668
727
|
return null;
|
|
669
728
|
}
|
|
670
|
-
|
|
729
|
+
const doc = await res.json();
|
|
730
|
+
const sanitized = this._sanitizeDiscovery(doc);
|
|
731
|
+
if (!sanitized) return null;
|
|
732
|
+
this._discovery = sanitized;
|
|
671
733
|
this._discoveryFetchedAt = now;
|
|
672
734
|
return this._discovery;
|
|
673
735
|
} catch (err) {
|
|
@@ -1027,6 +1089,10 @@ var _SsoClient = class _SsoClient {
|
|
|
1027
1089
|
throw new SsoError("network_error", "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", err);
|
|
1028
1090
|
}
|
|
1029
1091
|
if (!res.ok) {
|
|
1092
|
+
removeOAuthState(this.config.clientId);
|
|
1093
|
+
removeOAuthNonce(this.config.clientId);
|
|
1094
|
+
removePkceVerifier(this.config.clientId);
|
|
1095
|
+
removeSilentProbe(this.config.clientId);
|
|
1030
1096
|
let errData = {};
|
|
1031
1097
|
try {
|
|
1032
1098
|
errData = await res.json();
|
|
@@ -1042,6 +1108,10 @@ var _SsoClient = class _SsoClient {
|
|
|
1042
1108
|
assertValidTokenResponse(data, true);
|
|
1043
1109
|
const requestedScopes = (this.config.scopes || "openid profile").split(" ").filter(Boolean);
|
|
1044
1110
|
if (requestedScopes.includes("openid") && !data.id_token) {
|
|
1111
|
+
removeOAuthState(this.config.clientId);
|
|
1112
|
+
removeOAuthNonce(this.config.clientId);
|
|
1113
|
+
removePkceVerifier(this.config.clientId);
|
|
1114
|
+
removeSilentProbe(this.config.clientId);
|
|
1045
1115
|
removeTokenData(this.config.clientId);
|
|
1046
1116
|
throw new SsoError(
|
|
1047
1117
|
"id_token_invalid",
|
|
@@ -1186,7 +1256,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1186
1256
|
if (!tokenData) {
|
|
1187
1257
|
throw new SsoError("not_authenticated", "\u672A\u767B\u5F55");
|
|
1188
1258
|
}
|
|
1189
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1259
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1190
1260
|
tokenData = await this.refreshToken();
|
|
1191
1261
|
}
|
|
1192
1262
|
const userinfoEndpoint = await this._getUserinfoEndpoint();
|
|
@@ -1221,7 +1291,7 @@ var _SsoClient = class _SsoClient {
|
|
|
1221
1291
|
async getAccessToken() {
|
|
1222
1292
|
let tokenData = getTokenData(this.config.clientId);
|
|
1223
1293
|
if (!tokenData) return null;
|
|
1224
|
-
if (Date.now() >= tokenData.expires_at) {
|
|
1294
|
+
if (Date.now() >= tokenData.expires_at - EXPIRY_SKEW_MS) {
|
|
1225
1295
|
tokenData = await this.refreshToken();
|
|
1226
1296
|
}
|
|
1227
1297
|
return tokenData.access_token;
|
|
@@ -1230,6 +1300,9 @@ var _SsoClient = class _SsoClient {
|
|
|
1230
1300
|
* 检查是否已认证(不发起网络请求)
|
|
1231
1301
|
*
|
|
1232
1302
|
* 仅检查本地是否存在未过期的 access_token。
|
|
1303
|
+
* 故意不做 EXPIRY_SKEW_MS 提前量:这是同步的 UI 提示判定(是否显示"已登录"),
|
|
1304
|
+
* 提前 10s 翻转 false 会让调用方在 token 仍有效时误显示未登录;真正发请求的
|
|
1305
|
+
* getAccessToken/getUserInfo 已带提前量。
|
|
1233
1306
|
*/
|
|
1234
1307
|
isAuthenticated() {
|
|
1235
1308
|
const tokenData = getTokenData(this.config.clientId);
|