mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
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:
committed by
decolua
co-authored by
Cursor
parent
ce844899ed
commit
4d9da5db26
@@ -5,13 +5,14 @@ import { join } from "path";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/oauth/kiro/auto-import
|
* 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() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const cachePath = join(homedir(), ".aws/sso/cache");
|
const cachePath = join(homedir(), ".aws/sso/cache");
|
||||||
|
|
||||||
// Try to read cache directory
|
|
||||||
let files;
|
let files;
|
||||||
try {
|
try {
|
||||||
files = await readdir(cachePath);
|
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 refreshToken = null;
|
||||||
let foundFile = null;
|
let foundFile = null;
|
||||||
|
let tokenData = null;
|
||||||
|
|
||||||
// First try kiro-auth-token.json
|
// First try kiro-auth-token.json
|
||||||
const kiroTokenFile = "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")) {
|
if (data.refreshToken && data.refreshToken.startsWith("aorAAAAAG")) {
|
||||||
refreshToken = data.refreshToken;
|
refreshToken = data.refreshToken;
|
||||||
foundFile = kiroTokenFile;
|
foundFile = kiroTokenFile;
|
||||||
|
tokenData = data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Continue to search other files
|
// Continue to search other files
|
||||||
@@ -45,19 +47,16 @@ export async function GET() {
|
|||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.endsWith(".json")) continue;
|
if (!file.endsWith(".json")) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = await readFile(join(cachePath, file), "utf-8");
|
const content = await readFile(join(cachePath, file), "utf-8");
|
||||||
const data = JSON.parse(content);
|
const data = JSON.parse(content);
|
||||||
|
|
||||||
// Look for Kiro refresh token (starts with aorAAAAAG)
|
|
||||||
if (data.refreshToken && data.refreshToken.startsWith("aorAAAAAG")) {
|
if (data.refreshToken && data.refreshToken.startsWith("aorAAAAAG")) {
|
||||||
refreshToken = data.refreshToken;
|
refreshToken = data.refreshToken;
|
||||||
foundFile = file;
|
foundFile = file;
|
||||||
|
tokenData = data;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Skip invalid JSON files
|
|
||||||
continue;
|
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({
|
return NextResponse.json({
|
||||||
found: true,
|
found: true,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
source: foundFile,
|
source: foundFile,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
region,
|
||||||
|
authMethod,
|
||||||
|
profileArn,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Kiro auto-import error:", error);
|
console.log("Kiro auto-import error:", error);
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { createProviderConnection } from "@/models";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/oauth/kiro/import
|
* 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) {
|
export async function POST(request) {
|
||||||
try {
|
try {
|
||||||
const { refreshToken } = await request.json();
|
const { refreshToken, clientId, clientSecret, region, authMethod, profileArn } = await request.json();
|
||||||
|
|
||||||
if (!refreshToken || typeof refreshToken !== "string") {
|
if (!refreshToken || typeof refreshToken !== "string") {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -18,25 +20,33 @@ export async function POST(request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const kiroService = new KiroService();
|
const kiroService = new KiroService();
|
||||||
|
const isIdc = !!(clientId && clientSecret);
|
||||||
|
|
||||||
// Validate and refresh token
|
// For IDC tokens, refresh via the regional OIDC endpoint with client credentials.
|
||||||
const tokenData = await kiroService.validateImportToken(refreshToken.trim());
|
// 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 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({
|
const connection = await createProviderConnection({
|
||||||
provider: "kiro",
|
provider: "kiro",
|
||||||
authType: "oauth",
|
authType: "oauth",
|
||||||
accessToken: tokenData.accessToken,
|
accessToken: tokenData.accessToken,
|
||||||
refreshToken: tokenData.refreshToken,
|
refreshToken: tokenData.refreshToken || refreshToken.trim(),
|
||||||
expiresAt: new Date(Date.now() + tokenData.expiresIn * 1000).toISOString(),
|
expiresAt: new Date(Date.now() + (tokenData.expiresIn || 3600) * 1000).toISOString(),
|
||||||
email: email || null,
|
email: email || null,
|
||||||
providerSpecificData: {
|
providerSpecificData: {
|
||||||
profileArn: tokenData.profileArn,
|
profileArn: resolvedProfileArn,
|
||||||
authMethod: "imported",
|
authMethod: resolvedAuthMethod,
|
||||||
provider: "Imported",
|
provider: providerLabel,
|
||||||
|
...(isIdc ? { clientId, clientSecret, region: region || "us-east-1" } : {}),
|
||||||
},
|
},
|
||||||
testStatus: "active",
|
testStatus: "active",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function KiroAuthModal({ isOpen, onMethodSelect, onClose }) {
|
|||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const [autoDetecting, setAutoDetecting] = useState(false);
|
const [autoDetecting, setAutoDetecting] = useState(false);
|
||||||
const [autoDetected, setAutoDetected] = useState(false);
|
const [autoDetected, setAutoDetected] = useState(false);
|
||||||
|
const [idcCredentials, setIdcCredentials] = useState(null);
|
||||||
|
|
||||||
// Auto-detect token when import method is selected
|
// Auto-detect token when import method is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -28,6 +29,7 @@ export default function KiroAuthModal({ isOpen, onMethodSelect, onClose }) {
|
|||||||
setAutoDetecting(true);
|
setAutoDetecting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setAutoDetected(false);
|
setAutoDetected(false);
|
||||||
|
setIdcCredentials(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/oauth/kiro/auto-import");
|
const res = await fetch("/api/oauth/kiro/auto-import");
|
||||||
@@ -36,6 +38,16 @@ export default function KiroAuthModal({ isOpen, onMethodSelect, onClose }) {
|
|||||||
if (data.found) {
|
if (data.found) {
|
||||||
setRefreshToken(data.refreshToken);
|
setRefreshToken(data.refreshToken);
|
||||||
setAutoDetected(true);
|
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 {
|
} else {
|
||||||
setError(data.error || "Could not auto-detect token");
|
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", {
|
const res = await fetch("/api/oauth/kiro/import", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ refreshToken: refreshToken.trim() }),
|
body: JSON.stringify({
|
||||||
|
refreshToken: refreshToken.trim(),
|
||||||
|
...(idcCredentials || {}),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user