From 4d9da5db26a0741009aa2954c1fc84df0b8bf80b Mon Sep 17 00:00:00 2001 From: quanturbo Date: Fri, 26 Jun 2026 10:31:30 +0700 Subject: [PATCH] fix: support Kiro IDC (organization) token import When logged in to Kiro IDE as an organization (AWS IAM Identity Center), token import fails because IDC tokens require clientId/clientSecret for refresh and use a different profileArn than social/builder-id accounts. Changes: - auto-import: read clientId/clientSecret from SSO cache client registration file, read profileArn from Kiro IDE profile.json, normalize ARN region - import: accept IDC credentials, use KiroService.refreshToken with them, persist credentials for future automatic refreshes - KiroAuthModal: pass IDC credentials from auto-detect through to import Co-authored-by: Cursor --- src/app/api/oauth/kiro/auto-import/route.js | 61 ++++++++++++++++++--- src/app/api/oauth/kiro/import/route.js | 32 +++++++---- src/shared/components/KiroAuthModal.js | 17 +++++- 3 files changed, 91 insertions(+), 19 deletions(-) diff --git a/src/app/api/oauth/kiro/auto-import/route.js b/src/app/api/oauth/kiro/auto-import/route.js index e2a6182f..0d28ea6e 100644 --- a/src/app/api/oauth/kiro/auto-import/route.js +++ b/src/app/api/oauth/kiro/auto-import/route.js @@ -5,13 +5,14 @@ import { join } from "path"; /** * GET /api/oauth/kiro/auto-import - * Auto-detect and extract Kiro refresh token from AWS SSO cache + * Auto-detect and extract Kiro refresh token from AWS SSO cache. + * For IDC (organization) tokens, also resolves clientId/clientSecret from the + * linked client registration file so token refresh works. */ export async function GET() { try { const cachePath = join(homedir(), ".aws/sso/cache"); - // Try to read cache directory let files; try { files = await readdir(cachePath); @@ -22,9 +23,9 @@ export async function GET() { }); } - // Look for kiro-auth-token.json or any .json file with refreshToken let refreshToken = null; let foundFile = null; + let tokenData = null; // First try kiro-auth-token.json const kiroTokenFile = "kiro-auth-token.json"; @@ -35,6 +36,7 @@ export async function GET() { if (data.refreshToken && data.refreshToken.startsWith("aorAAAAAG")) { refreshToken = data.refreshToken; foundFile = kiroTokenFile; + tokenData = data; } } catch (error) { // Continue to search other files @@ -45,19 +47,16 @@ export async function GET() { if (!refreshToken) { for (const file of files) { if (!file.endsWith(".json")) continue; - try { const content = await readFile(join(cachePath, file), "utf-8"); const data = JSON.parse(content); - - // Look for Kiro refresh token (starts with aorAAAAAG) if (data.refreshToken && data.refreshToken.startsWith("aorAAAAAG")) { refreshToken = data.refreshToken; foundFile = file; + tokenData = data; break; } } catch (error) { - // Skip invalid JSON files continue; } } @@ -70,10 +69,58 @@ export async function GET() { }); } + // For IDC/organization tokens, resolve clientId and clientSecret from + // the linked client registration file (referenced by clientIdHash). + let clientId = null; + let clientSecret = null; + const region = tokenData?.region || null; + const authMethod = tokenData?.authMethod || null; + + if (tokenData?.clientIdHash) { + const clientFile = `${tokenData.clientIdHash}.json`; + try { + const clientContent = await readFile(join(cachePath, clientFile), "utf-8"); + const clientData = JSON.parse(clientContent); + if (clientData.clientId && clientData.clientSecret) { + clientId = clientData.clientId; + clientSecret = clientData.clientSecret; + } + } catch (error) { + // Client registration file not found - continue without it + } + } + + // Read profileArn from Kiro IDE's profile.json. + // Important: the runtime gateway requires us-east-1 in the ARN regardless + // of the IDC region, so we normalize the region in the ARN to us-east-1. + let profileArn = null; + const kiroProfilePaths = [ + join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "Kiro", "User", "globalStorage", "kiro.kiroagent", "profile.json"), + join(homedir(), ".config", "Kiro", "User", "globalStorage", "kiro.kiroagent", "profile.json"), + ]; + for (const profilePath of kiroProfilePaths) { + try { + const profileContent = await readFile(profilePath, "utf-8"); + const profileData = JSON.parse(profileContent); + if (profileData.arn) { + // Normalize region to us-east-1 for the runtime gateway + profileArn = profileData.arn.replace(/arn:aws:codewhisperer:[^:]+:/, "arn:aws:codewhisperer:us-east-1:"); + break; + } + } catch (error) { + continue; + } + } + return NextResponse.json({ found: true, refreshToken, source: foundFile, + clientId, + clientSecret, + region, + authMethod, + profileArn, }); } catch (error) { console.log("Kiro auto-import error:", error); diff --git a/src/app/api/oauth/kiro/import/route.js b/src/app/api/oauth/kiro/import/route.js index a278a1e8..46383410 100644 --- a/src/app/api/oauth/kiro/import/route.js +++ b/src/app/api/oauth/kiro/import/route.js @@ -4,11 +4,13 @@ import { createProviderConnection } from "@/models"; /** * POST /api/oauth/kiro/import - * Import and validate refresh token from Kiro IDE + * Import and validate refresh token from Kiro IDE. + * For IDC (organization) tokens, accepts clientId/clientSecret/region so the + * token can be refreshed via the regional AWS OIDC endpoint. */ export async function POST(request) { try { - const { refreshToken } = await request.json(); + const { refreshToken, clientId, clientSecret, region, authMethod, profileArn } = await request.json(); if (!refreshToken || typeof refreshToken !== "string") { return NextResponse.json( @@ -18,25 +20,33 @@ export async function POST(request) { } const kiroService = new KiroService(); + const isIdc = !!(clientId && clientSecret); - // Validate and refresh token - const tokenData = await kiroService.validateImportToken(refreshToken.trim()); + // For IDC tokens, refresh via the regional OIDC endpoint with client credentials. + // For social/builder-id tokens, use the standard social refresh endpoint. + const providerSpecificData = isIdc + ? { clientId, clientSecret, region: region || "us-east-1", authMethod: "idc" } + : {}; + + const tokenData = await kiroService.refreshToken(refreshToken.trim(), providerSpecificData); - // Extract email from JWT if available const email = kiroService.extractEmailFromJWT(tokenData.accessToken); + const resolvedAuthMethod = isIdc ? "idc" : "imported"; + const providerLabel = isIdc ? "Enterprise" : "Imported"; + const resolvedProfileArn = profileArn || tokenData.profileArn || null; - // Save to database const connection = await createProviderConnection({ provider: "kiro", authType: "oauth", accessToken: tokenData.accessToken, - refreshToken: tokenData.refreshToken, - expiresAt: new Date(Date.now() + tokenData.expiresIn * 1000).toISOString(), + refreshToken: tokenData.refreshToken || refreshToken.trim(), + expiresAt: new Date(Date.now() + (tokenData.expiresIn || 3600) * 1000).toISOString(), email: email || null, providerSpecificData: { - profileArn: tokenData.profileArn, - authMethod: "imported", - provider: "Imported", + profileArn: resolvedProfileArn, + authMethod: resolvedAuthMethod, + provider: providerLabel, + ...(isIdc ? { clientId, clientSecret, region: region || "us-east-1" } : {}), }, testStatus: "active", }); diff --git a/src/shared/components/KiroAuthModal.js b/src/shared/components/KiroAuthModal.js index 6d38c1e6..d5325f95 100644 --- a/src/shared/components/KiroAuthModal.js +++ b/src/shared/components/KiroAuthModal.js @@ -19,6 +19,7 @@ export default function KiroAuthModal({ isOpen, onMethodSelect, onClose }) { const [importing, setImporting] = useState(false); const [autoDetecting, setAutoDetecting] = useState(false); const [autoDetected, setAutoDetected] = useState(false); + const [idcCredentials, setIdcCredentials] = useState(null); // Auto-detect token when import method is selected useEffect(() => { @@ -28,6 +29,7 @@ export default function KiroAuthModal({ isOpen, onMethodSelect, onClose }) { setAutoDetecting(true); setError(null); setAutoDetected(false); + setIdcCredentials(null); try { const res = await fetch("/api/oauth/kiro/auto-import"); @@ -36,6 +38,16 @@ export default function KiroAuthModal({ isOpen, onMethodSelect, onClose }) { if (data.found) { setRefreshToken(data.refreshToken); setAutoDetected(true); + // Store IDC/organization credentials if present + if (data.clientId && data.clientSecret) { + setIdcCredentials({ + clientId: data.clientId, + clientSecret: data.clientSecret, + region: data.region, + authMethod: data.authMethod, + profileArn: data.profileArn, + }); + } } else { setError(data.error || "Could not auto-detect token"); } @@ -72,7 +84,10 @@ export default function KiroAuthModal({ isOpen, onMethodSelect, onClose }) { const res = await fetch("/api/oauth/kiro/import", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ refreshToken: refreshToken.trim() }), + body: JSON.stringify({ + refreshToken: refreshToken.trim(), + ...(idcCredentials || {}), + }), }); const data = await res.json();