@nihplod/sso-sdk 1.4.1 → 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.
@@ -10,6 +10,10 @@ import { NextRequest, NextResponse } from 'next/server';
10
10
  * - PKCE code_challenge 使用 Web Crypto API 的 crypto.subtle.digest(SHA-256)
11
11
  * (Edge Runtime 18+ 完全支持此 API)
12
12
  *
13
+ * ⚠️ 安全须知:middleware 只是 UX 层(未认证重定向的入口优化),
14
+ * 不能作为安全边界。敏感接口必须在 Route Handler / Server Component 中
15
+ * 用 @nihplod/sso-verify 对 access_token 做二次校验。
16
+ *
13
17
  * 用法 (src/middleware.ts):
14
18
  * ```ts
15
19
  * import { createSsoMiddleware } from "@nihplod/sso-sdk/next";
@@ -32,13 +36,25 @@ interface SsoMiddlewareConfig {
32
36
  redirectUri: string;
33
37
  /** 请求的 scope(空格分隔),默认 "openid profile" */
34
38
  scopes?: string;
35
- /** 公开路由前缀(不需要认证) */
39
+ /**
40
+ * 公开路由前缀(不需要认证)。
41
+ *
42
+ * 注意:/api/ 下的路径永不按静态资源扩展名(.js/.css/...)放行;
43
+ * 如需公开某个 API 路径,必须在此显式列出。
44
+ */
36
45
  publicPaths?: string[];
37
46
  /** 回调路径(不触发重定向),默认 "/api/auth/callback" */
38
47
  callbackPath?: string;
39
48
  /**
40
49
  * 主站用户会话 Cookie 名称,用于检测是否已有 SSO 会话。
41
50
  * 默认 "__Host-user_token"(与主站 C 端登录 Cookie 一致)。
51
+ *
52
+ * @deprecated 该「主站会话 Cookie 快速通道」已移除,此选项不再生效:
53
+ * 1. `__Host-user_token` 存的是主站内部 token(type="user", aud="user"),
54
+ * introspect 端点只接受 type="access_token",校验必然返回 inactive;
55
+ * 2. `__Host-` Cookie 不会下发到子域/其他域名,跨域子项目根本收不到。
56
+ * 该分支永不命中,却让每个请求白付一次 introspection 往返并被负缓存 30s。
57
+ * 保留字段仅为兼容旧配置,请勿使用;会话检测以本站 access_token Cookie 为准。
42
58
  */
43
59
  ssoCookieName?: string;
44
60
  /**
@@ -51,8 +67,27 @@ interface SsoMiddlewareConfig {
51
67
  * 默认 true(推荐)。设为 false 时仅检查 Cookie 存在性,延迟最低但可能放行
52
68
  * 已失效/被撤销的会话 —— 中间件本质上只是 UX 层,敏感数据必须在
53
69
  * Route Handler / Server Component 中二次校验。
70
+ *
71
+ * @deprecated 随「主站会话 Cookie 快速通道」一并移除(见 ssoCookieName),
72
+ * 此选项不再生效。保留字段仅为兼容旧配置,请勿使用。
54
73
  */
55
74
  validateSsoCookie?: boolean;
75
+ /**
76
+ * Introspection 不可达(网络异常/超时/5xx,token 有效性未确证)时的策略。
77
+ *
78
+ * 默认 false(fail-open):对已持有 access_token Cookie 的请求放行——
79
+ * middleware 只是 UX 层,敏感接口的鉴权由 Route Handler / Server Component
80
+ * 兜底,此时重定向到一个同样不可达的 SSO 中心没有意义。
81
+ *
82
+ * 设为 true(fail-closed)时:对持有 access_token Cookie 的请求返回
83
+ * 502 错误页而不是放行。适用于对可用性敏感、宁可拒绝也不放行的场景。
84
+ * 注意 fail-closed 只影响「不可达」这一种结果;token 确证无效(inactive)
85
+ * 无论该选项如何都会清除 Cookie 并重定向到 SSO 登录。
86
+ *
87
+ * ⚠️ 无论取何值,middleware 都只是 UX 层:敏感接口必须在
88
+ * Route Handler / Server Component 中用 @nihplod/sso-verify 二次校验。
89
+ */
90
+ failClosedOnIntrospectionError?: boolean;
56
91
  /** Access Token Cookie 名称,默认 __Host-nihplod_sso_at */
57
92
  accessTokenCookieName?: string;
58
93
  /** State Cookie 名称,默认 __Host-nihplod_sso_state */
@@ -137,6 +172,12 @@ interface CallbackRouteConfig {
137
172
  returnUrlCookieName?: string;
138
173
  /** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */
139
174
  verifierCookieName?: string;
175
+ /**
176
+ * 请求的 OAuth scope(空格分隔),建议与 createSsoMiddleware 的 scopes 保持一致。
177
+ * 仅当**显式配置**且包含 openid 时回调才强制要求 token 响应携带 id_token(fail-closed);
178
+ * 未配置时保持兼容行为(id_token 存在则校验,缺失不拒绝),避免升级后旧接入方登录失败。
179
+ */
180
+ scopes?: string;
140
181
  /**
141
182
  * 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除
142
183
  * __Host-/__Secure- 前缀;必须与 createSsoMiddleware 的配置保持一致,
@@ -150,6 +191,20 @@ interface CallbackRouteConfig {
150
191
  * 仅用于 token 交换等服务器间请求;浏览器跳转仍使用 ssoBaseUrl 公网地址。
151
192
  */
152
193
  serverBaseUrl?: string;
194
+ /**
195
+ * 自定义回调错误响应(可选)。
196
+ * 传入后优先生效;返回 undefined 时回退默认行为。
197
+ * 默认行为:浏览器导航(Accept 含 text/html 且未带 ?format=json)渲染内置中文错误页
198
+ * (含"重新登录"入口);其他请求(fetch/自动化)返回 JSON,保持 API 兼容。
199
+ */
200
+ renderErrorPage?: (ctx: {
201
+ error: string;
202
+ errorDescription: string;
203
+ status: number;
204
+ request: NextRequest;
205
+ }) => Response | undefined | Promise<Response | undefined>;
206
+ /** 调试日志开关(默认 false):回调失败时输出服务端告警日志,便于定位配置/流程问题 */
207
+ debug?: boolean;
153
208
  }
154
209
  declare function createCallbackRouteHandler(config: CallbackRouteConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
155
210
 
@@ -248,6 +303,28 @@ interface LogoutTokenPayload {
248
303
  sub?: string;
249
304
  sid?: string;
250
305
  }
306
+ /**
307
+ * jti 防重放存储接口(可注入)。
308
+ *
309
+ * 默认实现是进程内 Map(per-instance):多实例 / Serverless 部署下各实例
310
+ * 互不可见,重放检查形同虚设,必须注入共享存储(如 Redis)。
311
+ * key 为 `${issuer}:${jti}`,ttlSeconds 为建议过期时间(logout_token exp + 60s 宽限)。
312
+ */
313
+ interface LogoutJtiStore {
314
+ has(key: string): Promise<boolean> | boolean;
315
+ add(key: string, ttlSeconds: number): Promise<void> | void;
316
+ /**
317
+ * 原子 check-and-set(如 Redis `SET key 1 NX EX ttl`):返回 false 表示
318
+ * key 已存在(判重放)。提供时优先使用;缺失时回退 has+add
319
+ * (非原子,仅单实例部署可接受:并发窗口内同一 jti 可能双双通过)。
320
+ */
321
+ addIfAbsent?(key: string, ttlSeconds: number): Promise<boolean> | boolean;
322
+ /**
323
+ * 释放已记录的 key。onLogout 钩子失败后需要允许 IdP 用同一 logout_token
324
+ * 重投,SDK 会调用此方法;未实现时 IdP 重投将被重放检查以 400 拒绝。
325
+ */
326
+ remove?(key: string): Promise<void> | void;
327
+ }
251
328
 
252
329
  /**
253
330
  * App Router Backchannel Logout 接收端 Route Handler
@@ -290,6 +367,15 @@ interface BackchannelLogoutRouteConfig {
290
367
  refreshTokenCookieName?: string;
291
368
  /** ID Token Cookie 名称,默认 __Host-nihplod_sso_id */
292
369
  idTokenCookieName?: string;
370
+ /**
371
+ * jti 防重放存储(可选)。
372
+ *
373
+ * 默认实现是进程内 Map(per-instance):多实例 / Serverless 部署下各实例
374
+ * 互不可见,重放保护形同虚设(IdP 重投/攻击者重放到另一实例会被当作首次)。
375
+ * 这类部署 MUST 注入共享存储(如 Redis:addIfAbsent = `SET key 1 NX EX ttl`,
376
+ * remove = `DEL key`——onLogout 失败时用于释放 jti 允许 IdP 重投)。
377
+ */
378
+ jtiStore?: LogoutJtiStore;
293
379
  /**
294
380
  * 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除
295
381
  * __Host-/__Secure- 前缀;必须与 middleware / callback / logout 的配置保持一致。
@@ -341,4 +427,4 @@ declare function getSecureCookieOptions(maxAge?: number, path?: string, secure?:
341
427
  maxAge?: number;
342
428
  };
343
429
 
344
- export { type BackchannelLogoutRouteConfig, type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_ID_TOKEN_COOKIE_NAME, DEFAULT_LOGOUT_STATE_COOKIE_NAME, DEFAULT_NONCE_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutRouteConfig, type SsoMiddlewareConfig, createBackchannelLogoutRouteHandler, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions, toInsecureCookieName };
430
+ export { type BackchannelLogoutRouteConfig, type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_ID_TOKEN_COOKIE_NAME, DEFAULT_LOGOUT_STATE_COOKIE_NAME, DEFAULT_NONCE_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutJtiStore, type LogoutRouteConfig, type SsoMiddlewareConfig, createBackchannelLogoutRouteHandler, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions, toInsecureCookieName };
@@ -10,6 +10,10 @@ import { NextRequest, NextResponse } from 'next/server';
10
10
  * - PKCE code_challenge 使用 Web Crypto API 的 crypto.subtle.digest(SHA-256)
11
11
  * (Edge Runtime 18+ 完全支持此 API)
12
12
  *
13
+ * ⚠️ 安全须知:middleware 只是 UX 层(未认证重定向的入口优化),
14
+ * 不能作为安全边界。敏感接口必须在 Route Handler / Server Component 中
15
+ * 用 @nihplod/sso-verify 对 access_token 做二次校验。
16
+ *
13
17
  * 用法 (src/middleware.ts):
14
18
  * ```ts
