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:
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user