@nihplod/sso-verify 1.1.0 → 1.1.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 +39 -4
- package/dist/index.d.mts +42 -10
- package/dist/index.d.ts +42 -10
- package/dist/index.js +109 -44
- package/dist/index.mjs +109 -44
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -86,9 +86,9 @@ After successful verification, the token payload is attached to `req.user`.
|
|
|
86
86
|
|
|
87
87
|
## Notes
|
|
88
88
|
|
|
89
|
-
- The main site
|
|
90
|
-
- If your sub-project is an internal service and
|
|
91
|
-
- **Never distribute `JWT_ACCESS_SECRET` (the HS256 symmetric key) to external sub-projects.**
|
|
89
|
+
- The main site signs access tokens with **RS256 when `JWT_ACCESS_PRIVATE_KEY` is configured (recommended, enables local verification via JWKS)**, and falls back to HS256 symmetric signing when it is not. For external sub-projects, we recommend verifying access tokens via **Introspection** regardless of the signing algorithm — it always works and reflects revocations in real time.
|
|
90
|
+
- If your sub-project is an internal service and the main site has RS256 enabled, you can configure `accessTokenPublicKey` or `jwksUri` for local JWT verification.
|
|
91
|
+
- **Never distribute `JWT_ACCESS_SECRET` (the HS256 symmetric key) to external sub-projects.** `accessTokenSecret` local verification only applies to internal services that legitimately share the HS256 secret.
|
|
92
92
|
|
|
93
93
|
## Advanced Options
|
|
94
94
|
|
|
@@ -97,15 +97,50 @@ After successful verification, the token payload is attached to `req.user`.
|
|
|
97
97
|
| `introspectCacheTtl` | `30000` | Cache TTL (ms) for `active: true` introspection results |
|
|
98
98
|
| `introspectNegativeCacheTtl` | `5000` | Cache TTL (ms) for `active: false` results; set `0` to disable caching of revoked/invalid tokens |
|
|
99
99
|
| `introspectTimeoutMs` | `10000` | Introspection request timeout (ms); a timeout is treated as verification failure |
|
|
100
|
+
| `introspectRetries` | `1` | Retries for introspection requests on network errors / 5xx (short backoff); 4xx is never retried; set `0` to disable |
|
|
101
|
+
| `clockToleranceSeconds` | `60` | Clock skew tolerance (seconds) applied to all local JWT verification paths (HS256 / RS256 / logout token) |
|
|
100
102
|
| `accessTokenPublicKey` | — | RS256 public key (PEM) for local access token verification |
|
|
101
103
|
| `jwksUri` | — | JWKS endpoint URL; public keys are matched by `kid` |
|
|
102
104
|
| `logoutTokenPublicKey` | — | RS256 public key (PEM) for back-channel `logout_token` verification |
|
|
103
105
|
| `logoutTokenSecret` | — | HS256 secret for local `logout_token` verification |
|
|
106
|
+
| `logoutJtiStore` | — | External store for processed `logout_token` jtis (see "Multi-instance deployments" below) |
|
|
104
107
|
|
|
105
108
|
### Revocation latency trade-off
|
|
106
109
|
|
|
107
110
|
Introspection results are cached to reduce latency and load on the main site. This means a revoked access token may remain accepted locally until its cache entry expires. `active: true` results are cached for `introspectCacheTtl` (default 30s); `active: false` results use the much shorter `introspectNegativeCacheTtl` (default 5s), so a previously-invalid token that becomes valid (e.g. re-issued) converges quickly, while revocation of an already-cached token takes at most `introspectCacheTtl`. Lower `introspectCacheTtl` for stronger revocation guarantees at the cost of more introspection calls. Use `verifier.invalidateCache(token)` to evict a specific token immediately.
|
|
108
111
|
|
|
112
|
+
Concurrent `verify()` calls for the same token share a single in-flight introspection request, and failed requests (network errors / 5xx) are retried once by default — see `introspectRetries`.
|
|
113
|
+
|
|
114
|
+
### Audience (aud) binding for introspection
|
|
115
|
+
|
|
116
|
+
When verifying via introspection, the response's audience binding is checked against the configured `audience` (which is the sub-project's `client_id`):
|
|
117
|
+
|
|
118
|
+
- If the response contains `aud` (string or array), it must include the configured `audience`.
|
|
119
|
+
- Otherwise, if the response contains `client_id`, it must equal the configured `audience`.
|
|
120
|
+
- 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
|
+
|
|
109
122
|
### Logout token verification
|
|
110
123
|
|
|
111
|
-
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.
|
|
124
|
+
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
|
+
|
|
126
|
+
Per OIDC Back-Channel Logout 1.0, a `logout_token` must carry an `exp` claim and an `events` claim whose `http://schemas.openid.net/event/backchannel-logout` member is an object (typically `{}`); tokens failing either check are rejected. Verified `jti`s are replay-guarded per issuer for 10 minutes.
|
|
127
|
+
|
|
128
|
+
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
|
+
|
|
130
|
+
### Multi-instance deployments (logout jti replay store)
|
|
131
|
+
|
|
132
|
+
By default, processed `logout_token` jtis are kept in an in-process LRU cache — it is emptied on restart and **not shared across instances**, so a replayed logout token could be accepted by another instance. When running multiple instances, inject a shared store (e.g. backed by Redis) via `logoutJtiStore`:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const verifier = createTokenVerifier({
|
|
136
|
+
audience: "your-client-id",
|
|
137
|
+
logoutTokenPublicKey: process.env.LOGOUT_TOKEN_PUBLIC_KEY,
|
|
138
|
+
logoutJtiStore: {
|
|
139
|
+
// Both sync and async implementations are supported.
|
|
140
|
+
has: (key) => redis.exists(key).then((n) => n > 0),
|
|
141
|
+
add: (key, ttlSeconds) => redis.set(key, "1", "EX", ttlSeconds),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`has(key)` reports whether a jti was already processed; `add(key, ttlSeconds)` records it with expiry (the verifier passes 600 seconds, matching the built-in cache).
|
package/dist/index.d.mts
CHANGED
|
@@ -7,10 +7,11 @@ import { JWTPayload } from 'jose';
|
|
|
7
7
|
*
|
|
8
8
|
* 供子项目在后端验证主站签发的 Access Token。
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* 由于主站 access_token 的签名算法取决于部署配置(配置 JWT_ACCESS_PRIVATE_KEY
|
|
11
|
+
* 后以 RS256 签名,未配置时回退 HS256 对称签名),JWKS 端点仅在 RS256
|
|
12
|
+
* 模式下公开公钥,因此本工具包默认采用 OAuth 2.0 Token Introspection
|
|
13
|
+
* (RFC 7662)端点进行验证。对于已知共享密钥/公钥的内部服务,也可传入
|
|
14
|
+
* accessTokenSecret(HS256)或 accessTokenPublicKey / jwksUri(RS256)做本地 JWT 验证。
|
|
14
15
|
*
|
|
15
16
|
* 安装:npm install @nihplod/sso-verify
|
|
16
17
|
*
|
|
@@ -38,7 +39,7 @@ interface SsoVerifierOptions {
|
|
|
38
39
|
issuer?: string;
|
|
39
40
|
/**
|
|
40
41
|
* Token Introspection 端点 URL。
|
|
41
|
-
*
|
|
42
|
+
* 推荐优先使用,与主站签名算法(HS256/RS256)无关,且能实时感知撤销。
|
|
42
43
|
*/
|
|
43
44
|
introspectionEndpoint?: string;
|
|
44
45
|
/** 子项目的 client_id(用于 introspection) */
|
|
@@ -66,8 +67,9 @@ interface SsoVerifierOptions {
|
|
|
66
67
|
introspectNegativeCacheTtl?: number;
|
|
67
68
|
/**
|
|
68
69
|
* Access Token RS256 公钥(PEM 格式,可选)。
|
|
69
|
-
*
|
|
70
|
-
*
|
|
70
|
+
* 主站在配置 JWT_ACCESS_PRIVATE_KEY 后以 RS256 签名 access_token,
|
|
71
|
+
* 子项目可传入此公钥进行本地验证,避免每次都调用 Introspection 端点。
|
|
72
|
+
* 若主站未配置 RS256 密钥(回退 HS256 签名),此选项不生效。
|
|
71
73
|
*/
|
|
72
74
|
accessTokenPublicKey?: string;
|
|
73
75
|
/**
|
|
@@ -91,6 +93,34 @@ interface SsoVerifierOptions {
|
|
|
91
93
|
* 不会静默回退到 HS256。
|
|
92
94
|
*/
|
|
93
95
|
logoutTokenPublicKey?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Introspection 请求失败重试次数,默认 1。
|
|
98
|
+
* 仅对网络错误与 5xx 响应重试(短退避),4xx 不重试。
|
|
99
|
+
* 设为 0 表示不重试。
|
|
100
|
+
*/
|
|
101
|
+
introspectRetries?: number;
|
|
102
|
+
/**
|
|
103
|
+
* JWT 时钟偏移容忍(秒),默认 60。
|
|
104
|
+
* 应用于所有本地验签路径(HS256/RS256/logout token),
|
|
105
|
+
* 用于容忍子项目与主站之间的时钟偏差。
|
|
106
|
+
*/
|
|
107
|
+
clockToleranceSeconds?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Logout Token jti 外部存储(可选)。
|
|
110
|
+
* 默认使用进程内 LRU 缓存(重启即清空、多实例不共享);
|
|
111
|
+
* 多实例部署时应注入共享存储(如 Redis 实现)以防跨实例重放。
|
|
112
|
+
*/
|
|
113
|
+
logoutJtiStore?: LogoutJtiStore;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Logout Token jti 防重放存储接口(可注入 Redis 等共享存储实现)。
|
|
117
|
+
* has/add 均支持同步或异步返回。
|
|
118
|
+
*/
|
|
119
|
+
interface LogoutJtiStore {
|
|
120
|
+
/** 判断 jti 是否已处理过 */
|
|
121
|
+
has(key: string): boolean | Promise<boolean>;
|
|
122
|
+
/** 记录已处理的 jti,ttlSeconds 后过期 */
|
|
123
|
+
add(key: string, ttlSeconds: number): void | Promise<void>;
|
|
94
124
|
}
|
|
95
125
|
interface VerifiedTokenPayload extends JWTPayload {
|
|
96
126
|
sub: string;
|
|
@@ -134,6 +164,7 @@ interface LogoutTokenPayload {
|
|
|
134
164
|
interface IntrospectResponse {
|
|
135
165
|
active: boolean;
|
|
136
166
|
sub?: string;
|
|
167
|
+
aud?: string | string[];
|
|
137
168
|
client_id?: string;
|
|
138
169
|
scope?: string;
|
|
139
170
|
exp?: number;
|
|
@@ -167,7 +198,8 @@ declare function createTokenVerifier(options: SsoVerifierOptions): {
|
|
|
167
198
|
* 1. type === "logout_token"
|
|
168
199
|
* 2. iss 匹配配置的 issuer
|
|
169
200
|
* 3. aud 包含当前 client_id
|
|
170
|
-
* 4.
|
|
201
|
+
* 4. exp 存在(规范要求)
|
|
202
|
+
* 5. events 包含 backchannel-logout 事件,且事件值为对象
|
|
171
203
|
*
|
|
172
204
|
* @returns LogoutTokenPayload 或 null(验证失败)
|
|
173
205
|
*/
|
|
@@ -211,7 +243,7 @@ interface SsoMiddlewareResponse {
|
|
|
211
243
|
* }
|
|
212
244
|
* ```
|
|
213
245
|
*/
|
|
214
|
-
declare function ssoMiddleware(options: SsoVerifierOptions): (req: SsoMiddlewareRequest, res: SsoMiddlewareResponse, next: () => void) => Promise<void>;
|
|
246
|
+
declare function ssoMiddleware(options: SsoVerifierOptions): (req: SsoMiddlewareRequest, res: SsoMiddlewareResponse, next: (err?: unknown) => void) => Promise<void>;
|
|
215
247
|
/**
|
|
216
248
|
* 创建 Logout Token 专用验证器
|
|
217
249
|
*
|
|
@@ -248,4 +280,4 @@ declare function createLogoutTokenVerifier(options: SsoVerifierOptions): {
|
|
|
248
280
|
verify(token: string): Promise<LogoutTokenPayload | null>;
|
|
249
281
|
};
|
|
250
282
|
|
|
251
|
-
export { type LogoutTokenPayload, type SsoMiddlewareRequest, type SsoMiddlewareResponse, type SsoVerifierOptions, type VerifiedTokenPayload, createLogoutTokenVerifier, createTokenVerifier, ssoMiddleware };
|
|
283
|
+
export { type LogoutJtiStore, type LogoutTokenPayload, type SsoMiddlewareRequest, type SsoMiddlewareResponse, type SsoVerifierOptions, type VerifiedTokenPayload, createLogoutTokenVerifier, createTokenVerifier, ssoMiddleware };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,10 +7,11 @@ import { JWTPayload } from 'jose';
|
|
|
7
7
|
*
|
|
8
8
|
* 供子项目在后端验证主站签发的 Access Token。
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* 由于主站 access_token 的签名算法取决于部署配置(配置 JWT_ACCESS_PRIVATE_KEY
|
|
11
|
+
* 后以 RS256 签名,未配置时回退 HS256 对称签名),JWKS 端点仅在 RS256
|
|
12
|
+
* 模式下公开公钥,因此本工具包默认采用 OAuth 2.0 Token Introspection
|
|
13
|
+
* (RFC 7662)端点进行验证。对于已知共享密钥/公钥的内部服务,也可传入
|
|
14
|
+
* accessTokenSecret(HS256)或 accessTokenPublicKey / jwksUri(RS256)做本地 JWT 验证。
|
|
14
15
|
*
|
|
15
16
|
* 安装:npm install @nihplod/sso-verify
|
|
16
17
|
*
|
|
@@ -38,7 +39,7 @@ interface SsoVerifierOptions {
|
|
|
38
39
|
issuer?: string;
|
|
39
40
|
/**
|
|
40
41
|
* Token Introspection 端点 URL。
|
|
41
|
-
*
|
|
42
|
+
* 推荐优先使用,与主站签名算法(HS256/RS256)无关,且能实时感知撤销。
|
|
42
43
|
*/
|
|
43
44
|
introspectionEndpoint?: string;
|
|
44
45
|
/** 子项目的 client_id(用于 introspection) */
|
|
@@ -66,8 +67,9 @@ interface SsoVerifierOptions {
|
|
|
66
67
|
introspectNegativeCacheTtl?: number;
|
|
67
68
|
/**
|
|
68
69
|
* Access Token RS256 公钥(PEM 格式,可选)。
|
|
69
|
-
*
|
|
70
|
-
*
|
|
70
|
+
* 主站在配置 JWT_ACCESS_PRIVATE_KEY 后以 RS256 签名 access_token,
|
|
71
|
+
* 子项目可传入此公钥进行本地验证,避免每次都调用 Introspection 端点。
|
|
72
|
+
* 若主站未配置 RS256 密钥(回退 HS256 签名),此选项不生效。
|
|
71
73
|
*/
|
|
72
74
|
accessTokenPublicKey?: string;
|
|
73
75
|
/**
|
|
@@ -91,6 +93,34 @@ interface SsoVerifierOptions {
|
|
|
91
93
|
* 不会静默回退到 HS256。
|
|
92
94
|
*/
|
|
93
95
|
logoutTokenPublicKey?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Introspection 请求失败重试次数,默认 1。
|
|
98
|
+
* 仅对网络错误与 5xx 响应重试(短退避),4xx 不重试。
|
|
99
|
+
* 设为 0 表示不重试。
|
|
100
|
+
*/
|
|
101
|
+
introspectRetries?: number;
|
|
102
|
+
/**
|
|
103
|
+
* JWT 时钟偏移容忍(秒),默认 60。
|
|
104
|
+
* 应用于所有本地验签路径(HS256/RS256/logout token),
|
|
105
|
+
* 用于容忍子项目与主站之间的时钟偏差。
|
|
106
|
+
*/
|
|
107
|
+
clockToleranceSeconds?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Logout Token jti 外部存储(可选)。
|
|
110
|
+
* 默认使用进程内 LRU 缓存(重启即清空、多实例不共享);
|
|
111
|
+
* 多实例部署时应注入共享存储(如 Redis 实现)以防跨实例重放。
|
|
112
|
+
*/
|
|
113
|
+
logoutJtiStore?: LogoutJtiStore;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Logout Token jti 防重放存储接口(可注入 Redis 等共享存储实现)。
|
|
117
|
+
* has/add 均支持同步或异步返回。
|
|
118
|
+
*/
|
|
119
|
+
interface LogoutJtiStore {
|
|
120
|
+
/** 判断 jti 是否已处理过 */
|
|
121
|
+
has(key: string): boolean | Promise<boolean>;
|
|
122
|
+
/** 记录已处理的 jti,ttlSeconds 后过期 */
|
|
123
|
+
add(key: string, ttlSeconds: number): void | Promise<void>;
|
|
94
124
|
}
|
|
95
125
|
interface VerifiedTokenPayload extends JWTPayload {
|
|
96
126
|
sub: string;
|
|
@@ -134,6 +164,7 @@ interface LogoutTokenPayload {
|
|
|
134
164
|
interface IntrospectResponse {
|
|
135
165
|
active: boolean;
|
|
136
166
|
sub?: string;
|
|
167
|
+
aud?: string | string[];
|
|
137
168
|
client_id?: string;
|
|
138
169
|
scope?: string;
|
|
139
170
|
exp?: number;
|
|
@@ -167,7 +198,8 @@ declare function createTokenVerifier(options: SsoVerifierOptions): {
|
|
|
167
198
|
* 1. type === "logout_token"
|
|
168
199
|
* 2. iss 匹配配置的 issuer
|
|
169
200
|
* 3. aud 包含当前 client_id
|
|
170
|
-
* 4.
|
|
201
|
+
* 4. exp 存在(规范要求)
|
|
202
|
+
* 5. events 包含 backchannel-logout 事件,且事件值为对象
|
|
171
203
|
*
|
|
172
204
|
* @returns LogoutTokenPayload 或 null(验证失败)
|
|
173
205
|
*/
|
|
@@ -211,7 +243,7 @@ interface SsoMiddlewareResponse {
|
|
|
211
243
|
* }
|
|
212
244
|
* ```
|
|
213
245
|
*/
|
|
214
|
-
declare function ssoMiddleware(options: SsoVerifierOptions): (req: SsoMiddlewareRequest, res: SsoMiddlewareResponse, next: () => void) => Promise<void>;
|
|
246
|
+
declare function ssoMiddleware(options: SsoVerifierOptions): (req: SsoMiddlewareRequest, res: SsoMiddlewareResponse, next: (err?: unknown) => void) => Promise<void>;
|
|
215
247
|
/**
|
|
216
248
|
* 创建 Logout Token 专用验证器
|
|
217
249
|
*
|
|
@@ -248,4 +280,4 @@ declare function createLogoutTokenVerifier(options: SsoVerifierOptions): {
|
|
|
248
280
|
verify(token: string): Promise<LogoutTokenPayload | null>;
|
|
249
281
|
};
|
|
250
282
|
|
|
251
|
-
export { type LogoutTokenPayload, type SsoMiddlewareRequest, type SsoMiddlewareResponse, type SsoVerifierOptions, type VerifiedTokenPayload, createLogoutTokenVerifier, createTokenVerifier, ssoMiddleware };
|
|
283
|
+
export { type LogoutJtiStore, type LogoutTokenPayload, type SsoMiddlewareRequest, type SsoMiddlewareResponse, type SsoVerifierOptions, type VerifiedTokenPayload, createLogoutTokenVerifier, createTokenVerifier, ssoMiddleware };
|
package/dist/index.js
CHANGED
|
@@ -51,7 +51,10 @@ function createTokenVerifier(options) {
|
|
|
51
51
|
logoutTokenPublicKey,
|
|
52
52
|
introspectCacheTtl = 30 * 1e3,
|
|
53
53
|
introspectTimeoutMs = 10 * 1e3,
|
|
54
|
-
introspectNegativeCacheTtl = 5 * 1e3
|
|
54
|
+
introspectNegativeCacheTtl = 5 * 1e3,
|
|
55
|
+
introspectRetries = 1,
|
|
56
|
+
clockToleranceSeconds = 60,
|
|
57
|
+
logoutJtiStore
|
|
55
58
|
} = options;
|
|
56
59
|
const introspectCache = createIntrospectCache(introspectCacheTtl);
|
|
57
60
|
let _jwksKeySet = null;
|
|
@@ -99,6 +102,14 @@ function createTokenVerifier(options) {
|
|
|
99
102
|
}
|
|
100
103
|
return _logoutRs256PublicKey;
|
|
101
104
|
}
|
|
105
|
+
function matchesAudience(data) {
|
|
106
|
+
const aud = data.aud;
|
|
107
|
+
if (typeof aud === "string") return aud === audience;
|
|
108
|
+
if (Array.isArray(aud)) return aud.includes(audience);
|
|
109
|
+
if (typeof data.client_id === "string") return data.client_id === audience;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const inflightIntrospects = /* @__PURE__ */ new Map();
|
|
102
113
|
async function introspect(token) {
|
|
103
114
|
if (!introspectionEndpoint || !clientId) {
|
|
104
115
|
return null;
|
|
@@ -107,41 +118,73 @@ function createTokenVerifier(options) {
|
|
|
107
118
|
if (cached) {
|
|
108
119
|
return { active: cached.active, ...cached.payload || {} };
|
|
109
120
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
const inflight = inflightIntrospects.get(token);
|
|
122
|
+
if (inflight) {
|
|
123
|
+
return inflight;
|
|
124
|
+
}
|
|
125
|
+
const promise = doIntrospect(token).finally(() => {
|
|
126
|
+
inflightIntrospects.delete(token);
|
|
127
|
+
});
|
|
128
|
+
inflightIntrospects.set(token, promise);
|
|
129
|
+
return promise;
|
|
130
|
+
}
|
|
131
|
+
async function doIntrospect(token) {
|
|
132
|
+
const params = new URLSearchParams({ token, client_id: clientId });
|
|
133
|
+
if (clientSecret) {
|
|
134
|
+
params.set("client_secret", clientSecret);
|
|
135
|
+
}
|
|
136
|
+
let data = null;
|
|
137
|
+
for (let attempt = 0; ; attempt++) {
|
|
138
|
+
if (attempt > 0) {
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, 200 * attempt));
|
|
114
140
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
sub: data.sub,
|
|
127
|
-
aud: audience,
|
|
128
|
-
iss: issuer,
|
|
129
|
-
client_id: data.client_id,
|
|
130
|
-
scope: data.scope,
|
|
131
|
-
exp: data.exp
|
|
132
|
-
};
|
|
141
|
+
let response;
|
|
142
|
+
try {
|
|
143
|
+
response = await fetch(introspectionEndpoint, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
146
|
+
body: params,
|
|
147
|
+
signal: AbortSignal.timeout(introspectTimeoutMs)
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
if (attempt < introspectRetries) continue;
|
|
151
|
+
return null;
|
|
133
152
|
}
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
if (response.status >= 500 && attempt < introspectRetries) continue;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
data = await response.json();
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
if (!data) return null;
|
|
161
|
+
if (data.active && !matchesAudience(data)) {
|
|
162
|
+
if (introspectNegativeCacheTtl > 0) {
|
|
137
163
|
introspectCache.set(token, { active: false, payload: null }, {
|
|
138
164
|
ttl: introspectNegativeCacheTtl
|
|
139
165
|
});
|
|
140
166
|
}
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
167
|
+
return { active: false };
|
|
168
|
+
}
|
|
169
|
+
let payload = null;
|
|
170
|
+
if (data.active && data.sub) {
|
|
171
|
+
payload = {
|
|
172
|
+
sub: data.sub,
|
|
173
|
+
aud: audience,
|
|
174
|
+
iss: issuer,
|
|
175
|
+
client_id: data.client_id,
|
|
176
|
+
scope: data.scope,
|
|
177
|
+
exp: data.exp
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (data.active) {
|
|
181
|
+
introspectCache.set(token, { active: true, payload });
|
|
182
|
+
} else if (introspectNegativeCacheTtl > 0) {
|
|
183
|
+
introspectCache.set(token, { active: false, payload: null }, {
|
|
184
|
+
ttl: introspectNegativeCacheTtl
|
|
185
|
+
});
|
|
144
186
|
}
|
|
187
|
+
return data;
|
|
145
188
|
}
|
|
146
189
|
async function verifyLocally(token) {
|
|
147
190
|
if (!accessTokenSecret) return null;
|
|
@@ -150,7 +193,8 @@ function createTokenVerifier(options) {
|
|
|
150
193
|
const { payload } = await (0, import_jose.jwtVerify)(token, secret, {
|
|
151
194
|
issuer,
|
|
152
195
|
audience,
|
|
153
|
-
algorithms: ["HS256"]
|
|
196
|
+
algorithms: ["HS256"],
|
|
197
|
+
clockTolerance: clockToleranceSeconds
|
|
154
198
|
});
|
|
155
199
|
if (payload.type !== "access_token") {
|
|
156
200
|
return null;
|
|
@@ -170,7 +214,8 @@ function createTokenVerifier(options) {
|
|
|
170
214
|
const { payload } = await (0, import_jose.jwtVerify)(token, directKey, {
|
|
171
215
|
issuer,
|
|
172
216
|
audience,
|
|
173
|
-
algorithms: ["RS256"]
|
|
217
|
+
algorithms: ["RS256"],
|
|
218
|
+
clockTolerance: clockToleranceSeconds
|
|
174
219
|
});
|
|
175
220
|
if (payload.type !== "access_token") return null;
|
|
176
221
|
return payload;
|
|
@@ -180,7 +225,8 @@ function createTokenVerifier(options) {
|
|
|
180
225
|
const { payload } = await (0, import_jose.jwtVerify)(token, jwks, {
|
|
181
226
|
issuer,
|
|
182
227
|
audience,
|
|
183
|
-
algorithms: ["RS256"]
|
|
228
|
+
algorithms: ["RS256"],
|
|
229
|
+
clockTolerance: clockToleranceSeconds
|
|
184
230
|
});
|
|
185
231
|
if (payload.type !== "access_token") return null;
|
|
186
232
|
return payload;
|
|
@@ -237,17 +283,22 @@ function createTokenVerifier(options) {
|
|
|
237
283
|
* 1. type === "logout_token"
|
|
238
284
|
* 2. iss 匹配配置的 issuer
|
|
239
285
|
* 3. aud 包含当前 client_id
|
|
240
|
-
* 4.
|
|
286
|
+
* 4. exp 存在(规范要求)
|
|
287
|
+
* 5. events 包含 backchannel-logout 事件,且事件值为对象
|
|
241
288
|
*
|
|
242
289
|
* @returns LogoutTokenPayload 或 null(验证失败)
|
|
243
290
|
*/
|
|
244
291
|
async verifyLogoutToken(token) {
|
|
245
|
-
const validateLogoutPayload = (payload) => {
|
|
292
|
+
const validateLogoutPayload = async (payload) => {
|
|
246
293
|
if (payload.type !== "logout_token") {
|
|
247
294
|
return null;
|
|
248
295
|
}
|
|
296
|
+
if (typeof payload.exp !== "number") {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
249
299
|
const events = payload.events;
|
|
250
|
-
|
|
300
|
+
const logoutEvent = events?.["http://schemas.openid.net/event/backchannel-logout"];
|
|
301
|
+
if (!logoutEvent || typeof logoutEvent !== "object" || Array.isArray(logoutEvent)) {
|
|
251
302
|
return null;
|
|
252
303
|
}
|
|
253
304
|
const jti = payload.jti;
|
|
@@ -256,10 +307,17 @@ function createTokenVerifier(options) {
|
|
|
256
307
|
}
|
|
257
308
|
const iss = payload.iss || issuer;
|
|
258
309
|
const jtiKey = `${iss}:${jti}`;
|
|
259
|
-
if (
|
|
260
|
-
|
|
310
|
+
if (logoutJtiStore) {
|
|
311
|
+
if (await logoutJtiStore.has(jtiKey)) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
await logoutJtiStore.add(jtiKey, 10 * 60);
|
|
315
|
+
} else {
|
|
316
|
+
if (processedLogoutJtis.has(jtiKey)) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
processedLogoutJtis.set(jtiKey, Date.now());
|
|
261
320
|
}
|
|
262
|
-
processedLogoutJtis.set(jtiKey, Date.now());
|
|
263
321
|
return payload;
|
|
264
322
|
};
|
|
265
323
|
let alg;
|
|
@@ -275,9 +333,10 @@ function createTokenVerifier(options) {
|
|
|
275
333
|
const { payload } = await (0, import_jose.jwtVerify)(token, directKey, {
|
|
276
334
|
issuer,
|
|
277
335
|
audience,
|
|
278
|
-
algorithms: ["RS256"]
|
|
336
|
+
algorithms: ["RS256"],
|
|
337
|
+
clockTolerance: clockToleranceSeconds
|
|
279
338
|
});
|
|
280
|
-
return validateLogoutPayload(payload);
|
|
339
|
+
return await validateLogoutPayload(payload);
|
|
281
340
|
} catch {
|
|
282
341
|
return null;
|
|
283
342
|
}
|
|
@@ -288,9 +347,10 @@ function createTokenVerifier(options) {
|
|
|
288
347
|
const { payload } = await (0, import_jose.jwtVerify)(token, jwks, {
|
|
289
348
|
issuer,
|
|
290
349
|
audience,
|
|
291
|
-
algorithms: ["RS256"]
|
|
350
|
+
algorithms: ["RS256"],
|
|
351
|
+
clockTolerance: clockToleranceSeconds
|
|
292
352
|
});
|
|
293
|
-
return validateLogoutPayload(payload);
|
|
353
|
+
return await validateLogoutPayload(payload);
|
|
294
354
|
} catch {
|
|
295
355
|
return null;
|
|
296
356
|
}
|
|
@@ -305,9 +365,10 @@ function createTokenVerifier(options) {
|
|
|
305
365
|
const { payload } = await (0, import_jose.jwtVerify)(token, key, {
|
|
306
366
|
issuer,
|
|
307
367
|
audience,
|
|
308
|
-
algorithms: ["HS256"]
|
|
368
|
+
algorithms: ["HS256"],
|
|
369
|
+
clockTolerance: clockToleranceSeconds
|
|
309
370
|
});
|
|
310
|
-
return validateLogoutPayload(payload);
|
|
371
|
+
return await validateLogoutPayload(payload);
|
|
311
372
|
} catch {
|
|
312
373
|
return null;
|
|
313
374
|
}
|
|
@@ -324,14 +385,18 @@ function ssoMiddleware(options) {
|
|
|
324
385
|
if (!token) {
|
|
325
386
|
if (res.status) {
|
|
326
387
|
res.status(401).json({ error: "Unauthorized", message: "\u8BF7\u63D0\u4F9B Bearer token" });
|
|
388
|
+
return;
|
|
327
389
|
}
|
|
390
|
+
next(new Error("Unauthorized: \u8BF7\u63D0\u4F9B Bearer token"));
|
|
328
391
|
return;
|
|
329
392
|
}
|
|
330
393
|
const payload = await verifier.verify(token);
|
|
331
394
|
if (!payload) {
|
|
332
395
|
if (res.status) {
|
|
333
396
|
res.status(401).json({ error: "Unauthorized", message: "Token \u65E0\u6548\u6216\u5DF2\u8FC7\u671F" });
|
|
397
|
+
return;
|
|
334
398
|
}
|
|
399
|
+
next(new Error("Unauthorized: Token \u65E0\u6548\u6216\u5DF2\u8FC7\u671F"));
|
|
335
400
|
return;
|
|
336
401
|
}
|
|
337
402
|
req.user = payload;
|
package/dist/index.mjs
CHANGED
|
@@ -30,7 +30,10 @@ function createTokenVerifier(options) {
|
|
|
30
30
|
logoutTokenPublicKey,
|
|
31
31
|
introspectCacheTtl = 30 * 1e3,
|
|
32
32
|
introspectTimeoutMs = 10 * 1e3,
|
|
33
|
-
introspectNegativeCacheTtl = 5 * 1e3
|
|
33
|
+
introspectNegativeCacheTtl = 5 * 1e3,
|
|
34
|
+
introspectRetries = 1,
|
|
35
|
+
clockToleranceSeconds = 60,
|
|
36
|
+
logoutJtiStore
|
|
34
37
|
} = options;
|
|
35
38
|
const introspectCache = createIntrospectCache(introspectCacheTtl);
|
|
36
39
|
let _jwksKeySet = null;
|
|
@@ -78,6 +81,14 @@ function createTokenVerifier(options) {
|
|
|
78
81
|
}
|
|
79
82
|
return _logoutRs256PublicKey;
|
|
80
83
|
}
|
|
84
|
+
function matchesAudience(data) {
|
|
85
|
+
const aud = data.aud;
|
|
86
|
+
if (typeof aud === "string") return aud === audience;
|
|
87
|
+
if (Array.isArray(aud)) return aud.includes(audience);
|
|
88
|
+
if (typeof data.client_id === "string") return data.client_id === audience;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const inflightIntrospects = /* @__PURE__ */ new Map();
|
|
81
92
|
async function introspect(token) {
|
|
82
93
|
if (!introspectionEndpoint || !clientId) {
|
|
83
94
|
return null;
|
|
@@ -86,41 +97,73 @@ function createTokenVerifier(options) {
|
|
|
86
97
|
if (cached) {
|
|
87
98
|
return { active: cached.active, ...cached.payload || {} };
|
|
88
99
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
const inflight = inflightIntrospects.get(token);
|
|
101
|
+
if (inflight) {
|
|
102
|
+
return inflight;
|
|
103
|
+
}
|
|
104
|
+
const promise = doIntrospect(token).finally(() => {
|
|
105
|
+
inflightIntrospects.delete(token);
|
|
106
|
+
});
|
|
107
|
+
inflightIntrospects.set(token, promise);
|
|
108
|
+
return promise;
|
|
109
|
+
}
|
|
110
|
+
async function doIntrospect(token) {
|
|
111
|
+
const params = new URLSearchParams({ token, client_id: clientId });
|
|
112
|
+
if (clientSecret) {
|
|
113
|
+
params.set("client_secret", clientSecret);
|
|
114
|
+
}
|
|
115
|
+
let data = null;
|
|
116
|
+
for (let attempt = 0; ; attempt++) {
|
|
117
|
+
if (attempt > 0) {
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, 200 * attempt));
|
|
93
119
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
sub: data.sub,
|
|
106
|
-
aud: audience,
|
|
107
|
-
iss: issuer,
|
|
108
|
-
client_id: data.client_id,
|
|
109
|
-
scope: data.scope,
|
|
110
|
-
exp: data.exp
|
|
111
|
-
};
|
|
120
|
+
let response;
|
|
121
|
+
try {
|
|
122
|
+
response = await fetch(introspectionEndpoint, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
125
|
+
body: params,
|
|
126
|
+
signal: AbortSignal.timeout(introspectTimeoutMs)
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
if (attempt < introspectRetries) continue;
|
|
130
|
+
return null;
|
|
112
131
|
}
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
if (response.status >= 500 && attempt < introspectRetries) continue;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
data = await response.json();
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (!data) return null;
|
|
140
|
+
if (data.active && !matchesAudience(data)) {
|
|
141
|
+
if (introspectNegativeCacheTtl > 0) {
|
|
116
142
|
introspectCache.set(token, { active: false, payload: null }, {
|
|
117
143
|
ttl: introspectNegativeCacheTtl
|
|
118
144
|
});
|
|
119
145
|
}
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
|
|
146
|
+
return { active: false };
|
|
147
|
+
}
|
|
148
|
+
let payload = null;
|
|
149
|
+
if (data.active && data.sub) {
|
|
150
|
+
payload = {
|
|
151
|
+
sub: data.sub,
|
|
152
|
+
aud: audience,
|
|
153
|
+
iss: issuer,
|
|
154
|
+
client_id: data.client_id,
|
|
155
|
+
scope: data.scope,
|
|
156
|
+
exp: data.exp
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (data.active) {
|
|
160
|
+
introspectCache.set(token, { active: true, payload });
|
|
161
|
+
} else if (introspectNegativeCacheTtl > 0) {
|
|
162
|
+
introspectCache.set(token, { active: false, payload: null }, {
|
|
163
|
+
ttl: introspectNegativeCacheTtl
|
|
164
|
+
});
|
|
123
165
|
}
|
|
166
|
+
return data;
|
|
124
167
|
}
|
|
125
168
|
async function verifyLocally(token) {
|
|
126
169
|
if (!accessTokenSecret) return null;
|
|
@@ -129,7 +172,8 @@ function createTokenVerifier(options) {
|
|
|
129
172
|
const { payload } = await jwtVerify(token, secret, {
|
|
130
173
|
issuer,
|
|
131
174
|
audience,
|
|
132
|
-
algorithms: ["HS256"]
|
|
175
|
+
algorithms: ["HS256"],
|
|
176
|
+
clockTolerance: clockToleranceSeconds
|
|
133
177
|
});
|
|
134
178
|
if (payload.type !== "access_token") {
|
|
135
179
|
return null;
|
|
@@ -149,7 +193,8 @@ function createTokenVerifier(options) {
|
|
|
149
193
|
const { payload } = await jwtVerify(token, directKey, {
|
|
150
194
|
issuer,
|
|
151
195
|
audience,
|
|
152
|
-
algorithms: ["RS256"]
|
|
196
|
+
algorithms: ["RS256"],
|
|
197
|
+
clockTolerance: clockToleranceSeconds
|
|
153
198
|
});
|
|
154
199
|
if (payload.type !== "access_token") return null;
|
|
155
200
|
return payload;
|
|
@@ -159,7 +204,8 @@ function createTokenVerifier(options) {
|
|
|
159
204
|
const { payload } = await jwtVerify(token, jwks, {
|
|
160
205
|
issuer,
|
|
161
206
|
audience,
|
|
162
|
-
algorithms: ["RS256"]
|
|
207
|
+
algorithms: ["RS256"],
|
|
208
|
+
clockTolerance: clockToleranceSeconds
|
|
163
209
|
});
|
|
164
210
|
if (payload.type !== "access_token") return null;
|
|
165
211
|
return payload;
|
|
@@ -216,17 +262,22 @@ function createTokenVerifier(options) {
|
|
|
216
262
|
* 1. type === "logout_token"
|
|
217
263
|
* 2. iss 匹配配置的 issuer
|
|
218
264
|
* 3. aud 包含当前 client_id
|
|
219
|
-
* 4.
|
|
265
|
+
* 4. exp 存在(规范要求)
|
|
266
|
+
* 5. events 包含 backchannel-logout 事件,且事件值为对象
|
|
220
267
|
*
|
|
221
268
|
* @returns LogoutTokenPayload 或 null(验证失败)
|
|
222
269
|
*/
|
|
223
270
|
async verifyLogoutToken(token) {
|
|
224
|
-
const validateLogoutPayload = (payload) => {
|
|
271
|
+
const validateLogoutPayload = async (payload) => {
|
|
225
272
|
if (payload.type !== "logout_token") {
|
|
226
273
|
return null;
|
|
227
274
|
}
|
|
275
|
+
if (typeof payload.exp !== "number") {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
228
278
|
const events = payload.events;
|
|
229
|
-
|
|
279
|
+
const logoutEvent = events?.["http://schemas.openid.net/event/backchannel-logout"];
|
|
280
|
+
if (!logoutEvent || typeof logoutEvent !== "object" || Array.isArray(logoutEvent)) {
|
|
230
281
|
return null;
|
|
231
282
|
}
|
|
232
283
|
const jti = payload.jti;
|
|
@@ -235,10 +286,17 @@ function createTokenVerifier(options) {
|
|
|
235
286
|
}
|
|
236
287
|
const iss = payload.iss || issuer;
|
|
237
288
|
const jtiKey = `${iss}:${jti}`;
|
|
238
|
-
if (
|
|
239
|
-
|
|
289
|
+
if (logoutJtiStore) {
|
|
290
|
+
if (await logoutJtiStore.has(jtiKey)) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
await logoutJtiStore.add(jtiKey, 10 * 60);
|
|
294
|
+
} else {
|
|
295
|
+
if (processedLogoutJtis.has(jtiKey)) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
processedLogoutJtis.set(jtiKey, Date.now());
|
|
240
299
|
}
|
|
241
|
-
processedLogoutJtis.set(jtiKey, Date.now());
|
|
242
300
|
return payload;
|
|
243
301
|
};
|
|
244
302
|
let alg;
|
|
@@ -254,9 +312,10 @@ function createTokenVerifier(options) {
|
|
|
254
312
|
const { payload } = await jwtVerify(token, directKey, {
|
|
255
313
|
issuer,
|
|
256
314
|
audience,
|
|
257
|
-
algorithms: ["RS256"]
|
|
315
|
+
algorithms: ["RS256"],
|
|
316
|
+
clockTolerance: clockToleranceSeconds
|
|
258
317
|
});
|
|
259
|
-
return validateLogoutPayload(payload);
|
|
318
|
+
return await validateLogoutPayload(payload);
|
|
260
319
|
} catch {
|
|
261
320
|
return null;
|
|
262
321
|
}
|
|
@@ -267,9 +326,10 @@ function createTokenVerifier(options) {
|
|
|
267
326
|
const { payload } = await jwtVerify(token, jwks, {
|
|
268
327
|
issuer,
|
|
269
328
|
audience,
|
|
270
|
-
algorithms: ["RS256"]
|
|
329
|
+
algorithms: ["RS256"],
|
|
330
|
+
clockTolerance: clockToleranceSeconds
|
|
271
331
|
});
|
|
272
|
-
return validateLogoutPayload(payload);
|
|
332
|
+
return await validateLogoutPayload(payload);
|
|
273
333
|
} catch {
|
|
274
334
|
return null;
|
|
275
335
|
}
|
|
@@ -284,9 +344,10 @@ function createTokenVerifier(options) {
|
|
|
284
344
|
const { payload } = await jwtVerify(token, key, {
|
|
285
345
|
issuer,
|
|
286
346
|
audience,
|
|
287
|
-
algorithms: ["HS256"]
|
|
347
|
+
algorithms: ["HS256"],
|
|
348
|
+
clockTolerance: clockToleranceSeconds
|
|
288
349
|
});
|
|
289
|
-
return validateLogoutPayload(payload);
|
|
350
|
+
return await validateLogoutPayload(payload);
|
|
290
351
|
} catch {
|
|
291
352
|
return null;
|
|
292
353
|
}
|
|
@@ -303,14 +364,18 @@ function ssoMiddleware(options) {
|
|
|
303
364
|
if (!token) {
|
|
304
365
|
if (res.status) {
|
|
305
366
|
res.status(401).json({ error: "Unauthorized", message: "\u8BF7\u63D0\u4F9B Bearer token" });
|
|
367
|
+
return;
|
|
306
368
|
}
|
|
369
|
+
next(new Error("Unauthorized: \u8BF7\u63D0\u4F9B Bearer token"));
|
|
307
370
|
return;
|
|
308
371
|
}
|
|
309
372
|
const payload = await verifier.verify(token);
|
|
310
373
|
if (!payload) {
|
|
311
374
|
if (res.status) {
|
|
312
375
|
res.status(401).json({ error: "Unauthorized", message: "Token \u65E0\u6548\u6216\u5DF2\u8FC7\u671F" });
|
|
376
|
+
return;
|
|
313
377
|
}
|
|
378
|
+
next(new Error("Unauthorized: Token \u65E0\u6548\u6216\u5DF2\u8FC7\u671F"));
|
|
314
379
|
return;
|
|
315
380
|
}
|
|
316
381
|
req.user = payload;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nihplod/sso-verify",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "NIHPLOD 一网通 SSO Token 验证工具包 — 供子项目在后端验证主站签发的 Access Token",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -34,9 +34,12 @@
|
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/
|
|
37
|
+
"url": "https://github.com/Wilboerht/NIHPLOD-Homepage.CN",
|
|
38
38
|
"directory": "packages/sso-verify"
|
|
39
39
|
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
40
43
|
"peerDependencies": {},
|
|
41
44
|
"dependencies": {
|
|
42
45
|
"jose": "^5.0.0",
|