15
19
  * import { createSsoMiddleware } from "@nihplod/sso-sdk/next";
@@ -32,13 +36,25 @@ interface SsoMiddlewareConfig {
32
36
  redirectUri: string;
33
37
  /** 请求的 scope(空格分隔),默认 "openid profile" */
34
38
  scopes?: string;
35
- /** 公开路由前缀(不需要认证) */
39
+ /**
40
+ * 公开路由前缀(不需要认证)。
41
+ *
42
+ * 注意:/api/ 下的路径永不按静态资源扩展名(.js/.css/...)放行;
43
+ * 如需公开某个 API 路径,必须在此显式列出。
44
+ */
36
45
  publicPaths?: string[];
37
46
  /** 回调路径(不触发重定向),默认 "/api/auth/callback" */
38
47
  callbackPath?: string;
39
48
  /**
40
49
  * 主站用户会话 Cookie 名称,用于检测是否已有 SSO 会话。
41
50
  * 默认 "__Host-user_token"(与主站 C 端登录 Cookie 一致)。
51
+ *
52
+ * @deprecated 该「主站会话 Cookie 快速通道」已移除,此选项不再生效:
53
+ * 1. `__Host-user_token` 存的是主站内部 token(type="user", aud="user"),
54
+ * introspect 端点只接受 type="access_token",校验必然返回 inactive;
55
+ * 2. `__Host-` Cookie 不会下发到子域/其他域名,跨域子项目根本收不到。
56
+ * 该分支永不命中,却让每个请求白付一次 introspection 往返并被负缓存 30s。
57
+ * 保留字段仅为兼容旧配置,请勿使用;会话检测以本站 access_token Cookie 为准。
42
58
  */
43
59
  ssoCookieName?: string;
44
60
  /**
@@ -51,8 +67,27 @@ interface SsoMiddlewareConfig {
51
67
  * 默认 true(推荐)。设为 false 时仅检查 Cookie 存在性,延迟最低但可能放行
52
68
  * 已失效/被撤销的会话 —— 中间件本质上只是 UX 层,敏感数据必须在
53
69
  * Route Handler / Server Component 中二次校验。
70
+ *
71
+ * @deprecated 随「主站会话 Cookie 快速通道」一并移除(见 ssoCookieName),
72
+ * 此选项不再生效。保留字段仅为兼容旧配置,请勿使用。
54
73
  */
55
74
  validateSsoCookie?: boolean;
75
+ /**
76
+ * Introspection 不可达(网络异常/超时/5xx,token 有效性未确证)时的策略。
77
+ *
78
+ * 默认 false(fail-open):对已持有 access_token Cookie 的请求放行——
79
+ * middleware 只是 UX 层,敏感接口的鉴权由 Route Handler / Server Component
80
+ * 兜底,此时重定向到一个同样不可达的 SSO 中心没有意义。
81
+ *
82
+ * 设为 true(fail-closed)时:对持有 access_token Cookie 的请求返回
83
+ * 502 错误页而不是放行。适用于对可用性敏感、宁可拒绝也不放行的场景。
84
+ * 注意 fail-closed 只影响「不可达」这一种结果;token 确证无效(inactive)
85
+ * 无论该选项如何都会清除 Cookie 并重定向到 SSO 登录。
86
+ *
87
+ * ⚠️ 无论取何值,middleware 都只是 UX 层:敏感接口必须在
88
+ * Route Handler / Server Component 中用 @nihplod/sso-verify 二次校验。
89
+ */
90
+ failClosedOnIntrospectionError?: boolean;
56
91
  /** Access Token Cookie 名称,默认 __Host-nihplod_sso_at */
57
92
  accessTokenCookieName?: string;
58
93
  /** State Cookie 名称,默认 __Host-nihplod_sso_state */
@@ -137,6 +172,12 @@ interface CallbackRouteConfig {
137
172
  returnUrlCookieName?: string;
138
173
  /** PKCE Verifier Cookie 名称,默认 __Secure-nihplod_sso_verifier */
139
174
  verifierCookieName?: string;
175
+ /**
176
+ * 请求的 OAuth scope(空格分隔),建议与 createSsoMiddleware 的 scopes 保持一致。
177
+ * 仅当**显式配置**且包含 openid 时回调才强制要求 token 响应携带 id_token(fail-closed);
178
+ * 未配置时保持兼容行为(id_token 存在则校验,缺失不拒绝),避免升级后旧接入方登录失败。
179
+ */
180
+ scopes?: string;
140
181
  /**
141
182
  * 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除
142
183
  * __Host-/__Secure- 前缀;必须与 createSsoMiddleware 的配置保持一致,
@@ -150,6 +191,20 @@ interface CallbackRouteConfig {
150
191
  * 仅用于 token 交换等服务器间请求;浏览器跳转仍使用 ssoBaseUrl 公网地址。
151
192
  */
152
193
  serverBaseUrl?: string;
194
+ /**
195
+ * 自定义回调错误响应(可选)。
196
+ * 传入后优先生效;返回 undefined 时回退默认行为。
197
+ * 默认行为:浏览器导航(Accept 含 text/html 且未带 ?format=json)渲染内置中文错误页
198
+ * (含"重新登录"入口);其他请求(fetch/自动化)返回 JSON,保持 API 兼容。
199
+ */
200
+ renderErrorPage?: (ctx: {
201
+ error: string;
202
+ errorDescription: string;
203
+ status: number;
204
+ request: NextRequest;
205
+ }) => Response | undefined | Promise<Response | undefined>;
206
+ /** 调试日志开关(默认 false):回调失败时输出服务端告警日志,便于定位配置/流程问题 */
207
+ debug?: boolean;
153
208
  }
154
209
  declare function createCallbackRouteHandler(config: CallbackRouteConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
155
210
 
@@ -248,6 +303,28 @@ interface LogoutTokenPayload {
248
303
  sub?: string;
249
304
  sid?: string;
250
305
  }
306
+ /**
307
+ * jti 防重放存储接口(可注入)。
308
+ *
309
+ * 默认实现是进程内 Map(per-instance):多实例 / Serverless 部署下各实例
310
+ * 互不可见,重放检查形同虚设,必须注入共享存储(如 Redis)。
311
+ * key 为 `${issuer}:${jti}`,ttlSeconds 为建议过期时间(logout_token exp + 60s 宽限)。
312
+ */
313
+ interface LogoutJtiStore {
314
+ has(key: string): Promise<boolean> | boolean;
315
+ add(key: string, ttlSeconds: number): Promise<void> | void;
316
+ /**
317
+ * 原子 check-and-set(如 Redis `SET key 1 NX EX ttl`):返回 false 表示
318
+ * key 已存在(判重放)。提供时优先使用;缺失时回退 has+add
319
+ * (非原子,仅单实例部署可接受:并发窗口内同一 jti 可能双双通过)。
320
+ */
321
+ addIfAbsent?(key: string, ttlSeconds: number): Promise<boolean> | boolean;
322
+ /**
323
+ * 释放已记录的 key。onLogout 钩子失败后需要允许 IdP 用同一 logout_token
324
+ * 重投,SDK 会调用此方法;未实现时 IdP 重投将被重放检查以 400 拒绝。
325
+ */
326
+ remove?(key: string): Promise<void> | void;
327
+ }
251
328
 
252
329
  /**
253
330
  * App Router Backchannel Logout 接收端 Route Handler
@@ -290,6 +367,15 @@ interface BackchannelLogoutRouteConfig {
290
367
  refreshTokenCookieName?: string;
291
368
  /** ID Token Cookie 名称,默认 __Host-nihplod_sso_id */
292
369
  idTokenCookieName?: string;
370
+ /**
371
+ * jti 防重放存储(可选)。
372
+ *
373
+ * 默认实现是进程内 Map(per-instance):多实例 / Serverless 部署下各实例
374
+ * 互不可见,重放保护形同虚设(IdP 重投/攻击者重放到另一实例会被当作首次)。
375
+ * 这类部署 MUST 注入共享存储(如 Redis:addIfAbsent = `SET key 1 NX EX ttl`,
376
+ * remove = `DEL key`——onLogout 失败时用于释放 jti 允许 IdP 重投)。
377
+ */
378
+ jtiStore?: LogoutJtiStore;
293
379
  /**
294
380
  * 本地 HTTP 开发模式(默认 false)。关闭 Cookie 的 Secure 属性并去除
295
381
  * __Host-/__Secure- 前缀;必须与 middleware / callback / logout 的配置保持一致。
@@ -341,4 +427,4 @@ declare function getSecureCookieOptions(maxAge?: number, path?: string, secure?:
341
427
  maxAge?: number;
342
428
  };
343
429
 
344
- export { type BackchannelLogoutRouteConfig, type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_ID_TOKEN_COOKIE_NAME, DEFAULT_LOGOUT_STATE_COOKIE_NAME, DEFAULT_NONCE_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutRouteConfig, type SsoMiddlewareConfig, createBackchannelLogoutRouteHandler, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions, toInsecureCookieName };
430
+ export { type BackchannelLogoutRouteConfig, type CallbackRouteConfig, DEFAULT_ACCESS_TOKEN_COOKIE_NAME, DEFAULT_ID_TOKEN_COOKIE_NAME, DEFAULT_LOGOUT_STATE_COOKIE_NAME, DEFAULT_NONCE_COOKIE_NAME, DEFAULT_REFRESH_TOKEN_COOKIE_NAME, DEFAULT_RETURN_COOKIE_NAME, DEFAULT_STATE_COOKIE_NAME, DEFAULT_VERIFIER_COOKIE_NAME, type LogoutJtiStore, type LogoutRouteConfig, type SsoMiddlewareConfig, createBackchannelLogoutRouteHandler, createCallbackRouteHandler, createLogoutRouteHandler, createSsoMiddleware, getHostCookieOptions, getSecureCookieOptions, toInsecureCookieName };