Repository navigation
feat(kiro): support enterprise External IdP (Your organization) logins - #6363
diegosouzapw merged 8 commits into
Conversation
Kiro's enterprise "Your organization" sign-in federates through the org's own identity provider (e.g. Microsoft Entra ID) and produces an `external_idp` token that is fundamentally different from AWS Builder ID / IAM Identity Center (AWS SSO-OIDC, refresh token starts with `aorAAAAAG`) and the Google/GitHub social flow. Its `~/.aws/sso/cache/kiro-auth-token.json` carries an org-IdP JWT access token, an IdP refresh token, a per-tenant `tokenEndpoint`, a public `clientId` (no secret) and `scopes` (`codewhisperer:conversations …`). Before this change every import path rejected these tokens (the `aorAAAAAG` format gate + no client secret), and the runtime/quota calls would have failed even if imported, so organization accounts could not be used. This adds full external_idp support: - New `open-sse/services/kiroExternalIdp.ts`: public-client refresh_token grant builder (`buildExternalIdpRefreshParams`), a token-endpoint SSRF allowlist (`validateExternalIdpTokenEndpoint` — Microsoft/Okta/Auth0/OneLogin/Ping/ Google/Cognito, https only), scope normalization, JWT identity extraction (`preferred_username`/`upn`/`email`), and the `TokenType: EXTERNAL_IDP` header constants. - Runtime executor (`open-sse/executors/kiro.ts`): send `TokenType: EXTERNAL_IDP` for external_idp accounts. CodeWhisperer only binds the org-IdP bearer to the Amazon Q Developer profile with this header; without it every call returns `ValidationException: Invalid ARN <clientId>`. - Runtime + import token refresh (`open-sse/services/tokenRefresh.ts`, `src/lib/oauth/services/kiro.ts`): refresh external_idp tokens with a form-encoded public-client `refresh_token` grant against the org IdP's `tokenEndpoint` instead of AWS OIDC / the Kiro social endpoint. - Quota (`open-sse/services/usage/kiro.ts`): send the same header on `GetUsageLimits` so organization quota resolves. - Import routes: `POST /api/oauth/kiro/import` gains an external_idp branch (skips the `aorAAAAAG` gate, refreshes via the org IdP, stores clientId/tokenEndpoint/scope/region/profileArn); `GET /auto-import` now recognizes external_idp tokens in `~/.aws/sso/cache`, reads the profile ARN from the Kiro IDE `profile.json` (org tokens can't enumerate it via `ListAvailableProfiles`), and persists the connection. The profile.json reader is factored into a shared `readKiroIdeProfileArn()` helper. - Validation schema (`kiroImportSchema`): accept `tokenEndpoint` + `scopes`. Tests: new `tests/unit/kiro-external-idp.test.ts` (endpoint allowlist, scope normalization, identity extraction, public-client refresh body, the org IdP refresh path, and the `TokenType: EXTERNAL_IDP` header gating). Also hardens `kiro-windows-auto-import-3363.test.ts` to isolate `USERPROFILE` (Windows `os.homedir()` reads it, not `HOME`) so the probe never reads a real on-host Kiro login.
There was a problem hiding this comment.
Code Review
This pull request introduces support for Kiro / Amazon Q External IdP (enterprise "Your organization" SSO) accounts, including dedicated token refresh logic, token endpoint validation, and automatic profile ARN importing. The review feedback highlights several key improvement opportunities: validating the presence of access_token in refresh responses to prevent saving broken connections, safeguarding JSON parsing against non-object payloads in both JWT decoding and profile parsing, and optimizing performance by caching the profile ARN read operation outside of the cache-probing loop.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| 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 || 3600, | ||
| }; |
There was a problem hiding this comment.
If the External IdP refresh endpoint returns a successful response (HTTP 200) but is missing the access_token field, returning an undefined accessToken will result in a broken connection being saved. Validate that tokens.access_token is present and log an error/return null if it is missing.
const tokens = await response.json();
if (!tokens?.access_token) {
log?.error?.("TOKEN_REFRESH", "Kiro external_idp refresh response missing access_token");
return null;
}
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 || 3600,
};| const data = await response.json(); | ||
| return { | ||
| accessToken: data.access_token, | ||
| refreshToken: data.refresh_token || refreshToken, | ||
| expiresIn: data.expires_in || 3600, | ||
| }; |
There was a problem hiding this comment.
Ensure that the response from the External IdP refresh endpoint actually contains an access_token. If it is missing, throw an explicit error to prevent returning a broken token object to the caller.
const data = await response.json();
if (!data?.access_token) {
throw new Error("Response from External IdP is missing access_token");
}
return {
accessToken: data.access_token,
refreshToken: data.refresh_token || refreshToken,
expiresIn: data.expires_in || 3600,
};| export function decodeJwtPayload(jwt: unknown): Record<string, unknown> | null { | ||
| try { | ||
| if (typeof jwt !== "string") return null; | ||
| const parts = jwt.split("."); | ||
| if (parts.length !== 3) return null; | ||
| const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); | ||
| const padding = (4 - (base64.length % 4)) % 4; | ||
| const json = Buffer.from(`${base64}${"=".repeat(padding)}`, "base64").toString("utf8"); | ||
| return JSON.parse(json) as Record<string, unknown>; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
The decodeJwtPayload function parses the JWT payload using JSON.parse, which can return null, arrays, or other primitive types if the JSON is valid but not an object. To ensure type safety and prevent downstream runtime errors, verify that the parsed payload is a non-null object and not an array before returning it.
export function decodeJwtPayload(jwt: unknown): Record<string, unknown> | null {
try {
if (typeof jwt !== "string") return null;
const parts = jwt.split(".");
if (parts.length !== 3) return null;
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padding = (4 - (base64.length % 4)) % 4;
const json = Buffer.from(`${base64}${"=".repeat(padding)}`, "base64").toString("utf8");
const payload = JSON.parse(json);
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
return payload as Record<string, unknown>;
}
return null;
} catch {
return null;
}
}| const profileData = JSON.parse(profileContent); | ||
| if (profileData.arn) { | ||
| return profileData.arn.replace( | ||
| /arn:aws:codewhisperer:[^:]+:/, | ||
| "arn:aws:codewhisperer:us-east-1:" | ||
| ); | ||
| } |
There was a problem hiding this comment.
If profile.json contains a non-object JSON value (such as null or a primitive), JSON.parse will succeed but accessing profileData.arn will throw a runtime TypeError. Guard against this by checking that profileData is a non-null object.
| const profileData = JSON.parse(profileContent); | |
| if (profileData.arn) { | |
| return profileData.arn.replace( | |
| /arn:aws:codewhisperer:[^:]+:/, | |
| "arn:aws:codewhisperer:us-east-1:" | |
| ); | |
| } | |
| const profileData = JSON.parse(profileContent); | |
| if (profileData && typeof profileData === "object" && "arn" in profileData && typeof profileData.arn === "string") { | |
| return profileData.arn.replace( | |
| /arn:aws:codewhisperer:[^:]+:/, | |
| "arn:aws:codewhisperer:us-east-1:" | |
| ); | |
| } |
|
|
||
| if (isExternalIdp) { | ||
| const region: string | null = data.region || null; | ||
| const profileArn = await readKiroIdeProfileArn(); |
There was a problem hiding this comment.
Performance/Efficiency Optimization: readKiroIdeProfileArn is called inside the loop of tryAwsSsoCache for each file found in the cache directory. Since this function performs disk I/O on multiple paths, it is highly recommended to read the profile ARN once before the loop starts and reuse the cached value.
…e#6113 region-preservation fix Merges release/v3.8.47 into feat/kiro-external-idp (cheaper-inference#6363). The PR's own readKiroIdeProfileArn() helper reintroduced the forced us-east-1 ARN region normalization that cheaper-inference#6113 (already merged into release/v3.8.47) removed, because forcing us-east-1 403s IDC accounts outside that region. Fixed readKiroIdeProfileArn() to preserve the profile.json ARN region verbatim instead, and reused it from the IDC branch in tryAwsSsoCache() (deduplicating a second inline copy of the same logic), while keeping the new external_idp early-return branch untouched. Adds a CHANGELOG entry for the new External IdP support (the PR itself had not touched CHANGELOG.md). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…esync (CHANGELOG-eat guard) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…e sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…y release auto-resolve + rebaseline own tokenRefresh growth The release sync's merge auto-resolve silently reverted sibling PR cheaper-inference#6126's clinepass work (registry entry, catalog, oauth constants, clineAuth.ts, the clinepass token-refresh case, and its tests) — all outside this PR's Kiro external-IdP scope. Restored every affected file to the release version; the remaining diff is Kiro-IdP-only. Rebaselined tokenRefresh.ts 2182->2249 (+67, this PR's own external_idp refresh branch) with justification, and restored the cheaper-inference#6126 CHANGELOG bullet (re-inserting only this PR's own). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…external-idp # Conflicts: # config/quality/file-size-baseline.json
ece4bf7
into
cheaper-inference:release/v3.8.47
…al-IdP + function-length extractions Merges the release tip (cheaper-inference#6363 Kiro external-IdP) into the API-key branch: executors/kiro.ts and usage/kiro.ts now handle BOTH auth methods (api_key 'tokentype' + external_idp 'TokenType' headers) in one authHeaders path. getKiroUsage was over the function-length gate after the merge — extracted buildKiroUsageAttempts / runKiroUsageAttempts / resolveKiroUsageEndpoints / buildKiroAuthHeaders (complexity back to the baseline count). Restored the cheaper-inference#6363 CHANGELOG bullet, re-inserting only this PR's own. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
cheaper-inference#6363) * feat(kiro): support enterprise External IdP ("Your organization") logins Kiro's enterprise "Your organization" sign-in federates through the org's own identity provider (e.g. Microsoft Entra ID) and produces an `external_idp` token that is fundamentally different from AWS Builder ID / IAM Identity Center (AWS SSO-OIDC, refresh token starts with `aorAAAAAG`) and the Google/GitHub social flow. Its `~/.aws/sso/cache/kiro-auth-token.json` carries an org-IdP JWT access token, an IdP refresh token, a per-tenant `tokenEndpoint`, a public `clientId` (no secret) and `scopes` (`codewhisperer:conversations …`). Before this change every import path rejected these tokens (the `aorAAAAAG` format gate + no client secret), and the runtime/quota calls would have failed even if imported, so organization accounts could not be used. This adds full external_idp support: - New `open-sse/services/kiroExternalIdp.ts`: public-client refresh_token grant builder (`buildExternalIdpRefreshParams`), a token-endpoint SSRF allowlist (`validateExternalIdpTokenEndpoint` — Microsoft/Okta/Auth0/OneLogin/Ping/ Google/Cognito, https only), scope normalization, JWT identity extraction (`preferred_username`/`upn`/`email`), and the `TokenType: EXTERNAL_IDP` header constants. - Runtime executor (`open-sse/executors/kiro.ts`): send `TokenType: EXTERNAL_IDP` for external_idp accounts. CodeWhisperer only binds the org-IdP bearer to the Amazon Q Developer profile with this header; without it every call returns `ValidationException: Invalid ARN <clientId>`. - Runtime + import token refresh (`open-sse/services/tokenRefresh.ts`, `src/lib/oauth/services/kiro.ts`): refresh external_idp tokens with a form-encoded public-client `refresh_token` grant against the org IdP's `tokenEndpoint` instead of AWS OIDC / the Kiro social endpoint. - Quota (`open-sse/services/usage/kiro.ts`): send the same header on `GetUsageLimits` so organization quota resolves. - Import routes: `POST /api/oauth/kiro/import` gains an external_idp branch (skips the `aorAAAAAG` gate, refreshes via the org IdP, stores clientId/tokenEndpoint/scope/region/profileArn); `GET /auto-import` now recognizes external_idp tokens in `~/.aws/sso/cache`, reads the profile ARN from the Kiro IDE `profile.json` (org tokens can't enumerate it via `ListAvailableProfiles`), and persists the connection. The profile.json reader is factored into a shared `readKiroIdeProfileArn()` helper. - Validation schema (`kiroImportSchema`): accept `tokenEndpoint` + `scopes`. Tests: new `tests/unit/kiro-external-idp.test.ts` (endpoint allowlist, scope normalization, identity extraction, public-client refresh body, the org IdP refresh path, and the `TokenType: EXTERNAL_IDP` header gating). Also hardens `kiro-windows-auto-import-3363.test.ts` to isolate `USERPROFILE` (Windows `os.homedir()` reads it, not `HOME`) so the probe never reads a real on-host Kiro login. * fix(changelog): restore cheaper-inference#6363 bullet after release resync (CHANGELOG-eat guard) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(changelog): re-restore cheaper-inference#6363 bullet after release sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(merge): restore cheaper-inference#6126 clinepass files reverted by release auto-resolve + rebaseline own tokenRefresh growth The release sync's merge auto-resolve silently reverted sibling PR cheaper-inference#6126's clinepass work (registry entry, catalog, oauth constants, clineAuth.ts, the clinepass token-refresh case, and its tests) — all outside this PR's Kiro external-IdP scope. Restored every affected file to the release version; the remaining diff is Kiro-IdP-only. Rebaselined tokenRefresh.ts 2182->2249 (+67, this PR's own external_idp refresh branch) with justification, and restored the cheaper-inference#6126 CHANGELOG bullet (re-inserting only this PR's own). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: artickc <artickc@users.noreply.github.com>
cheaper-inference#6363) * feat(kiro): support enterprise External IdP ("Your organization") logins Kiro's enterprise "Your organization" sign-in federates through the org's own identity provider (e.g. Microsoft Entra ID) and produces an `external_idp` token that is fundamentally different from AWS Builder ID / IAM Identity Center (AWS SSO-OIDC, refresh token starts with `aorAAAAAG`) and the Google/GitHub social flow. Its `~/.aws/sso/cache/kiro-auth-token.json` carries an org-IdP JWT access token, an IdP refresh token, a per-tenant `tokenEndpoint`, a public `clientId` (no secret) and `scopes` (`codewhisperer:conversations …`). Before this change every import path rejected these tokens (the `aorAAAAAG` format gate + no client secret), and the runtime/quota calls would have failed even if imported, so organization accounts could not be used. This adds full external_idp support: - New `open-sse/services/kiroExternalIdp.ts`: public-client refresh_token grant builder (`buildExternalIdpRefreshParams`), a token-endpoint SSRF allowlist (`validateExternalIdpTokenEndpoint` — Microsoft/Okta/Auth0/OneLogin/Ping/ Google/Cognito, https only), scope normalization, JWT identity extraction (`preferred_username`/`upn`/`email`), and the `TokenType: EXTERNAL_IDP` header constants. - Runtime executor (`open-sse/executors/kiro.ts`): send `TokenType: EXTERNAL_IDP` for external_idp accounts. CodeWhisperer only binds the org-IdP bearer to the Amazon Q Developer profile with this header; without it every call returns `ValidationException: Invalid ARN <clientId>`. - Runtime + import token refresh (`open-sse/services/tokenRefresh.ts`, `src/lib/oauth/services/kiro.ts`): refresh external_idp tokens with a form-encoded public-client `refresh_token` grant against the org IdP's `tokenEndpoint` instead of AWS OIDC / the Kiro social endpoint. - Quota (`open-sse/services/usage/kiro.ts`): send the same header on `GetUsageLimits` so organization quota resolves. - Import routes: `POST /api/oauth/kiro/import` gains an external_idp branch (skips the `aorAAAAAG` gate, refreshes via the org IdP, stores clientId/tokenEndpoint/scope/region/profileArn); `GET /auto-import` now recognizes external_idp tokens in `~/.aws/sso/cache`, reads the profile ARN from the Kiro IDE `profile.json` (org tokens can't enumerate it via `ListAvailableProfiles`), and persists the connection. The profile.json reader is factored into a shared `readKiroIdeProfileArn()` helper. - Validation schema (`kiroImportSchema`): accept `tokenEndpoint` + `scopes`. Tests: new `tests/unit/kiro-external-idp.test.ts` (endpoint allowlist, scope normalization, identity extraction, public-client refresh body, the org IdP refresh path, and the `TokenType: EXTERNAL_IDP` header gating). Also hardens `kiro-windows-auto-import-3363.test.ts` to isolate `USERPROFILE` (Windows `os.homedir()` reads it, not `HOME`) so the probe never reads a real on-host Kiro login. * fix(changelog): restore cheaper-inference#6363 bullet after release resync (CHANGELOG-eat guard) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(changelog): re-restore cheaper-inference#6363 bullet after release sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(merge): restore cheaper-inference#6126 clinepass files reverted by release auto-resolve + rebaseline own tokenRefresh growth The release sync's merge auto-resolve silently reverted sibling PR cheaper-inference#6126's clinepass work (registry entry, catalog, oauth constants, clineAuth.ts, the clinepass token-refresh case, and its tests) — all outside this PR's Kiro external-IdP scope. Restored every affected file to the release version; the remaining diff is Kiro-IdP-only. Rebaselined tokenRefresh.ts 2182->2249 (+67, this PR's own external_idp refresh branch) with justification, and restored the cheaper-inference#6126 CHANGELOG bullet (re-inserting only this PR's own). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: artickc <artickc@users.noreply.github.com>
Summary
Adds support for Kiro enterprise "Your organization" (External IdP) logins — the enterprise SSO flow that federates through an org's own identity provider (most commonly Microsoft Entra ID). These accounts were previously impossible to use: every import path rejected them and the runtime/quota calls would fail even if imported.
An External IdP Kiro token (
~/.aws/sso/cache/kiro-auth-token.json) is fundamentally different from the three flows already supported:authMethodbuilder-id/idcimportedexternal_idpaorAAAAAG…(AWS SSO-OIDC)1.AXoA…)oidc.{region}.amazonaws.comprod.…auth.desktop.kiro.devtokenEndpointTokenType: EXTERNAL_IDPWhat was failing
POST /api/oauth/kiro/import→validateImportTokenthrowsInvalid token format. Token should start with aorAAAAAG….GET /api/oauth/kiro/auto-import→tryAwsSsoCacheonly matchesaorAAAAAGtokens, so the org token is never found.generateAssistantResponseandGetUsageLimitsreturnValidationException: Invalid ARN <clientId>because the org-IdP bearer isn't bound to the Amazon Q Developer profile.Changes
open-sse/services/kiroExternalIdp.ts—buildExternalIdpRefreshParams(public-clientrefresh_tokengrant:client_id+refresh_token+scope, no secret),validateExternalIdpTokenEndpoint(https + IdP host allowlist — Microsoft/Okta/Auth0/OneLogin/Ping/Google/Cognito — SSRF guard),normalizeScope,emailFromExternalIdpToken(preferred_username/upn/email), and theTokenType: EXTERNAL_IDPconstants.open-sse/executors/kiro.ts— sendTokenType: EXTERNAL_IDPfor external_idp accounts (gated onauthMethod; AWS SSO/social must not send it).open-sse/services/tokenRefresh.ts+src/lib/oauth/services/kiro.ts— refresh external_idp tokens against the org IdPtokenEndpoint.open-sse/services/usage/kiro.ts— sendTokenType: EXTERNAL_IDPonGetUsageLimitsso org quota resolves.src/app/api/oauth/kiro/import/route.ts— dedicated external_idp import branch.src/app/api/oauth/kiro/auto-import/route.ts— recognize external_idp tokens in~/.aws/sso/cache, read the profile ARN from the Kiro IDEprofile.json(org tokens can't enumerate it viaListAvailableProfiles, which returns an empty list), persist the connection. Factors the profile.json reader intoreadKiroIdeProfileArn().kiroImportSchema— accepttokenEndpoint+scopes.Verification
Validated end-to-end against a live Microsoft Entra org Kiro account:
POST login.microsoftonline.com/{tenant}/oauth2/v2.0/token(form-encoded public client) → new access + rotated refresh token. ✅generateAssistantResponsewithTokenType: EXTERNAL_IDP+ profileArn → HTTP 200, streamed assistant content + metering. ✅ (Without the header:ValidationException: Invalid ARN <clientId>.)GetUsageLimitswithTokenType: EXTERNAL_IDP→ fullusageBreakdownList(plan "KIRO POWER", credits used/limit). ✅Tests
tests/unit/kiro-external-idp.test.ts— endpoint allowlist, scope normalization, identity extraction, public-client refresh body, the org-IdP refresh path (asserts it does NOT hit AWS OIDC / Kiro social), andTokenType: EXTERNAL_IDPheader gating.tests/unit/kiro-windows-auto-import-3363.test.tsto isolateUSERPROFILE(Windowsos.homedir()readsUSERPROFILE, notHOME), so the probe never reads a real on-host Kiro login.Existing Kiro / token-refresh suites remain green; scoped typecheck is clean.