feat(kiro): add external_idp CLIProxyAPI import for Microsoft SSO

Import Kiro accounts authenticated via Microsoft Entra/365 SSO using
CLIProxyAPI JSON. Adds external_idp refresh path (form-encoded OAuth2,
Microsoft login host allowlist), TokenType: EXTERNAL_IDP header for
runtime and usage/quota requests, dashboard import UI, and unit tests.
Scoped to authMethod === "external_idp"; existing Kiro auth unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Stevanus Pangau
2026-06-26 11:42:05 +07:00
committed by decolua
co-authored by Cursor
parent 49a3ec7a72
commit a4f44e3e12
7 changed files with 554 additions and 5 deletions
+15 -5
View File
@@ -26,7 +26,11 @@ export class KiroExecutor extends BaseExecutor {
// exactly like an OAuth access token, but with an extra `tokentype: API_KEY`
// header so CodeWhisperer treats it as a long-lived API key rather than an
// OIDC/social access token. Mirrors the Kiro IDE headless-auth behavior.
const isApiKey = credentials?.providerSpecificData?.authMethod === "api_key";
// Enterprise / Microsoft Entra (external_idp) tokens are OAuth access tokens,
// but CodeWhisperer requires TokenType=EXTERNAL_IDP to bind them to profiles.
const authMethod = credentials?.providerSpecificData?.authMethod;
const isApiKey = authMethod === "api_key";
const isExternalIdp = authMethod === "external_idp";
const apiKey = credentials?.apiKey || (isApiKey ? credentials?.accessToken : null);
if (isApiKey && apiKey) {
@@ -34,6 +38,9 @@ export class KiroExecutor extends BaseExecutor {
headers["tokentype"] = "API_KEY";
} else if (credentials.accessToken) {
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
if (isExternalIdp) {
headers["TokenType"] = "EXTERNAL_IDP";
}
}
return headers;
@@ -49,13 +56,16 @@ export class KiroExecutor extends BaseExecutor {
* BaseExecutor.execute() returns immediately (only 429 / network errors fall
* through to the next host). So for api-key auth we must try the *.amazonaws.com
* CodeWhisperer hosts FIRST, mirroring the Kiro-Go reference fork which never
* routes api-key traffic through kiro.dev. OAuth keeps the default order
* (kiro.dev first) since its token is what that gateway accepts.
* routes api-key traffic through kiro.dev. External IdP enterprise tokens also
* use the CodeWhisperer surface, with the `TokenType: EXTERNAL_IDP` header.
* Other OAuth methods keep the default order (kiro.dev first) since their
* tokens are what that gateway accepts.
*/
getOrderedBaseUrls(credentials) {
const baseUrls = this.getBaseUrls();
const isApiKey = credentials?.providerSpecificData?.authMethod === "api_key";
if (!isApiKey) return baseUrls;
const authMethod = credentials?.providerSpecificData?.authMethod;
const isCodeWhispererSurface = authMethod === "api_key" || authMethod === "external_idp";
if (!isCodeWhispererSurface) return baseUrls;
const amazon = baseUrls.filter((u) => u.includes("amazonaws.com"));
const others = baseUrls.filter((u) => !u.includes("amazonaws.com"));
return amazon.length > 0 ? [...amazon, ...others] : baseUrls;
@@ -2,6 +2,7 @@ import { PROVIDERS, PROVIDER_OAUTH } from "../../config/providers.js";
import { OAUTH_ENDPOINTS, GITHUB_COPILOT } from "../../config/appConstants.js";
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { dedupRefresh } from "./dedup.js";
import { buildExternalIdpRefreshParams } from "../../../src/lib/oauth/kiroExternalIdp.js";
let _xaiServiceSingleton = null;
export async function refreshXaiToken(refreshToken, log) {
@@ -309,6 +310,49 @@ export async function refreshKiroToken(refreshToken, providerSpecificData, log,
const clientSecret = providerSpecificData?.clientSecret;
const region = providerSpecificData?.region;
if (authMethod === "external_idp") {
let refreshRequest;
try {
refreshRequest = buildExternalIdpRefreshParams(refreshToken, providerSpecificData);
} catch (error) {
log?.warn?.("TOKEN_REFRESH", `Invalid Kiro external_idp refresh config: ${error.message}`);
return null;
}
const response = await proxyAwareFetch(refreshRequest.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: refreshRequest.body,
}, proxyOptions);
if (!response.ok) {
const errorText = await response.text();
log?.error?.("TOKEN_REFRESH", "Failed to refresh Kiro external_idp token", {
status: response.status,
error: errorText,
});
return null;
}
const tokens = await response.json();
log?.info?.("TOKEN_REFRESH", "Successfully refreshed Kiro external_idp token", {
hasNewAccessToken: !!tokens.access_token,
hasNewRefreshToken: !!tokens.refresh_token,
expiresIn: tokens.expires_in,
});
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
providerSpecificData: refreshRequest.providerSpecificData,
};
}
if (clientId && clientSecret) {
const isIDC = authMethod === "idc";
const endpoint = isIDC && region
+5
View File
@@ -55,7 +55,9 @@ export async function getKiroUsage(accessToken, providerSpecificData, proxyOptio
// CodeWhisperer treats it as a long-lived API key rather than an OIDC token.
// Without this header the GetUsageLimits call is rejected (401/403).
const isApiKey = authMethod === "api_key";
const isExternalIdp = authMethod === "external_idp";
const apiKeyHeaders = isApiKey ? { tokentype: "API_KEY" } : {};
const externalIdpHeaders = isExternalIdp ? { TokenType: "EXTERNAL_IDP" } : {};
// For api-key auth, never inject the shared default placeholder profileArn —
// CodeWhisperer 403s a request whose profileArn isn't owned by the key's
@@ -84,6 +86,7 @@ export async function getKiroUsage(accessToken, providerSpecificData, proxyOptio
"x-amz-user-agent": "aws-sdk-js/1.0.0 KiroIDE",
"user-agent": "aws-sdk-js/1.0.0 KiroIDE",
...apiKeyHeaders,
...externalIdpHeaders,
},
},
proxyOptions
@@ -99,6 +102,7 @@ export async function getKiroUsage(accessToken, providerSpecificData, proxyOptio
"x-amz-target": "AmazonCodeWhispererService.GetUsageLimits",
"Accept": "application/json",
...apiKeyHeaders,
...externalIdpHeaders,
},
body: JSON.stringify({
origin: "AI_EDITOR",
@@ -121,6 +125,7 @@ export async function getKiroUsage(accessToken, providerSpecificData, proxyOptio
"Authorization": `Bearer ${accessToken}`,
"Accept": "application/json",
...apiKeyHeaders,
...externalIdpHeaders,
},
}, proxyOptions);
},