diff --git a/docs/guides/KIRO_SETUP.md b/docs/guides/KIRO_SETUP.md new file mode 100644 index 0000000000..63f4f97eeb --- /dev/null +++ b/docs/guides/KIRO_SETUP.md @@ -0,0 +1,136 @@ +# Kiro Setup Guide + +This guide covers adding Kiro (AWS-hosted AI coding assistant) accounts to OmniRoute, +with a focus on running multiple accounts simultaneously without session conflicts. + +--- + +## Background: Why Kiro Accounts Can Conflict + +Kiro's backend uses AWS SSO OIDC client registrations to track active sessions. +The critical constraint: **each OIDC client registration supports only one active +session at a time**. When a second device or user authenticates using the same +registered client, the backend invalidates the first account's refresh token. + +This is the same mechanism that causes problems when running `kiro-cli login` on a +machine where another Kiro account is already signed in — the new login revokes the +first account's token. + +--- + +## How OmniRoute Solves This (v3.8.0+) + +Starting with v3.8.0, OmniRoute calls `registerClient()` (AWS SSO OIDC) during every +Kiro connection import. This gives each OmniRoute connection its own dedicated OIDC +client registration. Because each client registration is independent, refreshing or +re-authenticating one account does not affect any other account's refresh token. + +The isolation applies to all three import methods: + +| Import method | Isolation status | +|---|---| +| AWS Builder ID / IDC device-code flow | Isolated since the device-code flow was introduced | +| **Import Token** (manual refresh token paste) | Isolated from v3.8.0 | +| **Google / GitHub social login** | Isolated from v3.8.0 | +| **Auto-Import** (kiro-cli SQLite) | Isolated from v3.8.0 (SQLite path was already isolated; SSO-cache fallback is now also isolated) | + +--- + +## Migration Note for Connections Created Before v3.8.0 + +Connections imported before v3.8.0 do not have a dedicated OIDC client registration +stored in `providerSpecificData`. These connections continue to work but use the shared +social-auth refresh endpoint, which means two such connections can still invalidate each +other. + +**To gain isolation:** delete the old connection from **Dashboard → Providers** and +re-import it using any of the supported import flows. All newly created connections will +receive their own client registration automatically. + +--- + +## Adding Two Kiro Accounts Side by Side + +### Prerequisites + +- OmniRoute v3.8.0 or later. +- A working Kiro account (email + password, Google, or GitHub login). +- Optionally a second Kiro account. + +### Step 1: Import the first account + +1. Open **Dashboard → Providers → Add Provider → Kiro**. +2. Choose one of: + - **Import Token** — paste a refresh token starting with `aorAAAAAG`. + - **Google / GitHub login** — complete the OAuth flow in the browser. + - **Auto-Import** — click the button; OmniRoute reads credentials from the + local kiro-cli database or `~/.aws/sso/cache`. +3. The connection is saved. OmniRoute automatically registers a dedicated OIDC client for it. + +### Step 2: Import the second account + +Repeat step 1 for the second account. Because each import creates a separate OIDC +client registration, the two connections are fully isolated. + +### Step 3: Verify both connections are active + +1. **Dashboard → Providers** — both Kiro connections should show **Active** status. +2. **Dashboard → Health** — both connections should pass their token health check. + +### Step 4: Use a combo to route between accounts + +Create a combo with both connections as targets to load-balance or fall back between them: + +``` +kiro/kiro-dev → kiro/kiro-pro +``` + +See [FEATURES.md](./FEATURES.md) and the routing documentation for combo configuration. + +--- + +## Enterprise / IDC Users + +For AWS IAM Identity Center (IDC) accounts, use the **AWS Builder ID / IDC device-code** +flow from **Dashboard → Providers → Kiro → Device Code**. The device-code flow has +always been fully isolated. No re-import is needed for these connections. + +Enterprise users who operate in a non-default AWS region can specify the region when +importing via the Import Token API: + +```bash +curl -X POST http://localhost:20128/api/oauth/kiro/import \ + -H "Content-Type: application/json" \ + -d '{"refreshToken": "aorAAAAAG...", "region": "eu-west-1"}' +``` + +The `region` field defaults to `us-east-1` when omitted. + +--- + +## OIDC Client Expiry + +AWS SSO OIDC public clients typically expire after 90 days +(`clientSecretExpiresAt`). OmniRoute stores this timestamp in `providerSpecificData` +for observability. If a connection stops refreshing after ~90 days, re-import the +connection to obtain a fresh OIDC client registration. Automatic re-registration on +expiry is tracked as a future improvement. + +--- + +## Troubleshooting + +### Second account keeps getting logged out + +- Check both connections in **Dashboard → Providers** and confirm each shows a non-null + `clientId` in its raw JSON (visible via the info icon). If either connection is missing + `clientId`, it was imported before v3.8.0 — re-import it. + +### Import fails with "Token validation failed" + +- Ensure the refresh token starts with `aorAAAAAG`. +- Ensure OmniRoute can reach `https://oidc.us-east-1.amazonaws.com` (or the configured + region). If you are behind a corporate proxy, set a provider-level proxy in + **Dashboard → Settings → Proxies**. + +For other issues, see the main [TROUBLESHOOTING.md](./TROUBLESHOOTING.md). diff --git a/docs/guides/TROUBLESHOOTING.md b/docs/guides/TROUBLESHOOTING.md index 70c95c0081..f8b3f53398 100644 --- a/docs/guides/TROUBLESHOOTING.md +++ b/docs/guides/TROUBLESHOOTING.md @@ -134,6 +134,26 @@ OmniRoute auto-refreshes tokens. If issues persist: 1. Dashboard → Provider → Reconnect 2. Delete and re-add the provider connection +### Kiro multi-account: second account invalidates the first + +**Cause:** Kiro's backend enforces a single active session per OIDC client registration. +When two accounts share the same registered client (connections imported before v3.8.0), +refreshing one account's token invalidates the other's refresh token. + +**Fix (v3.8.0+):** Re-import affected connections. +Starting with v3.8.0, every new Kiro connection created via **Import Token**, +**Google/GitHub social login**, or **Auto-Import** automatically registers its own +dedicated OIDC client. The connection is therefore fully isolated and refreshing one +account has no effect on any other account. + +Connections that were imported _before_ v3.8.0 do not carry a per-connection client +registration. Those connections continue to use the shared social-auth refresh endpoint. +To gain isolation, delete the old connection from Dashboard → Providers and re-add it +via any of the three import flows. + +For full details and step-by-step instructions for adding two Kiro accounts side by side, +see [`docs/guides/KIRO_SETUP.md`](./KIRO_SETUP.md). + --- ## Cloud Issues diff --git a/tests/unit/token-refresh-service.test.ts b/tests/unit/token-refresh-service.test.ts index 339e912056..4727684bdb 100644 --- a/tests/unit/token-refresh-service.test.ts +++ b/tests/unit/token-refresh-service.test.ts @@ -535,6 +535,60 @@ test("refreshKiroToken falls back to the social-auth refresh endpoint", async () }); }); +// Issue #2328 — once a social-auth token has clientId/clientSecret stored +// (because it was imported after v3.8.0), refreshKiroToken must use the AWS OIDC +// endpoint, not the shared social-auth endpoint, even though authMethod is "google". +test("refreshKiroToken uses AWS OIDC path for social-auth token when clientId is present (#2328)", async () => { + const log = createLog(); + const calls: any[] = []; + + await withMockedFetch( + async (url, options = {}) => { + calls.push({ url, options }); + return jsonResponse({ + accessToken: "kiro-isolated-access", + refreshToken: "kiro-isolated-refresh-next", + expiresIn: 900, + }); + }, + async () => { + const result = await refreshKiroToken( + "kiro-social-refresh", + { + authMethod: "google", + clientId: "isolated-client-id", + clientSecret: "isolated-client-secret", + region: "us-east-1", + }, + log + ); + + assert.deepEqual(result, { + accessToken: "kiro-isolated-access", + refreshToken: "kiro-isolated-refresh-next", + expiresIn: 900, + }); + } + ); + + // Must call the AWS OIDC endpoint — not the shared social-auth tokenUrl + assert.ok( + calls[0].url.includes("oidc.us-east-1.amazonaws.com/token"), + `expected AWS OIDC endpoint but got ${calls[0].url}` + ); + assert.notEqual( + calls[0].url, + PROVIDERS.kiro.tokenUrl, + "should not call the shared social-auth endpoint when clientId is set" + ); + assert.deepEqual(JSON.parse(calls[0].options.body), { + clientId: "isolated-client-id", + clientSecret: "isolated-client-secret", + refreshToken: "kiro-social-refresh", + grantType: "refresh_token", + }); +}); + test("refreshQoderToken uses basic auth once qoder oauth settings are configured", async () => { const log = createLog(); const calls: any[] = [];