UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

290 lines (244 loc) 8.89 kB
import debug from 'debug'; import Provider, { Configuration, KoaContextWithOIDC } from 'oidc-provider'; import urlJoin from 'url-join'; import { serverDBEnv } from '@/config/db'; import { UserModel } from '@/database/models/user'; import { LobeChatDatabase } from '@/database/type'; import { appEnv } from '@/envs/app'; import { oidcEnv } from '@/envs/oidc'; import { DrizzleAdapter } from './adapter'; import { defaultClaims, defaultClients, defaultScopes } from './config'; import { createInteractionPolicy } from './interaction-policy'; const logProvider = debug('lobe-oidc:provider'); // <--- 添加 provider 日志实例 /** * 从环境变量中获取 JWKS * 该 JWKS 是一个包含 RS256 私钥的 JSON 对象 */ const getJWKS = (): object => { try { const jwksString = oidcEnv.OIDC_JWKS_KEY; if (!jwksString) { throw new Error( 'OIDC_JWKS_KEY 环境变量是必需的。请使用 scripts/generate-oidc-jwk.mjs 生成 JWKS。', ); } // 尝试解析 JWKS JSON 字符串 const jwks = JSON.parse(jwksString); // 检查 JWKS 格式是否正确 if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) { throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组'); } // 检查是否有 RS256 算法的密钥 const hasRS256Key = jwks.keys.some((key: any) => key.alg === 'RS256' && key.kty === 'RSA'); if (!hasRS256Key) { throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥'); } return jwks; } catch (error) { console.error('解析 JWKS 失败:', error); throw new Error(`OIDC_JWKS_KEY 解析错误: ${(error as Error).message}`); } }; /** * 获取 Cookie 密钥,使用 KEY_VAULTS_SECRET */ const getCookieKeys = () => { const key = serverDBEnv.KEY_VAULTS_SECRET; if (!key) { throw new Error('KEY_VAULTS_SECRET is required for OIDC Provider cookie encryption'); } return [key]; }; /** * 创建 OIDC Provider 实例 * @param db - 数据库实例 * @returns 配置好的 OIDC Provider 实例 */ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider> => { // 获取 JWKS const jwks = getJWKS(); const cookieKeys = getCookieKeys(); const configuration: Configuration = { // 11. 数据库适配器 adapter: DrizzleAdapter.createAdapterFactory(db), // 4. Claims 定义 claims: defaultClaims, // 新增:客户端 CORS 控制逻辑 clientBasedCORS(ctx, origin, client) { // 检查客户端是否允许此来源 // 一个常见的策略是允许所有已注册的 redirect_uris 的来源 if (!client || !client.redirectUris) { logProvider('clientBasedCORS: No client or redirectUris found, denying origin: %s', origin); return false; // 如果没有客户端或重定向URI,则拒绝 } const allowed = client.redirectUris.some((uri) => { try { // 比较来源 (scheme, hostname, port) return new URL(uri).origin === origin; } catch { // 如果 redirect_uri 不是有效的 URL (例如自定义协议),则跳过 return false; } }); logProvider( 'clientBasedCORS check for origin [%s] and client [%s]: %s', origin, client.clientId, allowed ? 'Allowed' : 'Denied', ); return allowed; }, // 1. 客户端配置 clients: defaultClients, // 7. Cookie 配置 cookies: { keys: cookieKeys, long: { path: '/', signed: true }, short: { path: '/', signed: true }, }, // 5. 特性配置 features: { backchannelLogout: { enabled: true }, clientCredentials: { enabled: false }, devInteractions: { enabled: false }, deviceFlow: { enabled: false }, introspection: { enabled: true }, resourceIndicators: { enabled: false }, revocation: { enabled: true }, rpInitiatedLogout: { enabled: true }, userinfo: { enabled: true }, }, // 10. 账户查找 async findAccount(ctx: KoaContextWithOIDC, id: string) { logProvider('findAccount called for id: %s', id); // 检查是否有预先存储的外部账户 ID // @ts-ignore - 自定义属性 const externalAccountId = ctx.externalAccountId; if (externalAccountId) { logProvider('Found externalAccountId in context: %s', externalAccountId); } // 确定要查找的账户 ID // 优先级: 1. externalAccountId 2. ctx.oidc.session?.accountId 3. 传入的 id const accountIdToFind = externalAccountId || ctx.oidc?.session?.accountId || id; logProvider( 'Attempting to find account with ID: %s (source: %s)', accountIdToFind, externalAccountId ? 'externalAccountId' : ctx.oidc?.session?.accountId ? 'oidc_session' : 'parameter_id', ); // 如果没有可用的 ID,返回 undefined if (!accountIdToFind) { logProvider('findAccount: No account ID available, returning undefined.'); return undefined; } try { const user = await UserModel.findById(db, accountIdToFind); logProvider( 'UserModel.findById result for %s: %O', accountIdToFind, user ? { id: user.id, name: user.username } : null, ); if (!user) { logProvider('No user found for accountId: %s', accountIdToFind); return undefined; } return { accountId: user.id, async claims(use, scope): Promise<{ [key: string]: any; sub: string }> { logProvider('claims function called for user %s with scope: %s', user.id, scope); const claims: { [key: string]: any; sub: string } = { sub: user.id, }; if (scope.includes('profile')) { claims.name = user.fullName || user.username || `${user.firstName || ''} ${user.lastName || ''}`.trim(); claims.picture = user.avatar; } if (scope.includes('email')) { claims.email = user.email; claims.email_verified = !!user.emailVerifiedAt; } logProvider('Returning claims: %O', claims); return claims; }, }; } catch (error) { logProvider('Error finding account or generating claims: %O', error); console.error('Error finding account:', error); return undefined; } }, // 9. 交互策略 interactions: { policy: createInteractionPolicy(), url(ctx, interaction) { // ---> 添加日志 <--- logProvider('interactions.url function called'); logProvider('Interaction details: %O', interaction); const interactionUrl = `/oauth/consent/${interaction.uid}`; logProvider('Generated interaction URL: %s', interactionUrl); // ---> 添加日志结束 <--- return interactionUrl; }, }, // 6. 密钥配置 - 使用 RS256 JWKS jwks: jwks as { keys: any[] }, // 2. PKCE 配置 pkce: { required: () => true, }, // 12. 其他配置 renderError: async (ctx, out, error) => { ctx.type = 'html'; ctx.body = ` <html> <head> <title>LobeHub OIDC Error</title> </head> <body> <h1>LobeHub OIDC Error</h1> <p>${JSON.stringify(error, null, 2)}</p> <p>${JSON.stringify(out, null, 2)}</p> </body> </html> `; }, // 新增:启用 Refresh Token 轮换 rotateRefreshToken: true, routes: { authorization: '/oidc/auth', end_session: '/oidc/session/end', token: '/oidc/token', }, // 3. Scopes 定义 scopes: defaultScopes, // 8. 令牌有效期 ttl: { AccessToken: 7 * 24 * 60 * 60, // 1 week temporarily,need to revert 1 hour with better implement AuthorizationCode: 600, // 10 minutes DeviceCode: 600, // 10 minutes (if enabled) IdToken: 3600, // 1 hour Interaction: 3600, // 1 hour RefreshToken: 30 * 24 * 60 * 60, // 30 days Session: 30 * 24 * 60 * 60, // 30 days }, }; // 创建提供者实例 const baseUrl = urlJoin(appEnv.APP_URL!, '/oidc'); const provider = new Provider(baseUrl, configuration); provider.proxy = true; provider.on('server_error', (ctx, err) => { logProvider('OIDC Provider Server Error: %O', err); // Use logProvider console.error('OIDC Provider Error:', err); }); provider.on('authorization.success', (ctx) => { logProvider('Authorization successful for client: %s', ctx.oidc.client?.clientId); // Use logProvider }); return provider; }; export { type default as OIDCProvider } from 'oidc-provider';