Files
OmniRoute/tests/unit/kiro-external-idp.test.ts
NOXX - Commiter ece4bf7b53 feat(kiro): support enterprise External IdP (Your organization) logins (#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 #6363 bullet after release resync (CHANGELOG-eat guard)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore #6363 bullet after release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(merge): restore #6126 clinepass files reverted by release auto-resolve + rebaseline own tokenRefresh growth

The release sync's merge auto-resolve silently reverted sibling PR #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 #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>
2026-07-09 21:39:09 -03:00

139 lines
6.4 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
// Coverage for enterprise / Microsoft Entra "Your organization" (external_idp) Kiro accounts:
// • public-client refresh_token grant against the org IdP tokenEndpoint (no client secret),
// • the runtime `TokenType: EXTERNAL_IDP` header the CodeWhisperer service requires to bind
// the bearer to the Amazon Q Developer profile (without it every call is
// `ValidationException: Invalid ARN <clientId>`),
// • tokenEndpoint SSRF allowlist, scope normalization, and JWT identity extraction.
import {
buildExternalIdpRefreshParams,
validateExternalIdpTokenEndpoint,
normalizeScope,
isExternalIdpAuthMethod,
emailFromExternalIdpToken,
KIRO_EXTERNAL_IDP_TOKEN_TYPE_HEADER,
KIRO_EXTERNAL_IDP_TOKEN_TYPE_VALUE,
} from "../../open-sse/services/kiroExternalIdp.ts";
import { KiroExecutor } from "../../open-sse/executors/kiro.ts";
const MS_ENDPOINT = "https://login.microsoftonline.com/9d769d6d-e03a-442a-8ab1-a7da2037a5d4/oauth2/v2.0/token";
function makeJwt(payload: Record<string, unknown>): string {
const b64 = (o: unknown) => Buffer.from(JSON.stringify(o)).toString("base64url");
return `${b64({ alg: "none", typ: "JWT" })}.${b64(payload)}.sig`;
}
test("validateExternalIdpTokenEndpoint accepts Microsoft/Okta https, rejects others", () => {
assert.equal(validateExternalIdpTokenEndpoint(MS_ENDPOINT), MS_ENDPOINT);
assert.ok(validateExternalIdpTokenEndpoint("https://dev-123.okta.com/oauth2/v1/token"));
assert.throws(() => validateExternalIdpTokenEndpoint("http://login.microsoftonline.com/x/token"));
assert.throws(() => validateExternalIdpTokenEndpoint("https://evil.example.com/token"));
assert.throws(() => validateExternalIdpTokenEndpoint(""));
});
test("normalizeScope handles array and space-delimited string", () => {
assert.equal(normalizeScope(["a", "b", "offline_access"]), "a b offline_access");
assert.equal(normalizeScope("a b offline_access"), "a b offline_access");
assert.equal(normalizeScope([" x ", "", "y"]), "x y");
assert.equal(normalizeScope(undefined), "");
});
test("isExternalIdpAuthMethod recognizes external_idp (case-insensitive)", () => {
assert.equal(isExternalIdpAuthMethod("external_idp"), true);
assert.equal(isExternalIdpAuthMethod("EXTERNAL_IDP"), true);
assert.equal(isExternalIdpAuthMethod("idc"), false);
assert.equal(isExternalIdpAuthMethod(undefined), false);
});
test("emailFromExternalIdpToken reads preferred_username / upn / email", () => {
assert.equal(
emailFromExternalIdpToken(makeJwt({ preferred_username: "finbar.heslin@mrdevvn.cyou" })),
"finbar.heslin@mrdevvn.cyou"
);
assert.equal(emailFromExternalIdpToken(makeJwt({ upn: "a@b.com" })), "a@b.com");
assert.equal(emailFromExternalIdpToken(makeJwt({ email: "c@d.com" })), "c@d.com");
assert.equal(emailFromExternalIdpToken("not-a-jwt"), null);
});
test("buildExternalIdpRefreshParams builds a public-client form body", () => {
const req = buildExternalIdpRefreshParams("RT-123", {
clientId: "app-guid",
tokenEndpoint: MS_ENDPOINT,
scopes: ["api://app-guid/codewhisperer:conversations", "offline_access"],
});
assert.equal(req.tokenEndpoint, MS_ENDPOINT);
assert.equal(req.body.get("grant_type"), "refresh_token");
assert.equal(req.body.get("client_id"), "app-guid");
assert.equal(req.body.get("refresh_token"), "RT-123");
assert.equal(
req.body.get("scope"),
"api://app-guid/codewhisperer:conversations offline_access"
);
// Public client: never a secret.
assert.equal(req.body.get("client_secret"), null);
});
test("buildExternalIdpRefreshParams fails closed on missing fields", () => {
assert.throws(() => buildExternalIdpRefreshParams("", { clientId: "x", tokenEndpoint: MS_ENDPOINT, scopes: "s" }));
assert.throws(() => buildExternalIdpRefreshParams("rt", { tokenEndpoint: MS_ENDPOINT, scopes: "s" }));
assert.throws(() => buildExternalIdpRefreshParams("rt", { clientId: "x", scopes: "s" }));
assert.throws(() => buildExternalIdpRefreshParams("rt", { clientId: "x", tokenEndpoint: MS_ENDPOINT }));
});
test("KiroService.refreshToken uses the org IdP tokenEndpoint for external_idp", async () => {
const { KiroService } = await import("../../src/lib/oauth/services/kiro.ts");
const ORIGINAL_FETCH = globalThis.fetch;
const calls: { url: string; body: string; contentType: string | null }[] = [];
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
const u = String(url);
const body = init?.body instanceof URLSearchParams ? init.body.toString() : String(init?.body ?? "");
calls.push({ url: u, body, contentType: (init?.headers as Record<string, string>)?.["Content-Type"] ?? null });
return new Response(
JSON.stringify({ access_token: "new-at", refresh_token: "rotated-rt", expires_in: 4481 }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}) as typeof fetch;
try {
const svc = new KiroService();
const res = await svc.refreshToken("RT-old", {
authMethod: "external_idp",
clientId: "app-guid",
tokenEndpoint: MS_ENDPOINT,
scopes: "codewhisperer:conversations offline_access",
});
assert.equal(res.accessToken, "new-at");
assert.equal(res.refreshToken, "rotated-rt");
assert.equal(calls.length, 1);
assert.equal(calls[0].url, MS_ENDPOINT);
assert.ok(calls[0].url.includes("login.microsoftonline.com"));
// Must NOT hit AWS OIDC or the Kiro social endpoint.
assert.ok(!calls[0].url.includes("amazonaws.com"));
assert.ok(!calls[0].url.includes("desktop.kiro.dev"));
assert.ok(calls[0].body.includes("grant_type=refresh_token"));
assert.ok(calls[0].body.includes("client_id=app-guid"));
} finally {
globalThis.fetch = ORIGINAL_FETCH;
}
});
test("KiroExecutor.buildHeaders sends TokenType: EXTERNAL_IDP only for external_idp", () => {
const exec = new KiroExecutor();
const idpHeaders = exec.buildHeaders({
accessToken: "at",
providerSpecificData: { authMethod: "external_idp" },
} as never);
assert.equal(idpHeaders[KIRO_EXTERNAL_IDP_TOKEN_TYPE_HEADER], KIRO_EXTERNAL_IDP_TOKEN_TYPE_VALUE);
const idcHeaders = exec.buildHeaders({
accessToken: "at",
providerSpecificData: { authMethod: "idc" },
} as never);
assert.equal(idcHeaders[KIRO_EXTERNAL_IDP_TOKEN_TYPE_HEADER], undefined);
const builderIdHeaders = exec.buildHeaders({ accessToken: "at" } as never);
assert.equal(builderIdHeaders[KIRO_EXTERNAL_IDP_TOKEN_TYPE_HEADER], undefined);
});