@nihplod/sso-verify 1.1.1 → 1.1.2
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 +4 -1
- package/dist/index.d.mts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +11 -2
- package/dist/index.mjs +11 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ After successful verification, the token payload is attached to `req.user`.
|
|
|
104
104
|
| `logoutTokenPublicKey` | — | RS256 public key (PEM) for back-channel `logout_token` verification |
|
|
105
105
|
| `logoutTokenSecret` | — | HS256 secret for local `logout_token` verification |
|
|
106
106
|
| `logoutJtiStore` | — | External store for processed `logout_token` jtis (see "Multi-instance deployments" below) |
|
|
107
|
+
| `strictAudience` | `false` | Reject introspection responses that carry neither `aud` nor `client_id` (fail-closed). **Recommended for production** |
|
|
107
108
|
|
|
108
109
|
### Revocation latency trade-off
|
|
109
110
|
|
|
@@ -119,11 +120,13 @@ When verifying via introspection, the response's audience binding is checked aga
|
|
|
119
120
|
- Otherwise, if the response contains `client_id`, it must equal the configured `audience`.
|
|
120
121
|
- If the endpoint returns neither field, the response is trusted as-is (legacy behavior); the main site's introspection endpoint always returns `client_id`, so tokens issued to other clients are rejected.
|
|
121
122
|
|
|
123
|
+
> ⚠️ **Production recommendation: enable `strictAudience: true`.** In strict mode a response missing both `aud` and `client_id` is rejected (fail-closed) instead of trusted, eliminating the fail-open path entirely. The main site's introspection endpoint always returns `client_id`, so enabling it does not change behavior against the main site — only turn it off when integrating a third-party introspection endpoint that returns no audience fields.
|
|
124
|
+
|
|
122
125
|
### Logout token verification
|
|
123
126
|
|
|
124
127
|
Back-channel `logout_token`s are signed with a dedicated key pair (`kid: logout-token-rs256-v1`), which is **different** from the access token key — do not use `accessTokenPublicKey` for them. Configure `logoutTokenPublicKey` (PEM) or `jwksUri` (keys matched by `kid`). Verification is dispatched by the JWT header `alg`: an RS256 logout token without any matching public key fails closed (returns `null`) instead of silently falling back to HS256; HS256 is only used when `logoutTokenSecret` (or the legacy `accessTokenSecret` fallback) is explicitly configured.
|
|
125
128
|
|
|
126
|
-
Per OIDC Back-Channel Logout 1.0, a `logout_token` must carry an `exp` claim
|
|
129
|
+
Per OIDC Back-Channel Logout 1.0, a `logout_token` must carry an `exp` claim, an `events` claim whose `http://schemas.openid.net/event/backchannel-logout` member is an object (typically `{}`), and at least one of `sub` / `sid` (§2.4); tokens failing any of these checks are rejected. Verified `jti`s are replay-guarded per issuer for 10 minutes.
|
|
127
130
|
|
|
128
131
|
When the IdP revokes sessions administratively (consent revoke, session termination, user ban/deletion), the `logout_token` also carries a `sid` claim matching the terminated IdP session — RPs that track IdP `sid`s should invalidate the matching local session and may fall back to `sub`-level logout when `sid` is absent (e.g. user-initiated IdP logout without a known session). Failed deliveries are retried by the IdP with exponential backoff (up to 10 attempts), so RPs may receive a delayed `logout_token`; the `jti` replay guard still applies.
|
|
129
132
|
|
package/dist/index.d.mts
CHANGED
|
@@ -111,6 +111,17 @@ interface SsoVerifierOptions {
|
|
|
111
111
|
* 多实例部署时应注入共享存储(如 Redis 实现)以防跨实例重放。
|
|
112
112
|
*/
|
|
113
113
|
logoutJtiStore?: LogoutJtiStore;
|
|
114
|
+
/**
|
|
115
|
+
* Introspection audience 严格模式(默认 false,保持向后兼容)。
|
|
116
|
+
*
|
|
117
|
+
* 默认模式下,introspection 响应既无 aud 又无 client_id 时保持信任
|
|
118
|
+
* (兼容不返回归属字段的端点)。设为 true 后:响应缺少归属字段即拒绝
|
|
119
|
+
* (fail-closed),防止被 confused deputy 攻击利用。
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ 生产环境强烈建议开启。主站 introspection 端点始终返回 client_id,
|
|
122
|
+
* 开启后行为不变;仅在对接不返回归属字段的第三方端点时才需要保持 false。
|
|
123
|
+
*/
|
|
124
|
+
strictAudience?: boolean;
|
|
114
125
|
}
|
|
115
126
|
/**
|
|
116
127
|
* Logout Token jti 防重放存储接口(可注入 Redis 等共享存储实现)。
|
|
@@ -136,7 +147,7 @@ interface VerifiedTokenPayload extends JWTPayload {
|
|
|
136
147
|
* 主站签发 logout_token 时包含以下字段:
|
|
137
148
|
* - iss: 主站 issuer
|
|
138
149
|
* - aud: 目标 client_id
|
|
139
|
-
* - sub: 用户 ID
|
|
150
|
+
* - sub: 用户 ID(与 sid 至少其一)
|
|
140
151
|
* - iat: 签发时间
|
|
141
152
|
* - jti: 唯一 ID(防重放,建议接收方缓存已处理的 jti)
|
|
142
153
|
* - events: { "http://schemas.openid.net/event/backchannel-logout": {} }
|
|
@@ -148,8 +159,8 @@ interface LogoutTokenPayload {
|
|
|
148
159
|
iss: string;
|
|
149
160
|
/** 目标 audience */
|
|
150
161
|
aud: string;
|
|
151
|
-
/** 用户 ID */
|
|
152
|
-
sub
|
|
162
|
+
/** 用户 ID(Back-Channel Logout 1.0:sub 与 sid 至少其一) */
|
|
163
|
+
sub?: string;
|
|
153
164
|
/** 签发时间(UNIX timestamp) */
|
|
154
165
|
iat: number;
|
|
155
166
|
/** JWT ID(唯一标识,防重放) */
|
|
@@ -200,6 +211,7 @@ declare function createTokenVerifier(options: SsoVerifierOptions): {
|
|
|
200
211
|
* 3. aud 包含当前 client_id
|
|
201
212
|
* 4. exp 存在(规范要求)
|
|
202
213
|
* 5. events 包含 backchannel-logout 事件,且事件值为对象
|
|
214
|
+
* 6. sub 或 sid 至少其一(§2.4,否则无法定位要终止的会话)
|
|
203
215
|
*
|
|
204
216
|
* @returns LogoutTokenPayload 或 null(验证失败)
|
|
205
217
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -111,6 +111,17 @@ interface SsoVerifierOptions {
|
|
|
111
111
|
* 多实例部署时应注入共享存储(如 Redis 实现)以防跨实例重放。
|
|
112
112
|
*/
|
|
113
113
|
logoutJtiStore?: LogoutJtiStore;
|
|
114
|
+
/**
|
|
115
|
+
* Introspection audience 严格模式(默认 false,保持向后兼容)。
|
|
116
|
+
*
|
|
117
|
+
* 默认模式下,introspection 响应既无 aud 又无 client_id 时保持信任
|
|
118
|
+
* (兼容不返回归属字段的端点)。设为 true 后:响应缺少归属字段即拒绝
|
|
119
|
+
* (fail-closed),防止被 confused deputy 攻击利用。
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ 生产环境强烈建议开启。主站 introspection 端点始终返回 client_id,
|
|
122
|
+
* 开启后行为不变;仅在对接不返回归属字段的第三方端点时才需要保持 false。
|
|
123
|
+
*/
|
|
124
|
+
strictAudience?: boolean;
|
|
114
125
|
}
|
|
115
126
|
/**
|
|
116
127
|
* Logout Token jti 防重放存储接口(可注入 Redis 等共享存储实现)。
|
|
@@ -136,7 +147,7 @@ interface VerifiedTokenPayload extends JWTPayload {
|
|
|
136
147
|
* 主站签发 logout_token 时包含以下字段:
|
|
137
148
|
* - iss: 主站 issuer
|
|
138
149
|
* - aud: 目标 client_id
|
|
139
|
-
* - sub: 用户 ID
|
|
150
|
+
* - sub: 用户 ID(与 sid 至少其一)
|
|
140
151
|
* - iat: 签发时间
|
|
141
152
|
* - jti: 唯一 ID(防重放,建议接收方缓存已处理的 jti)
|
|
142
153
|
* - events: { "http://schemas.openid.net/event/backchannel-logout": {} }
|
|
@@ -148,8 +159,8 @@ interface LogoutTokenPayload {
|
|
|
148
159
|
iss: string;
|
|
149
160
|
/** 目标 audience */
|
|
150
161
|
aud: string;
|
|
151
|
-
/** 用户 ID */
|
|
152
|
-
sub
|
|
162
|
+
/** 用户 ID(Back-Channel Logout 1.0:sub 与 sid 至少其一) */
|
|
163
|
+
sub?: string;
|
|
153
164
|
/** 签发时间(UNIX timestamp) */
|
|
154
165
|
iat: number;
|
|
155
166
|
/** JWT ID(唯一标识,防重放) */
|
|
@@ -200,6 +211,7 @@ declare function createTokenVerifier(options: SsoVerifierOptions): {
|
|
|
200
211
|
* 3. aud 包含当前 client_id
|
|
201
212
|
* 4. exp 存在(规范要求)
|
|
202
213
|
* 5. events 包含 backchannel-logout 事件,且事件值为对象
|
|
214
|
+
* 6. sub 或 sid 至少其一(§2.4,否则无法定位要终止的会话)
|
|
203
215
|
*
|
|
204
216
|
* @returns LogoutTokenPayload 或 null(验证失败)
|
|
205
217
|
*/
|
package/dist/index.js
CHANGED
|
@@ -54,7 +54,8 @@ function createTokenVerifier(options) {
|
|
|
54
54
|
introspectNegativeCacheTtl = 5 * 1e3,
|
|
55
55
|
introspectRetries = 1,
|
|
56
56
|
clockToleranceSeconds = 60,
|
|
57
|
-
logoutJtiStore
|
|
57
|
+
logoutJtiStore,
|
|
58
|
+
strictAudience = false
|
|
58
59
|
} = options;
|
|
59
60
|
const introspectCache = createIntrospectCache(introspectCacheTtl);
|
|
60
61
|
let _jwksKeySet = null;
|
|
@@ -107,7 +108,7 @@ function createTokenVerifier(options) {
|
|
|
107
108
|
if (typeof aud === "string") return aud === audience;
|
|
108
109
|
if (Array.isArray(aud)) return aud.includes(audience);
|
|
109
110
|
if (typeof data.client_id === "string") return data.client_id === audience;
|
|
110
|
-
return
|
|
111
|
+
return !strictAudience;
|
|
111
112
|
}
|
|
112
113
|
const inflightIntrospects = /* @__PURE__ */ new Map();
|
|
113
114
|
async function introspect(token) {
|
|
@@ -285,6 +286,7 @@ function createTokenVerifier(options) {
|
|
|
285
286
|
* 3. aud 包含当前 client_id
|
|
286
287
|
* 4. exp 存在(规范要求)
|
|
287
288
|
* 5. events 包含 backchannel-logout 事件,且事件值为对象
|
|
289
|
+
* 6. sub 或 sid 至少其一(§2.4,否则无法定位要终止的会话)
|
|
288
290
|
*
|
|
289
291
|
* @returns LogoutTokenPayload 或 null(验证失败)
|
|
290
292
|
*/
|
|
@@ -305,6 +307,13 @@ function createTokenVerifier(options) {
|
|
|
305
307
|
if (!jti || typeof jti !== "string") {
|
|
306
308
|
return null;
|
|
307
309
|
}
|
|
310
|
+
const sub = payload.sub;
|
|
311
|
+
const sid = payload.sid;
|
|
312
|
+
const hasSub = typeof sub === "string" && sub.length > 0;
|
|
313
|
+
const hasSid = typeof sid === "string" && sid.length > 0;
|
|
314
|
+
if (!hasSub && !hasSid) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
308
317
|
const iss = payload.iss || issuer;
|
|
309
318
|
const jtiKey = `${iss}:${jti}`;
|
|
310
319
|
if (logoutJtiStore) {
|
package/dist/index.mjs
CHANGED
|
@@ -33,7 +33,8 @@ function createTokenVerifier(options) {
|
|
|
33
33
|
introspectNegativeCacheTtl = 5 * 1e3,
|
|
34
34
|
introspectRetries = 1,
|
|
35
35
|
clockToleranceSeconds = 60,
|
|
36
|
-
logoutJtiStore
|
|
36
|
+
logoutJtiStore,
|
|
37
|
+
strictAudience = false
|
|
37
38
|
} = options;
|
|
38
39
|
const introspectCache = createIntrospectCache(introspectCacheTtl);
|
|
39
40
|
let _jwksKeySet = null;
|
|
@@ -86,7 +87,7 @@ function createTokenVerifier(options) {
|
|
|
86
87
|
if (typeof aud === "string") return aud === audience;
|
|
87
88
|
if (Array.isArray(aud)) return aud.includes(audience);
|
|
88
89
|
if (typeof data.client_id === "string") return data.client_id === audience;
|
|
89
|
-
return
|
|
90
|
+
return !strictAudience;
|
|
90
91
|
}
|
|
91
92
|
const inflightIntrospects = /* @__PURE__ */ new Map();
|
|
92
93
|
async function introspect(token) {
|
|
@@ -264,6 +265,7 @@ function createTokenVerifier(options) {
|
|
|
264
265
|
* 3. aud 包含当前 client_id
|
|
265
266
|
* 4. exp 存在(规范要求)
|
|
266
267
|
* 5. events 包含 backchannel-logout 事件,且事件值为对象
|
|
268
|
+
* 6. sub 或 sid 至少其一(§2.4,否则无法定位要终止的会话)
|
|
267
269
|
*
|
|
268
270
|
* @returns LogoutTokenPayload 或 null(验证失败)
|
|
269
271
|
*/
|
|
@@ -284,6 +286,13 @@ function createTokenVerifier(options) {
|
|
|
284
286
|
if (!jti || typeof jti !== "string") {
|
|
285
287
|
return null;
|
|
286
288
|
}
|
|
289
|
+
const sub = payload.sub;
|
|
290
|
+
const sid = payload.sid;
|
|
291
|
+
const hasSub = typeof sub === "string" && sub.length > 0;
|
|
292
|
+
const hasSid = typeof sid === "string" && sid.length > 0;
|
|
293
|
+
if (!hasSub && !hasSid) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
287
296
|
const iss = payload.iss || issuer;
|
|
288
297
|
const jtiKey = `${iss}:${jti}`;
|
|
289
298
|
if (logoutJtiStore) {
|