UNPKG

feishu-mcp

Version:

Model Context Protocol server for Feishu integration

199 lines (198 loc) 9.46 kB
import axios from 'axios'; import { Config } from '../utils/config.js'; import { Logger } from '../utils/logger.js'; import { TokenCacheManager } from '../utils/auth/tokenCacheManager.js'; import { AuthRequiredError } from '../utils/error.js'; export class AuthService { constructor() { Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: Config.getInstance() }); } // 获取用户信息 async getUserInfo(access_token) { Logger.warn('[AuthService] getUserInfo called'); try { const response = await axios.get('https://open.feishu.cn/open-apis/authen/v1/user_info', { headers: { Authorization: `Bearer ${access_token}` } }); Logger.debug('[AuthService] getUserInfo response', response.data); return response.data; } catch (error) { Logger.error('[AuthService] getUserInfo error', error); throw error; } } // 通过授权码换取user_access_token async getUserTokenByCode({ client_id, client_secret, code, redirect_uri, code_verifier }) { Logger.warn('[AuthService] getUserTokenByCode called', { client_id, code, redirect_uri }); const body = { grant_type: 'authorization_code', client_id, client_secret, code, redirect_uri }; if (code_verifier) body.code_verifier = code_verifier; Logger.debug('[AuthService] getUserTokenByCode request', body); const response = await fetch('https://open.feishu.cn/open-apis/authen/v2/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const data = await response.json(); Logger.debug('[AuthService] getUserTokenByCode response', data); return data; } /** * 刷新用户访问令牌 * 从缓存中获取token信息并刷新,如果缓存中没有必要信息则使用传入的备用参数 * @param clientKey 客户端缓存键 * @param appId 应用ID(可选,如果tokenInfo中没有则使用此参数) * @param appSecret 应用密钥(可选,如果tokenInfo中没有则使用此参数) * @returns 刷新后的token信息 * @throws 如果无法获取必要的刷新信息则抛出错误 */ async refreshUserToken(clientKey, appId, appSecret) { const tokenCacheManager = TokenCacheManager.getInstance(); // 从缓存中获取token信息 const tokenInfo = tokenCacheManager.getUserTokenInfo(clientKey); if (!tokenInfo) { throw new Error(`无法获取token信息: ${clientKey}`); } // 获取刷新所需的必要信息 const actualRefreshToken = tokenInfo.refresh_token; const actualAppId = tokenInfo.client_id || appId; const actualAppSecret = tokenInfo.client_secret || appSecret; // 验证必要参数 if (!actualRefreshToken) { throw new Error('无法获取refresh_token,无法刷新用户访问令牌'); } if (!actualAppId || !actualAppSecret) { throw new Error('无法获取client_id或client_secret,无法刷新用户访问令牌'); } const body = { grant_type: 'refresh_token', client_id: actualAppId, client_secret: actualAppSecret, refresh_token: actualRefreshToken }; Logger.debug('[AuthService] 刷新用户访问令牌请求:', { clientKey, client_id: actualAppId, has_refresh_token: !!actualRefreshToken }); const response = await axios.post('https://open.feishu.cn/open-apis/authen/v2/oauth/token', body, { headers: { 'Content-Type': 'application/json' } }); const data = response.data; if (data && data.access_token && data.expires_in) { // 计算过期时间戳 data.expires_at = Math.floor(Date.now() / 1000) + data.expires_in; if (data.refresh_token_expires_in) { data.refresh_token_expires_at = Math.floor(Date.now() / 1000) + data.refresh_token_expires_in; } // 保留client_id和client_secret(优先使用tokenInfo中的,如果没有则使用实际使用的参数) data.client_id = actualAppId; data.client_secret = actualAppSecret; // 缓存新的token信息 const refreshTtl = data.refresh_token_expires_in || 3600 * 24 * 365; // 默认1年 tokenCacheManager.cacheUserToken(clientKey, data, refreshTtl); Logger.info(`[AuthService] 用户访问令牌刷新并缓存成功: ${clientKey}`); return data; } else { Logger.warn('[AuthService] 刷新用户访问令牌失败:', data); throw new Error('刷新用户访问令牌失败'); } } /** * 获取租户访问令牌 * 从缓存中获取或请求新的租户token * @param appId 应用ID * @param appSecret 应用密钥 * @param clientKey 客户端缓存键 * @returns 租户访问令牌 */ async getTenantAccessToken(appId, appSecret, clientKey) { const tokenCacheManager = TokenCacheManager.getInstance(); const cachedToken = tokenCacheManager.getTenantToken(clientKey); if (cachedToken) { Logger.debug('使用缓存的租户访问令牌'); return cachedToken; } Logger.info('缓存中没有租户token,请求新的租户访问令牌'); try { const requestData = { app_id: appId, app_secret: appSecret }; const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal'; const headers = { 'Content-Type': 'application/json' }; Logger.debug('请求租户访问令牌:', url, requestData); const response = await axios.post(url, requestData, { headers }); const data = response.data; if (data.code !== 0) { throw new Error(`获取租户访问令牌失败:${data.msg || '未知错误'} (错误码: ${data.code})`); } if (!data.tenant_access_token) { throw new Error('获取租户访问令牌失败:响应中没有token'); } const expire_at = Math.floor(Date.now() / 1000) + (data.expire || 0); const tokenInfo = { app_access_token: data.tenant_access_token, expires_at: expire_at }; tokenCacheManager.cacheTenantToken(clientKey, tokenInfo, data.expire); Logger.info('租户访问令牌获取并缓存成功'); return data.tenant_access_token; } catch (error) { Logger.error('获取租户访问令牌失败:', error); throw new Error('获取租户访问令牌失败: ' + (error instanceof Error ? error.message : String(error))); } } /** * 获取用户访问令牌 * 检查token状态,如果有效则返回缓存的token,如果过期则尝试刷新 * @param clientKey 客户端缓存键 * @param appId 应用ID(可选,如果tokenInfo中没有则使用此参数) * @param appSecret 应用密钥(可选,如果tokenInfo中没有则使用此参数) * @returns 用户访问令牌 * @throws 如果无法获取有效的token则抛出AuthRequiredError */ async getUserAccessToken(clientKey, appId, appSecret) { const tokenCacheManager = TokenCacheManager.getInstance(); // 检查用户token状态 const tokenStatus = tokenCacheManager.checkUserTokenStatus(clientKey); Logger.debug(`[AuthService] 用户token状态:`, tokenStatus); if (tokenStatus.isValid && !tokenStatus.shouldRefresh) { // token有效且不需要刷新,直接返回 const cachedToken = tokenCacheManager.getUserToken(clientKey); if (cachedToken) { Logger.debug('[AuthService] 使用缓存的用户访问令牌'); return cachedToken; } } if (tokenStatus.canRefresh && (tokenStatus.isExpired || tokenStatus.shouldRefresh)) { // 可以刷新token Logger.info('[AuthService] 尝试刷新用户访问令牌'); try { // 使用统一的刷新方法,它会自动从缓存中获取必要信息 const refreshedToken = await this.refreshUserToken(clientKey, appId, appSecret); if (refreshedToken && refreshedToken.access_token) { Logger.info('[AuthService] 用户访问令牌刷新成功'); return refreshedToken.access_token; } } catch (error) { Logger.warn('[AuthService] 刷新用户访问令牌失败:', error); // 刷新失败,清除缓存,需要重新授权 tokenCacheManager.removeUserToken(clientKey); } } // 没有有效的token或刷新失败,需要用户授权 Logger.warn('[AuthService] 没有有效的用户token,需要用户授权'); throw new AuthRequiredError('user', '需要用户授权'); } }