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 <cursoragent@cursor.com>
This commit is contained in:
quanturbo
2026-06-26 10:31:30 +07:00
committed by decolua
parent ce844899ed
commit 4d9da5db26
3 changed files with 91 additions and 19 deletions

View File

@@ -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);

View File

@@ -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",
});

View File

@@ -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();