Merge pull request #119 from Ravens2121/feature/claude-kiro-oauth

feat: 为 claude-kiro-oauth 提供商添加 OAuth 授权生成功能(支持 Google/GitHub/AWS Builder ID)
This commit is contained in:
何夕2077 2025-12-21 18:50:17 +08:00 committed by GitHub
commit abf874b43c
3 changed files with 585 additions and 3 deletions

View file

@ -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<Object>} 返回授权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);
});
}

View file

@ -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({

View file

@ -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 = `
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3><i class="fas fa-key"></i> </h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="auth-method-options" style="display: flex; flex-direction: column; gap: 12px;">
<button class="auth-method-btn" data-method="google" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-google" style="font-size: 24px; color: #4285f4;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;">Google 账号登录</div>
<div style="font-size: 12px; color: #666;">使用 Google 账号进行社交登录</div>
</div>
</button>
<button class="auth-method-btn" data-method="github" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-github" style="font-size: 24px; color: #333;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;">GitHub 账号登录</div>
<div style="font-size: 12px; color: #666;">使用 GitHub 账号进行社交登录</div>
</div>
</button>
<button class="auth-method-btn" data-method="builder-id" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-aws" style="font-size: 24px; color: #ff9900;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;">AWS Builder ID</div>
<div style="font-size: 12px; color: #666;">使用 AWS Builder ID 进行设备码授权</div>
</div>
</button>
</div>
</div>
<div class="modal-footer">
<button class="modal-cancel">${t('modal.provider.cancel')}</button>
</div>
</div>
`;
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) {
</ol>
</div>
`;
} else if (authInfo.provider === 'claude-kiro-oauth') {
const methodDisplay = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : `Social (${authInfo.socialProvider || 'Google'})`;
instructionsHtml = `
<div class="auth-instructions">
<h4 data-i18n="oauth.modal.steps">${t('oauth.modal.steps')}</h4>
<p><strong>认证方式:</strong> ${methodDisplay}</p>
<ol>
<li>点击下方按钮在浏览器中打开授权链接</li>
<li>使用您的 ${authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : authInfo.socialProvider || 'Google'} 账号登录</li>
<li>授权完成后页面会自动关闭</li>
<li>刷新本页面查看凭据文件</li>
</ol>
</div>
`;
} else {
instructionsHtml = `
<div class="auth-instructions">