diff --git a/src/oauth-handlers.js b/src/oauth-handlers.js index 071229e..38c0495 100644 --- a/src/oauth-handlers.js +++ b/src/oauth-handlers.js @@ -55,6 +55,54 @@ const QWEN_OAUTH_CONFIG = { logPrefix: '[Qwen Auth]' }; +/** + * Kiro OAuth 配置(支持多种认证方式) + */ +const KIRO_OAUTH_CONFIG = { + // Kiro Auth Service 端点 (用于 Social Auth) + authServiceEndpoint: 'https://prod.us-east-1.auth.desktop.kiro.dev', + + // AWS SSO OIDC 端点 (用于 Builder ID) + ssoOIDCEndpoint: 'https://oidc.us-east-1.amazonaws.com', + + // AWS Builder ID 起始 URL + builderIDStartURL: 'https://view.awsapps.com/start', + + // 本地回调端口范围(用于 Social Auth HTTP 回调) + callbackPortStart: 19876, + callbackPortEnd: 19880, + + // 超时配置 + authTimeout: 10 * 60 * 1000, // 10 分钟 + pollInterval: 5000, // 5 秒 + + // CodeWhisperer Scopes + scopes: [ + 'codewhisperer:completions', + 'codewhisperer:analysis', + 'codewhisperer:conversations', + 'codewhisperer:transformations', + 'codewhisperer:taskassist' + ], + + // 凭据存储(符合现有规范) + credentialsDir: '.kiro', + credentialsFile: 'oauth_creds.json', + + // 日志前缀 + logPrefix: '[Kiro Auth]' +}; + +/** + * 活动的 Kiro 回调服务器管理 + */ +const activeKiroServers = new Map(); + +/** + * 活动的 Kiro 轮询任务管理(用于 Builder ID Device Code) + */ +const activeKiroPollingTasks = new Map(); + /** * 生成 HTML 响应页面 * @param {boolean} isSuccess - 是否成功 @@ -514,4 +562,426 @@ export async function handleQwenOAuth(currentConfig, options = {}) { console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 请求失败:`, error); throw new Error(`Qwen OAuth 授权失败: ${error.message}`); } +} + +/** + * 处理 Kiro OAuth 授权(统一入口) + * @param {Object} currentConfig - 当前配置对象 + * @param {Object} options - 额外选项 + * - method: 'google' | 'github' | 'builder-id' + * - saveToConfigs: boolean + * @returns {Promise} 返回授权URL和相关信息 + */ +export async function handleKiroOAuth(currentConfig, options = {}) { + const method = options.method || 'google'; // 默认使用 Google + + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Starting OAuth with method: ${method}`); + + switch (method) { + case 'google': + return handleKiroSocialAuth('Google', currentConfig, options); + case 'github': + return handleKiroSocialAuth('Github', currentConfig, options); + case 'builder-id': + return handleKiroBuilderIDDeviceCode(currentConfig, options); + default: + throw new Error(`不支持的认证方式: ${method}`); + } +} + +/** + * Kiro Social Auth (Google/GitHub) - 使用 HTTP localhost 回调 + */ +async function handleKiroSocialAuth(provider, currentConfig, options = {}) { + // 生成 PKCE 参数 + const codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + const state = crypto.randomBytes(16).toString('base64url'); + + // 启动本地回调服务器并获取端口 + const handlerPort = await startKiroCallbackServer(codeVerifier, state, options); + + // 使用 HTTP localhost 作为 redirect_uri + const redirectUri = `http://127.0.0.1:${handlerPort}/oauth/callback`; + + // 构建授权 URL + const authUrl = `${KIRO_OAUTH_CONFIG.authServiceEndpoint}/login?` + + `idp=${provider}&` + + `redirect_uri=${encodeURIComponent(redirectUri)}&` + + `code_challenge=${codeChallenge}&` + + `code_challenge_method=S256&` + + `state=${state}&` + + `prompt=select_account`; + + return { + authUrl, + authInfo: { + provider: 'claude-kiro-oauth', + authMethod: 'social', + socialProvider: provider, + port: handlerPort, + redirectUri: redirectUri, + state: state + } + }; +} + +/** + * Kiro Builder ID - Device Code Flow(类似 Qwen OAuth 模式) + */ +async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) { + // 1. 注册 OIDC 客户端 + const regResponse = await fetch(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/client/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'KiroIDE' + }, + body: JSON.stringify({ + clientName: 'Kiro IDE', + clientType: 'public', + scopes: KIRO_OAUTH_CONFIG.scopes, + grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'] + }) + }); + + if (!regResponse.ok) { + throw new Error(`Kiro OAuth 客户端注册失败: ${regResponse.status}`); + } + + const regData = await regResponse.json(); + + // 2. 启动设备授权 + const authResponse = await fetch(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/device_authorization`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'KiroIDE' + }, + body: JSON.stringify({ + clientId: regData.clientId, + clientSecret: regData.clientSecret, + startUrl: KIRO_OAUTH_CONFIG.builderIDStartURL + }) + }); + + if (!authResponse.ok) { + throw new Error(`Kiro OAuth 设备授权失败: ${authResponse.status}`); + } + + const deviceAuth = await authResponse.json(); + + // 3. 启动后台轮询(类似 Qwen OAuth 的模式) + const taskId = `kiro-${deviceAuth.deviceCode.substring(0, 8)}-${Date.now()}`; + + // 停止之前的轮询任务 + for (const [existingTaskId] of activeKiroPollingTasks.entries()) { + if (existingTaskId.startsWith('kiro-')) { + stopKiroPollingTask(existingTaskId); + } + } + + // 异步轮询 + pollKiroBuilderIDToken( + regData.clientId, + regData.clientSecret, + deviceAuth.deviceCode, + deviceAuth.interval || 5, + deviceAuth.expiresIn || 300, + taskId, + options + ).catch(error => { + console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error); + broadcastEvent('oauth_error', { + provider: 'claude-kiro-oauth', + error: error.message, + timestamp: new Date().toISOString() + }); + }); + + return { + authUrl: deviceAuth.verificationUriComplete, + authInfo: { + provider: 'claude-kiro-oauth', + authMethod: 'builder-id', + deviceCode: deviceAuth.deviceCode, + userCode: deviceAuth.userCode, + verificationUri: deviceAuth.verificationUri, + verificationUriComplete: deviceAuth.verificationUriComplete, + expiresIn: deviceAuth.expiresIn, + interval: deviceAuth.interval + } + }; +} + +/** + * 轮询获取 Kiro Builder ID Token + */ +async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interval, expiresIn, taskId, options = {}) { + let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile); + const maxAttempts = Math.floor(expiresIn / interval); + let attempts = 0; + + const taskControl = { shouldStop: false }; + activeKiroPollingTasks.set(taskId, taskControl); + + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}]`); + + const poll = async () => { + if (taskControl.shouldStop) { + throw new Error('轮询任务已被取消'); + } + + if (attempts >= maxAttempts) { + activeKiroPollingTasks.delete(taskId); + throw new Error('授权超时'); + } + + attempts++; + + try { + const response = await fetch(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'KiroIDE' + }, + body: JSON.stringify({ + clientId, + clientSecret, + deviceCode, + grantType: 'urn:ietf:params:oauth:grant-type:device_code' + }) + }); + + const data = await response.json(); + + if (response.ok && data.accessToken) { + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`); + + // 保存令牌(符合现有规范) + if (options.saveToConfigs) { + const targetDir = path.join(process.cwd(), 'configs', 'kiro'); + await fs.promises.mkdir(targetDir, { recursive: true }); + const timestamp = Date.now(); + credPath = path.join(targetDir, `${timestamp}_oauth_creds.json`); + } + + const tokenData = { + accessToken: data.accessToken, + refreshToken: data.refreshToken, + expiresAt: new Date(Date.now() + data.expiresIn * 1000).toISOString(), + authMethod: 'builder-id', + clientId, + clientSecret, + region: 'us-east-1' + }; + + await fs.promises.mkdir(path.dirname(credPath), { recursive: true }); + await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2)); + + activeKiroPollingTasks.delete(taskId); + + // 广播成功事件(符合现有规范) + broadcastEvent('oauth_success', { + provider: 'claude-kiro-oauth', + credPath, + relativePath: path.relative(process.cwd(), credPath), + timestamp: new Date().toISOString() + }); + + return tokenData; + } + + // 检查错误类型 + if (data.error === 'authorization_pending') { + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (${attempts}/${maxAttempts})`); + await new Promise(resolve => setTimeout(resolve, interval * 1000)); + return poll(); + } else if (data.error === 'slow_down') { + await new Promise(resolve => setTimeout(resolve, (interval + 5) * 1000)); + return poll(); + } else { + activeKiroPollingTasks.delete(taskId); + throw new Error(`授权失败: ${data.error || '未知错误'}`); + } + } catch (error) { + if (error.message.includes('授权') || error.message.includes('取消')) { + throw error; + } + await new Promise(resolve => setTimeout(resolve, interval * 1000)); + return poll(); + } + }; + + return poll(); +} + +/** + * 停止 Kiro 轮询任务 + */ +function stopKiroPollingTask(taskId) { + const task = activeKiroPollingTasks.get(taskId); + if (task) { + task.shouldStop = true; + activeKiroPollingTasks.delete(taskId); + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`); + } +} + +/** + * 启动 Kiro 回调服务器(用于 Social Auth HTTP 回调) + */ +async function startKiroCallbackServer(codeVerifier, expectedState, options = {}) { + const portStart = KIRO_OAUTH_CONFIG.callbackPortStart; + const portEnd = KIRO_OAUTH_CONFIG.callbackPortEnd; + + for (let port = portStart; port <= portEnd; port++) { + // 关闭已存在的服务器 + await closeKiroServer(port); + + try { + const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options); + activeKiroServers.set(port, server); + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`); + return port; + } catch (err) { + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 端口 ${port} 被占用,尝试下一个...`); + } + } + + throw new Error('所有端口都被占用'); +} + +/** + * 关闭 Kiro 服务器 + */ +async function closeKiroServer(port) { + const existingServer = activeKiroServers.get(port); + if (existingServer && existingServer.listening) { + return new Promise((resolve) => { + existingServer.close(() => { + activeKiroServers.delete(port); + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); + resolve(); + }); + }); + } +} + +/** + * 创建 Kiro HTTP 回调服务器 + */ +function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options = {}) { + const redirectUri = `http://127.0.0.1:${port}/oauth/callback`; + + return new Promise((resolve, reject) => { + const server = http.createServer(async (req, res) => { + try { + const url = new URL(req.url, `http://127.0.0.1:${port}`); + + if (url.pathname === '/oauth/callback') { + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const errorParam = url.searchParams.get('error'); + + if (errorParam) { + res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(generateResponsePage(false, `授权失败: ${errorParam}`)); + return; + } + + if (state !== expectedState) { + res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(generateResponsePage(false, 'State 验证失败')); + return; + } + + // 交换 Code 获取 Token(使用动态的 redirect_uri) + const tokenResponse = await fetch(`${KIRO_OAUTH_CONFIG.authServiceEndpoint}/oauth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'AIClient-2-API/1.0.0' + }, + body: JSON.stringify({ + code, + code_verifier: codeVerifier, + redirect_uri: redirectUri + }) + }); + + if (!tokenResponse.ok) { + const errorText = await tokenResponse.text(); + console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token exchange failed:`, errorText); + res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(generateResponsePage(false, `获取令牌失败: ${tokenResponse.status}`)); + return; + } + + const tokenData = await tokenResponse.json(); + + // 保存令牌 + let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile); + + if (options.saveToConfigs) { + const targetDir = path.join(process.cwd(), 'configs', 'kiro'); + await fs.promises.mkdir(targetDir, { recursive: true }); + const timestamp = Date.now(); + credPath = path.join(targetDir, `${timestamp}_oauth_creds.json`); + } + + const saveData = { + accessToken: tokenData.accessToken, + refreshToken: tokenData.refreshToken, + profileArn: tokenData.profileArn, + expiresAt: new Date(Date.now() + (tokenData.expiresIn || 3600) * 1000).toISOString(), + authMethod: 'social', + region: 'us-east-1' + }; + + await fs.promises.mkdir(path.dirname(credPath), { recursive: true }); + await fs.promises.writeFile(credPath, JSON.stringify(saveData, null, 2)); + + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 令牌已保存: ${credPath}`); + + // 广播成功事件 + broadcastEvent('oauth_success', { + provider: 'claude-kiro-oauth', + credPath, + relativePath: path.relative(process.cwd(), credPath), + timestamp: new Date().toISOString() + }); + + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(generateResponsePage(true, '授权成功!您可以关闭此页面')); + + // 关闭服务器 + server.close(() => { + activeKiroServers.delete(port); + }); + + } else { + res.writeHead(204); + res.end(); + } + } catch (error) { + console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error); + res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(generateResponsePage(false, `服务器错误: ${error.message}`)); + } + }); + + server.on('error', reject); + server.listen(port, '127.0.0.1', () => resolve(server)); + + // 超时自动关闭 + setTimeout(() => { + if (server.listening) { + server.close(() => { + activeKiroServers.delete(port); + }); + } + }, KIRO_OAUTH_CONFIG.authTimeout); + }); } \ No newline at end of file diff --git a/src/ui-manager.js b/src/ui-manager.js index 3e0f1ef..626af9a 100644 --- a/src/ui-manager.js +++ b/src/ui-manager.js @@ -9,7 +9,7 @@ import { getAllProviderModels, getProviderModels } from './provider-models.js'; import { CONFIG } from './config-manager.js'; import { serviceInstances, getServiceAdapter } from './adapter.js'; import { initApiService } from './service-manager.js'; -import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, handleQwenOAuth } from './oauth-handlers.js'; +import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, handleQwenOAuth, handleKiroOAuth } from './oauth-handlers.js'; import { generateUUID, normalizePath, @@ -1372,6 +1372,12 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo const result = await handleQwenOAuth(currentConfig, options); authUrl = result.authUrl; authInfo = result.authInfo; + } else if (providerType === 'claude-kiro-oauth') { + // Kiro OAuth 支持多种认证方式 + // options.method 可以是: 'google' | 'github' | 'builder-id' + const result = await handleKiroOAuth(currentConfig, options); + authUrl = result.authUrl; + authInfo = result.authInfo; } else { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js index 1ee7618..c98c97c 100644 --- a/static/app/provider-manager.js +++ b/static/app/provider-manager.js @@ -322,7 +322,7 @@ async function openProviderManager(providerType) { */ function generateAuthButton(providerType) { // 只为支持OAuth的提供商显示授权按钮 - const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth']; + const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'claude-kiro-oauth']; if (!oauthProviders.includes(providerType)) { return ''; @@ -341,6 +341,97 @@ function generateAuthButton(providerType) { * @param {string} providerType - 提供商类型 */ async function handleGenerateAuthUrl(providerType) { + // 如果是 Kiro OAuth,先显示认证方式选择对话框 + if (providerType === 'claude-kiro-oauth') { + showKiroAuthMethodSelector(providerType); + return; + } + + await executeGenerateAuthUrl(providerType, {}); +} + +/** + * 显示 Kiro OAuth 认证方式选择对话框 + * @param {string} providerType - 提供商类型 + */ +function showKiroAuthMethodSelector(providerType) { + const modal = document.createElement('div'); + modal.className = 'modal-overlay'; + modal.style.display = 'flex'; + + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + + // 关闭按钮事件 + const closeBtn = modal.querySelector('.modal-close'); + const cancelBtn = modal.querySelector('.modal-cancel'); + [closeBtn, cancelBtn].forEach(btn => { + btn.addEventListener('click', () => { + modal.remove(); + }); + }); + + // 认证方式选择按钮事件 + const methodBtns = modal.querySelectorAll('.auth-method-btn'); + methodBtns.forEach(btn => { + btn.addEventListener('mouseenter', () => { + btn.style.borderColor = '#00a67e'; + btn.style.background = '#f8fffe'; + }); + btn.addEventListener('mouseleave', () => { + btn.style.borderColor = '#e0e0e0'; + btn.style.background = 'white'; + }); + btn.addEventListener('click', async () => { + const method = btn.dataset.method; + modal.remove(); + await executeGenerateAuthUrl(providerType, { method }); + }); + }); +} + +/** + * 执行生成授权链接 + * @param {string} providerType - 提供商类型 + * @param {Object} extraOptions - 额外选项 + */ +async function executeGenerateAuthUrl(providerType, extraOptions = {}) { try { showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info'); @@ -351,7 +442,8 @@ async function handleGenerateAuthUrl(providerType) { `/providers/${encodeURIComponent(providerType)}/generate-auth-url`, { saveToConfigs: true, - providerDir: providerDir + providerDir: providerDir, + ...extraOptions } ); @@ -408,6 +500,20 @@ function showAuthModal(authUrl, authInfo) { `; + } else if (authInfo.provider === 'claude-kiro-oauth') { + const methodDisplay = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : `Social (${authInfo.socialProvider || 'Google'})`; + instructionsHtml = ` +
+

${t('oauth.modal.steps')}

+

认证方式: ${methodDisplay}

+
    +
  1. 点击下方按钮在浏览器中打开授权链接
  2. +
  3. 使用您的 ${authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : authInfo.socialProvider || 'Google'} 账号登录
  4. +
  5. 授权完成后页面会自动关闭
  6. +
  7. 刷新本页面查看凭据文件
  8. +
+
+ `; } else { instructionsHtml = `