@lobehub/chat 1.79.6 → 1.79.8
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/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +119 -0
- package/docs/self-hosting/advanced/online-search.mdx +63 -0
- package/locales/ar/models.json +12 -0
- package/locales/ar/oauth.json +39 -0
- package/locales/bg-BG/models.json +12 -0
- package/locales/bg-BG/oauth.json +39 -0
- package/locales/de-DE/models.json +12 -0
- package/locales/de-DE/oauth.json +39 -0
- package/locales/en-US/models.json +12 -0
- package/locales/en-US/oauth.json +39 -0
- package/locales/es-ES/models.json +12 -0
- package/locales/es-ES/oauth.json +39 -0
- package/locales/fa-IR/models.json +12 -0
- package/locales/fa-IR/oauth.json +39 -0
- package/locales/fr-FR/models.json +12 -0
- package/locales/fr-FR/oauth.json +39 -0
- package/locales/it-IT/models.json +12 -0
- package/locales/it-IT/oauth.json +39 -0
- package/locales/ja-JP/models.json +12 -0
- package/locales/ja-JP/oauth.json +39 -0
- package/locales/ko-KR/models.json +12 -0
- package/locales/ko-KR/oauth.json +39 -0
- package/locales/nl-NL/models.json +12 -0
- package/locales/nl-NL/oauth.json +39 -0
- package/locales/pl-PL/models.json +12 -0
- package/locales/pl-PL/oauth.json +39 -0
- package/locales/pt-BR/models.json +12 -0
- package/locales/pt-BR/oauth.json +39 -0
- package/locales/ru-RU/models.json +12 -0
- package/locales/ru-RU/oauth.json +39 -0
- package/locales/tr-TR/models.json +12 -0
- package/locales/tr-TR/oauth.json +39 -0
- package/locales/vi-VN/models.json +12 -0
- package/locales/vi-VN/oauth.json +39 -0
- package/locales/zh-CN/models.json +12 -0
- package/locales/zh-CN/oauth.json +39 -0
- package/locales/zh-TW/models.json +12 -0
- package/locales/zh-TW/oauth.json +39 -0
- package/package.json +5 -2
- package/scripts/generate-oidc-jwk.mjs +59 -0
- package/scripts/migrateServerDB/index.ts +3 -1
- package/src/app/(backend)/oidc/[...oidc]/route.ts +270 -0
- package/src/app/(backend)/oidc/consent/route.ts +97 -0
- package/src/app/[variants]/oauth/consent/[uid]/Client.tsx +97 -0
- package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +36 -0
- package/src/app/[variants]/oauth/consent/[uid]/page.tsx +71 -0
- package/src/app/[variants]/oauth/consent/[uid]/success/page.tsx +30 -0
- package/src/const/hotkeys.ts +2 -2
- package/src/const/trace.ts +1 -0
- package/src/database/client/migrations.json +27 -8
- package/src/database/migrations/0020_add_oidc.sql +124 -0
- package/src/database/migrations/meta/0020_snapshot.json +4975 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/repositories/tableViewer/index.test.ts +1 -1
- package/src/database/schemas/index.ts +1 -0
- package/src/database/schemas/oidc.ts +158 -0
- package/src/database/server/models/__tests__/adapter.test.ts +503 -0
- package/src/envs/oidc.ts +18 -0
- package/src/libs/agent-runtime/azureOpenai/index.ts +4 -1
- package/src/libs/agent-runtime/utils/streams/protocol.ts +2 -4
- package/src/libs/oidc-provider/adapter.ts +494 -0
- package/src/libs/oidc-provider/config.ts +53 -0
- package/src/libs/oidc-provider/http-adapter.ts +279 -0
- package/src/libs/oidc-provider/interaction-policy.ts +37 -0
- package/src/libs/oidc-provider/provider.ts +260 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/oauth.ts +41 -0
- package/src/middleware.ts +94 -6
- package/src/server/services/oidc/index.ts +29 -0
- package/src/server/services/oidc/oidcProvider.ts +27 -0
- package/src/store/chat/slices/aiChat/actions/memory.ts +6 -1
- package/src/types/hotkey.ts +54 -3
@@ -0,0 +1,279 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import { cookies } from 'next/headers';
|
3
|
+
import { NextRequest } from 'next/server';
|
4
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
5
|
+
import urlJoin from 'url-join';
|
6
|
+
|
7
|
+
import { appEnv } from '@/config/app';
|
8
|
+
|
9
|
+
const log = debug('lobe-oidc:http-adapter');
|
10
|
+
|
11
|
+
/**
|
12
|
+
* 将 Next.js 请求头转换为标准 Node.js HTTP 头格式
|
13
|
+
*/
|
14
|
+
export const convertHeadersToNodeHeaders = (nextHeaders: Headers): Record<string, string> => {
|
15
|
+
const headers: Record<string, string> = {};
|
16
|
+
nextHeaders.forEach((value, key) => {
|
17
|
+
headers[key] = value;
|
18
|
+
});
|
19
|
+
return headers;
|
20
|
+
};
|
21
|
+
|
22
|
+
/**
|
23
|
+
* 创建用于 OIDC Provider 的 Node.js HTTP 请求对象
|
24
|
+
* @param req Next.js 请求对象
|
25
|
+
* @param pathPrefix 路径前缀
|
26
|
+
* @param bodyText 可选的请求体文本,用于 POST 请求
|
27
|
+
*/
|
28
|
+
export const createNodeRequest = (
|
29
|
+
req: NextRequest,
|
30
|
+
pathPrefix: string = '/oidc',
|
31
|
+
bodyText?: string,
|
32
|
+
): IncomingMessage => {
|
33
|
+
// 构建 URL 对象
|
34
|
+
const url = new URL(req.url);
|
35
|
+
|
36
|
+
// 计算相对于前缀的路径
|
37
|
+
let providerPath = url.pathname;
|
38
|
+
if (pathPrefix && url.pathname.startsWith(pathPrefix)) {
|
39
|
+
providerPath = url.pathname.slice(pathPrefix.length);
|
40
|
+
}
|
41
|
+
|
42
|
+
// 确保路径始终以/开头
|
43
|
+
if (!providerPath.startsWith('/')) {
|
44
|
+
providerPath = '/' + providerPath;
|
45
|
+
}
|
46
|
+
|
47
|
+
log('Creating Node.js request from Next.js request');
|
48
|
+
log('Original path: %s, Provider path: %s', url.pathname, providerPath);
|
49
|
+
|
50
|
+
const nodeRequest = {
|
51
|
+
// 基本属性
|
52
|
+
headers: convertHeadersToNodeHeaders(req.headers),
|
53
|
+
method: req.method,
|
54
|
+
// 模拟可读流行为
|
55
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
56
|
+
on: (event: string, handler: Function) => {
|
57
|
+
if (event === 'data' && bodyText) {
|
58
|
+
handler(bodyText);
|
59
|
+
}
|
60
|
+
if (event === 'end') {
|
61
|
+
handler();
|
62
|
+
}
|
63
|
+
},
|
64
|
+
|
65
|
+
url: providerPath + url.search,
|
66
|
+
|
67
|
+
// POST 请求所需属性
|
68
|
+
...(bodyText && {
|
69
|
+
body: bodyText,
|
70
|
+
readable: true,
|
71
|
+
}),
|
72
|
+
|
73
|
+
// 添加 Node.js 服务器所期望的额外属性
|
74
|
+
socket: {
|
75
|
+
remoteAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
76
|
+
},
|
77
|
+
} as unknown as IncomingMessage;
|
78
|
+
|
79
|
+
log('Node.js request created with method %s and path %s', nodeRequest.method, nodeRequest.url);
|
80
|
+
return nodeRequest;
|
81
|
+
};
|
82
|
+
|
83
|
+
/**
|
84
|
+
* 响应收集器接口,用于捕获 OIDC Provider 的响应
|
85
|
+
*/
|
86
|
+
export interface ResponseCollector {
|
87
|
+
nodeResponse: ServerResponse;
|
88
|
+
readonly responseBody: string | Buffer;
|
89
|
+
readonly responseHeaders: Record<string, string | string[]>;
|
90
|
+
readonly responseStatus: number;
|
91
|
+
}
|
92
|
+
|
93
|
+
/**
|
94
|
+
* 创建用于 OIDC Provider 的 Node.js HTTP 响应对象
|
95
|
+
* @param resolvePromise 当响应完成时调用的解析函数
|
96
|
+
*/
|
97
|
+
export const createNodeResponse = (resolvePromise: () => void): ResponseCollector => {
|
98
|
+
log('Creating Node.js response collector');
|
99
|
+
|
100
|
+
// 存储响应状态的对象
|
101
|
+
const state = {
|
102
|
+
responseBody: '' as string | Buffer,
|
103
|
+
responseHeaders: {} as Record<string, string | string[]>,
|
104
|
+
responseStatus: 200,
|
105
|
+
};
|
106
|
+
|
107
|
+
let promiseResolved = false;
|
108
|
+
|
109
|
+
const nodeResponse: any = {
|
110
|
+
end: (chunk?: string | Buffer) => {
|
111
|
+
log('NodeResponse.end called');
|
112
|
+
if (chunk) {
|
113
|
+
log('NodeResponse.end chunk: %s', typeof chunk === 'string' ? chunk : '(Buffer)');
|
114
|
+
// @ts-ignore
|
115
|
+
state.responseBody += chunk;
|
116
|
+
}
|
117
|
+
|
118
|
+
const locationHeader = state.responseHeaders['location'];
|
119
|
+
if (locationHeader && state.responseStatus === 200) {
|
120
|
+
log('Location header detected with status 200, overriding to 302');
|
121
|
+
state.responseStatus = 302;
|
122
|
+
}
|
123
|
+
|
124
|
+
if (!promiseResolved) {
|
125
|
+
log('Resolving response promise');
|
126
|
+
promiseResolved = true;
|
127
|
+
resolvePromise();
|
128
|
+
}
|
129
|
+
},
|
130
|
+
|
131
|
+
getHeader: (name: string) => {
|
132
|
+
const lowerName = name.toLowerCase();
|
133
|
+
return state.responseHeaders[lowerName];
|
134
|
+
},
|
135
|
+
|
136
|
+
getHeaderNames: () => {
|
137
|
+
return Object.keys(state.responseHeaders);
|
138
|
+
},
|
139
|
+
|
140
|
+
getHeaders: () => {
|
141
|
+
return state.responseHeaders;
|
142
|
+
},
|
143
|
+
|
144
|
+
headersSent: false,
|
145
|
+
|
146
|
+
setHeader: (name: string, value: string | string[]) => {
|
147
|
+
const lowerName = name.toLowerCase();
|
148
|
+
log('Setting header: %s = %s', lowerName, value);
|
149
|
+
state.responseHeaders[lowerName] = value;
|
150
|
+
},
|
151
|
+
|
152
|
+
write: (chunk: string | Buffer) => {
|
153
|
+
log('NodeResponse.write called with chunk');
|
154
|
+
// @ts-ignore
|
155
|
+
state.responseBody += chunk;
|
156
|
+
},
|
157
|
+
|
158
|
+
writeHead: (status: number, headers?: Record<string, string | string[]>) => {
|
159
|
+
log('NodeResponse.writeHead called with status: %d', status);
|
160
|
+
state.responseStatus = status;
|
161
|
+
|
162
|
+
if (headers) {
|
163
|
+
const lowerCaseHeaders = Object.entries(headers).reduce(
|
164
|
+
(acc, [key, value]) => {
|
165
|
+
acc[key.toLowerCase()] = value;
|
166
|
+
return acc;
|
167
|
+
},
|
168
|
+
{} as Record<string, string | string[]>,
|
169
|
+
);
|
170
|
+
state.responseHeaders = { ...state.responseHeaders, ...lowerCaseHeaders };
|
171
|
+
}
|
172
|
+
|
173
|
+
(nodeResponse as any).headersSent = true;
|
174
|
+
},
|
175
|
+
} as unknown as ServerResponse;
|
176
|
+
|
177
|
+
log('Node.js response collector created successfully');
|
178
|
+
|
179
|
+
return {
|
180
|
+
nodeResponse,
|
181
|
+
get responseBody() {
|
182
|
+
return state.responseBody;
|
183
|
+
},
|
184
|
+
get responseHeaders() {
|
185
|
+
return state.responseHeaders;
|
186
|
+
},
|
187
|
+
get responseStatus() {
|
188
|
+
return state.responseStatus;
|
189
|
+
},
|
190
|
+
};
|
191
|
+
};
|
192
|
+
|
193
|
+
/**
|
194
|
+
* 创建用于调用 provider.interactionDetails 的上下文 (req, res)
|
195
|
+
* @param uid 交互 ID
|
196
|
+
*/
|
197
|
+
export const createContextForInteractionDetails = async (
|
198
|
+
uid: string,
|
199
|
+
): Promise<{ req: IncomingMessage; res: ServerResponse }> => {
|
200
|
+
log('Creating context for interaction details for uid: %s', uid);
|
201
|
+
|
202
|
+
// 使用APP_URL环境变量来构建URL基础部分
|
203
|
+
const baseUrl = appEnv.APP_URL!;
|
204
|
+
log('Using base URL: %s', baseUrl);
|
205
|
+
|
206
|
+
// 从baseUrl提取主机名用于headers
|
207
|
+
const hostName = new URL(baseUrl).host;
|
208
|
+
|
209
|
+
// 1. 获取真实的 Cookies
|
210
|
+
const cookieStore = await cookies();
|
211
|
+
const realCookies: Record<string, string> = {};
|
212
|
+
cookieStore.getAll().forEach((cookie) => {
|
213
|
+
realCookies[cookie.name] = cookie.value;
|
214
|
+
});
|
215
|
+
log('Real cookies found: %o', Object.keys(realCookies));
|
216
|
+
|
217
|
+
// 特别检查交互会话cookie
|
218
|
+
const interactionCookieName = `_interaction_${uid}`;
|
219
|
+
if (realCookies[interactionCookieName]) {
|
220
|
+
log('Found interaction session cookie: %s', interactionCookieName);
|
221
|
+
} else {
|
222
|
+
log('Warning: Interaction session cookie not found: %s', interactionCookieName);
|
223
|
+
}
|
224
|
+
|
225
|
+
// 2. 构建包含真实 Cookie 的 Headers
|
226
|
+
const headers = new Headers({ host: hostName });
|
227
|
+
const cookieString = Object.entries(realCookies)
|
228
|
+
.map(([name, value]) => `${name}=${value}`)
|
229
|
+
.join('; ');
|
230
|
+
if (cookieString) {
|
231
|
+
headers.set('cookie', cookieString);
|
232
|
+
log('Setting cookie header');
|
233
|
+
} else {
|
234
|
+
log('No cookies found to set in header');
|
235
|
+
}
|
236
|
+
|
237
|
+
// 3. 创建模拟的 NextRequest
|
238
|
+
// 注意:这里的 IP, geo, ua 等信息可能是 oidc-provider 某些特性需要的,
|
239
|
+
// 如果遇到相关问题,可能需要从真实请求头中提取 (e.g., 'x-forwarded-for', 'user-agent')
|
240
|
+
const interactionUrl = urlJoin(baseUrl, `/oauth/consent/${uid}`);
|
241
|
+
log('Creating interaction URL: %s', interactionUrl);
|
242
|
+
|
243
|
+
const mockNextRequest = {
|
244
|
+
cookies: {
|
245
|
+
// 模拟 NextRequestCookies 接口
|
246
|
+
get: (name: string) => cookieStore.get(name)?.value,
|
247
|
+
getAll: () => cookieStore.getAll(),
|
248
|
+
has: (name: string) => cookieStore.has(name),
|
249
|
+
},
|
250
|
+
geo: {},
|
251
|
+
headers: headers,
|
252
|
+
ip: '127.0.0.1',
|
253
|
+
method: 'GET',
|
254
|
+
nextUrl: new URL(interactionUrl),
|
255
|
+
page: { name: undefined, params: undefined },
|
256
|
+
ua: undefined,
|
257
|
+
url: new URL(interactionUrl),
|
258
|
+
} as unknown as NextRequest;
|
259
|
+
log('Mock NextRequest created for url: %s', mockNextRequest.url);
|
260
|
+
|
261
|
+
// 4. 使用 createNodeRequest 创建模拟的 Node.js IncomingMessage
|
262
|
+
// pathPrefix 设置为 '/' 因为我们的 URL 已经是 Provider 期望的路径格式 /interaction/:uid
|
263
|
+
const req: IncomingMessage = createNodeRequest(mockNextRequest, '/');
|
264
|
+
// @ts-ignore - 将解析出的 cookies 附加到模拟的 Node.js 请求上
|
265
|
+
req.cookies = realCookies;
|
266
|
+
log('Node.js IncomingMessage created, attached real cookies');
|
267
|
+
|
268
|
+
// 5. 使用 createNodeResponse 创建模拟的 Node.js ServerResponse
|
269
|
+
let resolveFunc: () => void;
|
270
|
+
new Promise<void>((resolve) => {
|
271
|
+
resolveFunc = resolve;
|
272
|
+
});
|
273
|
+
|
274
|
+
const responseCollector: ResponseCollector = createNodeResponse(() => resolveFunc());
|
275
|
+
const res: ServerResponse = responseCollector.nodeResponse;
|
276
|
+
log('Node.js ServerResponse created');
|
277
|
+
|
278
|
+
return { req, res };
|
279
|
+
};
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import { interactionPolicy } from 'oidc-provider';
|
3
|
+
|
4
|
+
const { base } = interactionPolicy; // Import Check and base
|
5
|
+
const log = debug('lobe-oidc:interaction-policy');
|
6
|
+
|
7
|
+
/**
|
8
|
+
* 创建自定义交互策略
|
9
|
+
*/
|
10
|
+
export const createInteractionPolicy = () => {
|
11
|
+
log('Creating custom interaction policy');
|
12
|
+
const policy = base();
|
13
|
+
|
14
|
+
log('Base policy details: %O', {
|
15
|
+
promptNames: Array.from(policy.keys()),
|
16
|
+
size: policy.length,
|
17
|
+
});
|
18
|
+
|
19
|
+
const loginPrompt = policy.get('login');
|
20
|
+
log('Accessing login prompt from policy: %O', !!loginPrompt);
|
21
|
+
|
22
|
+
if (loginPrompt) {
|
23
|
+
log('Login prompt details: %O', {
|
24
|
+
checks: Array.from(loginPrompt.checks.keys()),
|
25
|
+
name: loginPrompt.name,
|
26
|
+
requestable: loginPrompt.requestable,
|
27
|
+
});
|
28
|
+
} else {
|
29
|
+
console.warn(
|
30
|
+
"Could not find 'login' prompt in the base policy. Custom session check not applied.",
|
31
|
+
);
|
32
|
+
log('WARNING: login prompt not found in base policy');
|
33
|
+
}
|
34
|
+
|
35
|
+
log('Custom interaction policy created successfully');
|
36
|
+
return policy;
|
37
|
+
};
|
@@ -0,0 +1,260 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import Provider, { Configuration, KoaContextWithOIDC } from 'oidc-provider';
|
3
|
+
|
4
|
+
import { serverDBEnv } from '@/config/db';
|
5
|
+
import { UserModel } from '@/database/models/user';
|
6
|
+
import { LobeChatDatabase } from '@/database/type';
|
7
|
+
import { oidcEnv } from '@/envs/oidc';
|
8
|
+
|
9
|
+
import { DrizzleAdapter } from './adapter';
|
10
|
+
import { defaultClaims, defaultClients, defaultScopes } from './config';
|
11
|
+
import { createInteractionPolicy } from './interaction-policy';
|
12
|
+
|
13
|
+
const logProvider = debug('lobe-oidc:provider'); // <--- 添加 provider 日志实例
|
14
|
+
|
15
|
+
/**
|
16
|
+
* 从环境变量中获取 JWKS
|
17
|
+
* 该 JWKS 是一个包含 RS256 私钥的 JSON 对象
|
18
|
+
*/
|
19
|
+
const getJWKS = (): object => {
|
20
|
+
try {
|
21
|
+
const jwksString = oidcEnv.OIDC_JWKS_KEY;
|
22
|
+
|
23
|
+
if (!jwksString) {
|
24
|
+
throw new Error(
|
25
|
+
'OIDC_JWKS_KEY 环境变量是必需的。请使用 scripts/generate-oidc-jwk.mjs 生成 JWKS。',
|
26
|
+
);
|
27
|
+
}
|
28
|
+
|
29
|
+
// 尝试解析 JWKS JSON 字符串
|
30
|
+
const jwks = JSON.parse(jwksString);
|
31
|
+
|
32
|
+
// 检查 JWKS 格式是否正确
|
33
|
+
if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
|
34
|
+
throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
|
35
|
+
}
|
36
|
+
|
37
|
+
// 检查是否有 RS256 算法的密钥
|
38
|
+
const hasRS256Key = jwks.keys.some((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
|
39
|
+
if (!hasRS256Key) {
|
40
|
+
throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
|
41
|
+
}
|
42
|
+
|
43
|
+
return jwks;
|
44
|
+
} catch (error) {
|
45
|
+
console.error('解析 JWKS 失败:', error);
|
46
|
+
throw new Error(`OIDC_JWKS_KEY 解析错误: ${(error as Error).message}`);
|
47
|
+
}
|
48
|
+
};
|
49
|
+
|
50
|
+
/**
|
51
|
+
* 获取 Cookie 密钥,使用 KEY_VAULTS_SECRET
|
52
|
+
*/
|
53
|
+
const getCookieKeys = () => {
|
54
|
+
const key = serverDBEnv.KEY_VAULTS_SECRET;
|
55
|
+
if (!key) {
|
56
|
+
throw new Error('KEY_VAULTS_SECRET is required for OIDC Provider cookie encryption');
|
57
|
+
}
|
58
|
+
return [key];
|
59
|
+
};
|
60
|
+
|
61
|
+
/**
|
62
|
+
* 创建 OIDC Provider 实例
|
63
|
+
* @param db - 数据库实例
|
64
|
+
* @param baseUrl - 服务部署的基础URL
|
65
|
+
* @returns 配置好的 OIDC Provider 实例
|
66
|
+
*/
|
67
|
+
export const createOIDCProvider = async (
|
68
|
+
db: LobeChatDatabase,
|
69
|
+
baseUrl: string,
|
70
|
+
): Promise<Provider> => {
|
71
|
+
const issuerUrl = `${baseUrl}/oidc`;
|
72
|
+
if (!issuerUrl) {
|
73
|
+
throw new Error('Base URL is required for OIDC Provider');
|
74
|
+
}
|
75
|
+
|
76
|
+
// 获取 JWKS
|
77
|
+
const jwks = getJWKS();
|
78
|
+
|
79
|
+
const cookieKeys = getCookieKeys();
|
80
|
+
|
81
|
+
const configuration: Configuration = {
|
82
|
+
// 11. 数据库适配器
|
83
|
+
adapter: DrizzleAdapter.createAdapterFactory(db),
|
84
|
+
|
85
|
+
// 4. Claims 定义
|
86
|
+
claims: defaultClaims,
|
87
|
+
|
88
|
+
// 1. 客户端配置
|
89
|
+
clients: defaultClients,
|
90
|
+
|
91
|
+
// 7. Cookie 配置
|
92
|
+
cookies: {
|
93
|
+
keys: cookieKeys,
|
94
|
+
long: { signed: true },
|
95
|
+
short: { path: '/', signed: true },
|
96
|
+
},
|
97
|
+
|
98
|
+
// 5. 特性配置
|
99
|
+
features: {
|
100
|
+
backchannelLogout: { enabled: true },
|
101
|
+
clientCredentials: { enabled: false },
|
102
|
+
devInteractions: { enabled: false },
|
103
|
+
deviceFlow: { enabled: false },
|
104
|
+
introspection: { enabled: true },
|
105
|
+
resourceIndicators: { enabled: false },
|
106
|
+
revocation: { enabled: true },
|
107
|
+
rpInitiatedLogout: { enabled: true },
|
108
|
+
userinfo: { enabled: true },
|
109
|
+
},
|
110
|
+
|
111
|
+
// 10. 账户查找
|
112
|
+
async findAccount(ctx: KoaContextWithOIDC, id: string) {
|
113
|
+
logProvider('findAccount called for id: %s', id);
|
114
|
+
|
115
|
+
// 检查是否有预先存储的外部账户 ID
|
116
|
+
// @ts-ignore - 自定义属性
|
117
|
+
const externalAccountId = ctx.externalAccountId;
|
118
|
+
if (externalAccountId) {
|
119
|
+
logProvider('Found externalAccountId in context: %s', externalAccountId);
|
120
|
+
}
|
121
|
+
|
122
|
+
// 确定要查找的账户 ID
|
123
|
+
// 优先级: 1. externalAccountId 2. ctx.oidc.session?.accountId 3. 传入的 id
|
124
|
+
const accountIdToFind = externalAccountId || ctx.oidc?.session?.accountId || id;
|
125
|
+
|
126
|
+
logProvider(
|
127
|
+
'Attempting to find account with ID: %s (source: %s)',
|
128
|
+
accountIdToFind,
|
129
|
+
externalAccountId
|
130
|
+
? 'externalAccountId'
|
131
|
+
: ctx.oidc?.session?.accountId
|
132
|
+
? 'oidc_session'
|
133
|
+
: 'parameter_id',
|
134
|
+
);
|
135
|
+
|
136
|
+
// 如果没有可用的 ID,返回 undefined
|
137
|
+
if (!accountIdToFind) {
|
138
|
+
logProvider('findAccount: No account ID available, returning undefined.');
|
139
|
+
return undefined;
|
140
|
+
}
|
141
|
+
|
142
|
+
try {
|
143
|
+
const user = await UserModel.findById(db, accountIdToFind);
|
144
|
+
logProvider(
|
145
|
+
'UserModel.findById result for %s: %O',
|
146
|
+
accountIdToFind,
|
147
|
+
user ? { id: user.id, name: user.username } : null,
|
148
|
+
);
|
149
|
+
|
150
|
+
if (!user) {
|
151
|
+
logProvider('No user found for accountId: %s', accountIdToFind);
|
152
|
+
return undefined;
|
153
|
+
}
|
154
|
+
|
155
|
+
return {
|
156
|
+
accountId: user.id,
|
157
|
+
async claims(use, scope): Promise<{ [key: string]: any; sub: string }> {
|
158
|
+
logProvider('claims function called for user %s with scope: %s', user.id, scope);
|
159
|
+
const claims: { [key: string]: any; sub: string } = {
|
160
|
+
sub: user.id,
|
161
|
+
};
|
162
|
+
|
163
|
+
if (scope.includes('profile')) {
|
164
|
+
claims.name =
|
165
|
+
user.fullName ||
|
166
|
+
user.username ||
|
167
|
+
`${user.firstName || ''} ${user.lastName || ''}`.trim();
|
168
|
+
claims.picture = user.avatar;
|
169
|
+
}
|
170
|
+
|
171
|
+
if (scope.includes('email')) {
|
172
|
+
claims.email = user.email;
|
173
|
+
claims.email_verified = !!user.emailVerifiedAt;
|
174
|
+
}
|
175
|
+
|
176
|
+
logProvider('Returning claims: %O', claims);
|
177
|
+
return claims;
|
178
|
+
},
|
179
|
+
};
|
180
|
+
} catch (error) {
|
181
|
+
logProvider('Error finding account or generating claims: %O', error);
|
182
|
+
console.error('Error finding account:', error);
|
183
|
+
return undefined;
|
184
|
+
}
|
185
|
+
},
|
186
|
+
// 9. 交互策略
|
187
|
+
interactions: {
|
188
|
+
policy: createInteractionPolicy(),
|
189
|
+
url(ctx, interaction) {
|
190
|
+
// ---> 添加日志 <---
|
191
|
+
logProvider('interactions.url function called');
|
192
|
+
logProvider('Interaction details: %O', interaction);
|
193
|
+
const interactionUrl = `/oauth/consent/${interaction.uid}`;
|
194
|
+
logProvider('Generated interaction URL: %s', interactionUrl);
|
195
|
+
// ---> 添加日志结束 <---
|
196
|
+
return interactionUrl;
|
197
|
+
},
|
198
|
+
},
|
199
|
+
|
200
|
+
// 6. 密钥配置 - 使用 RS256 JWKS
|
201
|
+
jwks: jwks as { keys: any[] },
|
202
|
+
|
203
|
+
// 2. PKCE 配置
|
204
|
+
pkce: {
|
205
|
+
required: () => true,
|
206
|
+
},
|
207
|
+
|
208
|
+
// 12. 其他配置
|
209
|
+
renderError: async (ctx, out, error) => {
|
210
|
+
ctx.type = 'html';
|
211
|
+
ctx.body = `
|
212
|
+
<html>
|
213
|
+
<head>
|
214
|
+
<title>LobeHub OIDC Error</title>
|
215
|
+
</head>
|
216
|
+
<body>
|
217
|
+
<h1>LobeHub OIDC Error</h1>
|
218
|
+
<p>${JSON.stringify(error, null, 2)}</p>
|
219
|
+
<p>${JSON.stringify(out, null, 2)}</p>
|
220
|
+
</body>
|
221
|
+
</html>
|
222
|
+
`;
|
223
|
+
},
|
224
|
+
|
225
|
+
// 新增:启用 Refresh Token 轮换
|
226
|
+
rotateRefreshToken: true,
|
227
|
+
|
228
|
+
// 3. Scopes 定义
|
229
|
+
scopes: defaultScopes,
|
230
|
+
|
231
|
+
// 8. 令牌有效期
|
232
|
+
ttl: {
|
233
|
+
AccessToken: 3600, // 1 hour in seconds
|
234
|
+
AuthorizationCode: 600, // 10 minutes
|
235
|
+
DeviceCode: 600, // 10 minutes (if enabled)
|
236
|
+
|
237
|
+
IdToken: 3600, // 1 hour
|
238
|
+
Interaction: 3600, // 1 hour
|
239
|
+
|
240
|
+
RefreshToken: 30 * 24 * 60 * 60, // 30 days
|
241
|
+
Session: 30 * 24 * 60 * 60, // 30 days
|
242
|
+
},
|
243
|
+
};
|
244
|
+
|
245
|
+
// 创建提供者实例
|
246
|
+
const provider = new Provider(issuerUrl, configuration);
|
247
|
+
|
248
|
+
provider.on('server_error', (ctx, err) => {
|
249
|
+
logProvider('OIDC Provider Server Error: %O', err); // Use logProvider
|
250
|
+
console.error('OIDC Provider Error:', err);
|
251
|
+
});
|
252
|
+
|
253
|
+
provider.on('authorization.success', (ctx) => {
|
254
|
+
logProvider('Authorization successful for client: %s', ctx.oidc.client?.clientId); // Use logProvider
|
255
|
+
});
|
256
|
+
|
257
|
+
return provider;
|
258
|
+
};
|
259
|
+
|
260
|
+
export { type default as OIDCProvider } from 'oidc-provider';
|
@@ -13,6 +13,7 @@ import metadata from './metadata';
|
|
13
13
|
import migration from './migration';
|
14
14
|
import modelProvider from './modelProvider';
|
15
15
|
import models from './models';
|
16
|
+
import oauth from './oauth';
|
16
17
|
import plugin from './plugin';
|
17
18
|
import portal from './portal';
|
18
19
|
import providers from './providers';
|
@@ -39,6 +40,7 @@ const resources = {
|
|
39
40
|
migration,
|
40
41
|
modelProvider,
|
41
42
|
models,
|
43
|
+
oauth,
|
42
44
|
plugin,
|
43
45
|
portal,
|
44
46
|
providers,
|
@@ -0,0 +1,41 @@
|
|
1
|
+
const oauth = {
|
2
|
+
consent: {
|
3
|
+
buttons: {
|
4
|
+
accept: '授权',
|
5
|
+
deny: '拒绝',
|
6
|
+
},
|
7
|
+
description: '应用 {clientId} 请求访问您的 LobeChat 账户',
|
8
|
+
error: {
|
9
|
+
sessionInvalid: {
|
10
|
+
message: '授权会话已过期或无效,请重新发起授权流程。',
|
11
|
+
title: '授权会话无效',
|
12
|
+
},
|
13
|
+
title: '发生错误',
|
14
|
+
unsupportedInteraction: {
|
15
|
+
message: '不支持的交互类型: {promptName}',
|
16
|
+
title: '不支持的交互类型',
|
17
|
+
},
|
18
|
+
},
|
19
|
+
permissionsTitle: '应用请求以下权限:',
|
20
|
+
scope: {
|
21
|
+
'email': '访问您的电子邮件地址',
|
22
|
+
'offline_access': '在您离线时继续访问您的数据',
|
23
|
+
'openid': '使用您的 LobeChat 账户进行身份验证',
|
24
|
+
'profile': '访问您的基本资料信息(名称、头像等)',
|
25
|
+
'sync:read': '读取您的同步数据',
|
26
|
+
'sync:write': '写入并更新您的同步数据',
|
27
|
+
},
|
28
|
+
title: '授权请求',
|
29
|
+
},
|
30
|
+
failed: {
|
31
|
+
backToHome: '返回首页',
|
32
|
+
subTitle: '您已拒绝授权应用访问您的 LobeChat 账户',
|
33
|
+
title: '授权被拒绝',
|
34
|
+
},
|
35
|
+
success: {
|
36
|
+
subTitle: '您已成功授权应用访问您的 LobeChat 账户,可以关闭该页面了',
|
37
|
+
title: '授权成功',
|
38
|
+
},
|
39
|
+
};
|
40
|
+
|
41
|
+
export default oauth;
|