@nihplod/sso-sdk 1.0.1 → 1.2.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 +92 -9
- package/dist/index.d.mts +136 -13
- package/dist/index.d.ts +136 -13
- package/dist/index.js +475 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +467 -77
- package/dist/index.mjs.map +1 -1
- package/dist/next/index.d.mts +54 -6
- package/dist/next/index.d.ts +54 -6
- package/dist/next/index.js +355 -85
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +351 -84
- package/dist/next/index.mjs.map +1 -1
- package/dist/react/index.d.mts +175 -14
- package/dist/react/index.d.ts +175 -14
- package/dist/react/index.js +627 -151
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +630 -155
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react/index.d.ts
CHANGED
|
@@ -3,9 +3,15 @@ import React, { ReactNode, ComponentType } from 'react';
|
|
|
3
3
|
/**
|
|
4
4
|
* Token 存储抽象层
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
6
|
+
* 分两类存储:
|
|
7
|
+
* - Token 数据:默认使用 sessionStorage(标签页级持久化)。登录回调后 CallbackPage
|
|
8
|
+
* 默认整页跳转,内存存储会丢失登录态,因此默认改为 sessionStorage:
|
|
9
|
+
* 整页跳转与刷新后登录态保留,关闭标签页自动清除;SSR / 隐私模式写入失败时
|
|
10
|
+
* 降级为内存 Map。对需要多 Tab 共享 token 或 BFF/Confidential Client 场景,
|
|
11
|
+
* 可通过 setTokenStorage() 注入 localStorage 实现(如 createSecureStorage({ persist: true }))。
|
|
12
|
+
* - 临时数据(PKCE verifier / state / returnUrl / popup nonce):必须跨整页重定向存活
|
|
13
|
+
* (login() 会 302 跳转到 SSO 中心再回来),因此默认写入 sessionStorage;
|
|
14
|
+
* SSR 等无 sessionStorage 环境自动降级为内存 Map。
|
|
9
15
|
*
|
|
10
16
|
* 多 client 隔离:
|
|
11
17
|
* - token / state / return_url 均支持按 clientId 隔离 key
|
|
@@ -54,6 +60,8 @@ interface SsoClientConfig {
|
|
|
54
60
|
ssoBaseUrl: string;
|
|
55
61
|
/** 请求的 scope(空格分隔),如 "openid profile phone" */
|
|
56
62
|
scopes?: string;
|
|
63
|
+
/** RP-Initiated Logout 返回地址(可选)。不传时回退到 redirectUri */
|
|
64
|
+
postLogoutRedirectUri?: string;
|
|
57
65
|
}
|
|
58
66
|
/** 用户信息 */
|
|
59
67
|
interface SsoUser {
|
|
@@ -102,26 +110,78 @@ declare class SsoClient {
|
|
|
102
110
|
private _getTokenEndpoint;
|
|
103
111
|
/** 获取 userinfo 端点 URL(优先 Discovery,回退默认) */
|
|
104
112
|
private _getUserinfoEndpoint;
|
|
113
|
+
/**
|
|
114
|
+
* 开放重定向防护:仅保存相对路径或与当前页面同源的 returnUrl,
|
|
115
|
+
* 其余(如 https://evil.com)忽略并告警,防止回调后跳转到钓鱼站点。
|
|
116
|
+
*/
|
|
117
|
+
private _saveReturnUrlIfTrusted;
|
|
105
118
|
/**
|
|
106
119
|
* 发起 SSO 登录
|
|
107
120
|
*
|
|
108
121
|
* 生成 PKCE code_verifier/code_challenge 和 state 参数,
|
|
109
122
|
* 构建 authorize URL,通过 302 跳转到 SSO 登录页。
|
|
110
123
|
*
|
|
111
|
-
* @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage
|
|
124
|
+
* @param returnUrl - 登录成功后的返回地址(可选,保存到 sessionStorage;
|
|
125
|
+
* 仅允许相对路径或同源绝对 URL,否则忽略并告警)
|
|
126
|
+
*
|
|
127
|
+
* ⚠️ 不要与 getLoginUrl() 混用:两者都会重新生成并覆盖 sessionStorage 中的
|
|
128
|
+
* state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
|
|
129
|
+
* 同一次登录只使用其中一个入口。
|
|
112
130
|
*/
|
|
113
131
|
login(returnUrl?: string): Promise<void>;
|
|
114
132
|
/**
|
|
115
133
|
* 构建登录 URL(不跳转,返回 URL 字符串)
|
|
116
134
|
*
|
|
117
135
|
* 适用于需要手动处理跳转的场景。
|
|
136
|
+
*
|
|
137
|
+
* ⚠️ 不要与 login() 混用:两者都会重新生成并覆盖 sessionStorage 中的
|
|
138
|
+
* state / PKCE verifier,先调用的那次授权流程将因 state 不匹配而失败。
|
|
139
|
+
* 同一次登录只使用其中一个入口。
|
|
118
140
|
*/
|
|
119
141
|
getLoginUrl(returnUrl?: string): Promise<string>;
|
|
142
|
+
/**
|
|
143
|
+
* 弹窗模式 SSO 登录
|
|
144
|
+
*
|
|
145
|
+
* 打开一个小窗口进行登录,认证完成后窗口自动关闭,
|
|
146
|
+
* 主页面不丢失状态(适用于 SPA 中需要保持表单/浏览上下文的场景)。
|
|
147
|
+
*
|
|
148
|
+
* 流程:
|
|
149
|
+
* 1. window.open() 打开授权 URL 到弹窗
|
|
150
|
+
* 2. 用户在弹窗中完成登录
|
|
151
|
+
* 3. 弹窗加载 CallbackPage 时检测到 window.opener,通过 postMessage 回传回调 URL
|
|
152
|
+
* 4. 主页面收到消息后调用 handleCallback() 交换 token
|
|
153
|
+
* 5. 弹窗自动关闭
|
|
154
|
+
*
|
|
155
|
+
* @param options - 弹窗配置
|
|
156
|
+
* @param options.returnUrl - 登录成功后的返回地址
|
|
157
|
+
* @param options.width - 弹窗宽度(默认 480)
|
|
158
|
+
* @param options.height - 弹窗高度(默认 640)
|
|
159
|
+
* @returns TokenData
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const client = new SsoClient({ clientId: "xxx", redirectUri: "https://myapp.com/callback", ssoBaseUrl: "https://nihplod.cn" });
|
|
164
|
+
* try {
|
|
165
|
+
* const tokenData = await client.loginPopup({ returnUrl: "/dashboard" });
|
|
166
|
+
* console.log("登录成功", tokenData);
|
|
167
|
+
* } catch (err) {
|
|
168
|
+
* if (err instanceof SsoError && err.code === "popup_blocked") {
|
|
169
|
+
* // 弹窗被拦截,回退到同页重定向
|
|
170
|
+
* await client.login("/dashboard");
|
|
171
|
+
* }
|
|
172
|
+
* }
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
loginPopup(options?: {
|
|
176
|
+
returnUrl?: string;
|
|
177
|
+
width?: number;
|
|
178
|
+
height?: number;
|
|
179
|
+
}): Promise<TokenData>;
|
|
120
180
|
/**
|
|
121
181
|
* 处理 OAuth 回调
|
|
122
182
|
*
|
|
123
183
|
* 解析回调 URL,校验 state 参数,用授权码交换 token。
|
|
124
|
-
* 成功后 token 自动保存到 sessionStorage
|
|
184
|
+
* 成功后 token 自动保存到 token 存储(默认 sessionStorage,可通过 setTokenStorage 定制)。
|
|
125
185
|
*
|
|
126
186
|
* @param callbackUrl - 完整的回调 URL(window.location.href)
|
|
127
187
|
* @returns TokenData 或 null
|
|
@@ -159,9 +219,28 @@ declare class SsoClient {
|
|
|
159
219
|
* 登出
|
|
160
220
|
*
|
|
161
221
|
* 清除本地所有 token 和临时数据,并尝试撤销服务端 refresh_token。
|
|
162
|
-
* @param redirectToSso - 是否重定向到 SSO 登出页(默认 false
|
|
222
|
+
* @param redirectToSso - 是否重定向到 SSO 登出页(默认 false)。
|
|
223
|
+
* 为 true 时携带 state 参数(已保存到 sessionStorage),
|
|
224
|
+
* 回跳页面应调用 validateLogoutState() 校验防登出 CSRF。
|
|
163
225
|
*/
|
|
164
226
|
logout(redirectToSso?: boolean): Promise<void>;
|
|
227
|
+
/**
|
|
228
|
+
* 校验 RP-Initiated Logout 回跳的 state 参数(登出 CSRF 防护)
|
|
229
|
+
*
|
|
230
|
+
* 在 post_logout_redirect_uri 指向的页面加载时调用;
|
|
231
|
+
* 仅在 URL 携带 state 且与 logout(redirectToSso=true) 保存的值一致时返回 true,
|
|
232
|
+
* 校验后清除已保存的 logout state(一次性)。
|
|
233
|
+
*
|
|
234
|
+
* @param url - 当前页面完整 URL(window.location.href)
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* if (sso.validateLogoutState(window.location.href)) {
|
|
239
|
+
* // 来自 SSO 登出的可信回跳
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
validateLogoutState(url: string): boolean;
|
|
165
244
|
/**
|
|
166
245
|
* 获取 OIDC Discovery 文档
|
|
167
246
|
*
|
|
@@ -171,6 +250,21 @@ declare class SsoClient {
|
|
|
171
250
|
getDiscovery(): Promise<OidcDiscovery | null>;
|
|
172
251
|
}
|
|
173
252
|
|
|
253
|
+
/**
|
|
254
|
+
* SSO SDK 错误类型
|
|
255
|
+
*/
|
|
256
|
+
/** SSO 错误码 */
|
|
257
|
+
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";
|
|
258
|
+
/**
|
|
259
|
+
* SSO SDK 自定义错误
|
|
260
|
+
*/
|
|
261
|
+
declare class SsoError extends Error {
|
|
262
|
+
readonly code: SsoErrorCode;
|
|
263
|
+
readonly description: string;
|
|
264
|
+
readonly cause?: unknown;
|
|
265
|
+
constructor(code: SsoErrorCode, description: string, cause?: unknown);
|
|
266
|
+
}
|
|
267
|
+
|
|
174
268
|
/**
|
|
175
269
|
* SsoProvider - React Context Provider
|
|
176
270
|
*
|
|
@@ -180,6 +274,12 @@ declare class SsoClient {
|
|
|
180
274
|
* - 提供 useSso() hook
|
|
181
275
|
*/
|
|
182
276
|
|
|
277
|
+
/**
|
|
278
|
+
* 认证状态为三态:
|
|
279
|
+
* - isLoading=true:初始化/刷新中,user 与 error 均可能为 null
|
|
280
|
+
* - error 非 null:加载用户信息失败(如会话已失效),user 为 null
|
|
281
|
+
* - isAuthenticated=true 且 user 非 null:已登录
|
|
282
|
+
*/
|
|
183
283
|
interface SsoContextValue {
|
|
184
284
|
/** 当前用户信息 */
|
|
185
285
|
user: SsoUser | null;
|
|
@@ -187,8 +287,16 @@ interface SsoContextValue {
|
|
|
187
287
|
isAuthenticated: boolean;
|
|
188
288
|
/** 是否正在加载(初始化/刷新中) */
|
|
189
289
|
isLoading: boolean;
|
|
190
|
-
/**
|
|
290
|
+
/** 最近一次加载用户信息失败的错误(成功或登出后为 null) */
|
|
291
|
+
error: SsoError | null;
|
|
292
|
+
/** 发起登录(同页重定向) */
|
|
191
293
|
login: (returnUrl?: string) => Promise<void>;
|
|
294
|
+
/** 弹窗模式登录(保持当前页面状态不丢失) */
|
|
295
|
+
loginPopup: (options?: {
|
|
296
|
+
returnUrl?: string;
|
|
297
|
+
width?: number;
|
|
298
|
+
height?: number;
|
|
299
|
+
}) => Promise<TokenData>;
|
|
192
300
|
/** 登出 */
|
|
193
301
|
logout: (redirectToSso?: boolean) => Promise<void>;
|
|
194
302
|
/** 刷新用户信息 */
|
|
@@ -210,8 +318,9 @@ interface SsoProviderProps {
|
|
|
210
318
|
*/
|
|
211
319
|
refreshThreshold?: number;
|
|
212
320
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
321
|
+
* Token 静默刷新成功后的回调(可选)。
|
|
322
|
+
* 每次刷新成功时以新的 access_token 调用,可用于同步 token 到外部状态;
|
|
323
|
+
* SDK 不会据此自动重试先前失败的 API 请求,重试需由调用方自行实现。
|
|
215
324
|
*/
|
|
216
325
|
onTokenRefreshed?: (token: string) => void;
|
|
217
326
|
}
|
|
@@ -247,20 +356,38 @@ interface RequireAuthProps {
|
|
|
247
356
|
fallback?: React.ReactNode;
|
|
248
357
|
/** 是否在检测到未登录时自动发起登录跳转 */
|
|
249
358
|
autoLogin?: boolean;
|
|
359
|
+
/**
|
|
360
|
+
* 是否使用弹窗模式登录(保持当前页面状态不丢失)
|
|
361
|
+
*
|
|
362
|
+
* 仅在 autoLogin=true 时生效。弹窗被拦截时自动回退到同页重定向。
|
|
363
|
+
*/
|
|
364
|
+
usePopup?: boolean;
|
|
365
|
+
/**
|
|
366
|
+
* 登录发起失败时的回调(如弹窗被关闭 popup_closed)。
|
|
367
|
+
* 失败后组件会展示重试入口,不会永久停在"请先登录"。
|
|
368
|
+
*/
|
|
369
|
+
onError?: (error: unknown) => void;
|
|
370
|
+
/**
|
|
371
|
+
* 自定义登录失败 UI。不传时显示默认的错误提示与"重试"按钮。
|
|
372
|
+
* retry() 会重新发起登录。
|
|
373
|
+
*/
|
|
374
|
+
renderLoginError?: (error: unknown, retry: () => void) => React.ReactNode;
|
|
250
375
|
}
|
|
251
376
|
/**
|
|
252
377
|
* 路由保护组件
|
|
253
378
|
*
|
|
254
379
|
* 未登录时自动触发登录跳转或显示回退内容。
|
|
380
|
+
* 登录发起失败(如弹窗被关闭)时显示重试入口,并通过 onError 上报。
|
|
255
381
|
*
|
|
256
382
|
* @example
|
|
257
383
|
* ```tsx
|
|
258
|
-
*
|
|
384
|
+
* // 弹窗模式:不中断用户当前操作
|
|
385
|
+
* <RequireAuth autoLogin usePopup>
|
|
259
386
|
* <Dashboard />
|
|
260
387
|
* </RequireAuth>
|
|
261
388
|
* ```
|
|
262
389
|
*/
|
|
263
|
-
declare function RequireAuth({ children, fallback, autoLogin, }: RequireAuthProps): string | number | bigint | true | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.FunctionComponentElement<React.FragmentProps>;
|
|
390
|
+
declare function RequireAuth({ children, fallback, autoLogin, usePopup, onError, renderLoginError, }: RequireAuthProps): string | number | bigint | true | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.FunctionComponentElement<React.FragmentProps>;
|
|
264
391
|
/**
|
|
265
392
|
* 高阶组件:包装页面组件,要求认证后才能访问
|
|
266
393
|
*
|
|
@@ -276,7 +403,8 @@ declare function withAuth<P extends object>(Component: ComponentType<P>): Compon
|
|
|
276
403
|
* CallbackPage — 通用 OAuth 回调页面组件
|
|
277
404
|
*
|
|
278
405
|
* 子项目只需在回调路由渲染此组件即可完成 code → token 交换。
|
|
279
|
-
*
|
|
406
|
+
* 交换成功后默认整页跳转到 returnUrl 或首页;
|
|
407
|
+
* 传入 onSuccess 可跳过默认跳转,由 SPA 路由接管(保持应用状态)。
|
|
280
408
|
*
|
|
281
409
|
* @example
|
|
282
410
|
* ```tsx
|
|
@@ -286,9 +414,42 @@ declare function withAuth<P extends object>(Component: ComponentType<P>): Compon
|
|
|
286
414
|
* return <CallbackPage />;
|
|
287
415
|
* }
|
|
288
416
|
* ```
|
|
417
|
+
*
|
|
418
|
+
* @example SPA 路由接管跳转(不整页刷新):
|
|
419
|
+
* ```tsx
|
|
420
|
+
* <CallbackPage onSuccess={() => navigate("/dashboard", { replace: true })} />
|
|
421
|
+
* ```
|
|
289
422
|
*/
|
|
290
423
|
|
|
291
|
-
|
|
424
|
+
interface CallbackPageProps {
|
|
425
|
+
/**
|
|
426
|
+
* 登录成功回调。传入后跳过默认的整页跳转(window.location.href),
|
|
427
|
+
* 由调用方用 SPA 路由接管跳转,避免丢失应用内状态。
|
|
428
|
+
*/
|
|
429
|
+
onSuccess?: (tokenData: TokenData) => void;
|
|
430
|
+
/** 登录失败回调(错误同时会展示在错误页,除非提供了 renderError) */
|
|
431
|
+
onError?: (error: Error) => void;
|
|
432
|
+
/**
|
|
433
|
+
* 自定义错误页渲染。不传时使用默认错误 UI(DefaultCallbackError)。
|
|
434
|
+
*/
|
|
435
|
+
renderError?: (error: string) => React.ReactNode;
|
|
436
|
+
}
|
|
437
|
+
/** 默认错误页 UI(可通过 renderError 完全替换) */
|
|
438
|
+
declare function DefaultCallbackError({ error }: {
|
|
439
|
+
error: string;
|
|
440
|
+
}): React.DetailedReactHTMLElement<{
|
|
441
|
+
style: {
|
|
442
|
+
display: "flex";
|
|
443
|
+
flexDirection: "column";
|
|
444
|
+
alignItems: "center";
|
|
445
|
+
justifyContent: "center";
|
|
446
|
+
minHeight: string;
|
|
447
|
+
fontFamily: "system-ui, sans-serif";
|
|
448
|
+
};
|
|
449
|
+
}, HTMLElement>;
|
|
450
|
+
declare function CallbackPage({ onSuccess, onError, renderError }?: CallbackPageProps): React.FunctionComponentElement<React.FragmentProps> | React.FunctionComponentElement<{
|
|
451
|
+
error: string;
|
|
452
|
+
}> | React.DetailedReactHTMLElement<{
|
|
292
453
|
style: {
|
|
293
454
|
display: "flex";
|
|
294
455
|
alignItems: "center";
|
|
@@ -298,4 +459,4 @@ declare function CallbackPage(): React.DetailedReactHTMLElement<{
|
|
|
298
459
|
};
|
|
299
460
|
}, HTMLElement> | null;
|
|
300
461
|
|
|
301
|
-
export { CallbackPage, RequireAuth, SsoProvider, useSso, withAuth };
|
|
462
|
+
export { CallbackPage, type CallbackPageProps, DefaultCallbackError, RequireAuth, SsoProvider, useSso, withAuth };
|