fix(auth): support importing external_idp credentials and Kiro IDE cache for Azure tenant SSO - #1
Conversation
apiImportCredentials previously dropped the external_idp refresh material
(token_endpoint/issuer_url/scopes/profile_arn) and read camelCase only, so a
CLIProxyAPI_*.json minted by kiro-login-helper.py could never be imported by
API — refreshExternalIdpToken hard-fails without token_endpoint+client_id.
- Add proxy/cli_json_import.go: single normalizer accepting snake_case (helper
native) and camelCase, auth-method normalization + inference, provider
defaulting (external_idp -> AzureAD), us-east-1 default.
- Refactor apiImportCredentials to share an importOne core; carry external_idp
fields into the mandatory refresh and the persisted account, mirroring the
account apiPollKiroSso writes for an interactive login. profileArn falls back
to the helper-provided ARN (external_idp refresh returns none by design),
else resolved lazily on first use.
- Add POST /admin/api/auth/import-cli-json: ingest one/many helper docs (object,
array, {files|accounts} wrapper, or concatenated text) with per-item results.
- Email stored as label only; password never persisted or sent upstream (D5).
- Tests (compiled, not run): normalizer table tests, external_idp happy path via
httptest IdP token server, missing-token_endpoint 400, cli-json batch.
- README import section + env vars; CHANGELOG; plan doc.
D3: data/imports/ watcher (KIRO_IMPORT_WATCH, opt-in; on in docker-compose) auto-ingests CLIProxyAPI_*.json via importOne -> config.AddAccount, moving handled files to processed/ or failed/ with an .error.txt sidecar; dedupes by refresh token / email+authMethod. D4: Enterprise SSO modal file picker + paste box posting to /auth/import-cli-json; EN/ZH locale strings. docker-compose: KIRO_IMPORT_WATCH=1 + ./data/imports mount.
… env Precedence: CLI flag > env > config.json. docker-compose honors KIRO_PORT for the published host port. Lets the proxy run on a port other than 8080 without editing config.json.
The refreshLocks comment claimed the per-account lock 'replaces' the handler-level tokenRefreshMu, but tokenRefreshMu is still active at handler.go:2148 (request-path refresh) and is part of the documented global lock order at account.go:250 (tokenRefreshMu -> p.mu -> refreshLockFor -> cfgLock). Reworded to 'reduces the global contention tokenRefreshMu imposed' + explicit note that tokenRefreshMu still guards the request-path refresh and is intentionally kept (removal = risky concurrency change violating the documented lock order, out of bug-fix scope). Closes Task 1 minor #1. Comment-only.
|
Reviewed — the feature and its security posture look good (import endpoints are admin-gated, the auto-import watcher is opt-in via Cause: Fix is test-only (impl is correct): backdate the fixture mtime after writing, e.g. old := time.Now().Add(-3 * time.Second)
os.Chtimes(path, old, old)in the test helper, so the file clears the min-age gate deterministically. (The 2 translator failures on this branch — TestClaudeToolResultMixedTextAndImage, TestOpenAIToolResultImageCarriedWhenFollowedByUser — are pre-existing on main and unrelated to this PR.) Once the watcher tests are green I'll resolve the conflicts (handler.go, web/app.js, en/zh locales, import_credentials_test.go) and merge into feat/azure-tenant-sso. |
Add DoS guard, per-key limits, Force Model, proxy pool failover + EN/…
|
@0xharryriddle following up on my earlier review — this is still blocked, and I don't see any new commits since 1. The 3 watcher tests still failRe-confirmed just now on the current head: The implementation is correct — this is a test-only issue. Fix is to backdate the fixture mtime after writing it, so it clears the min-age gate deterministically: old := time.Now().Add(-3 * time.Second)
if err := os.Chtimes(path, old, old); err != nil {
t.Fatal(err)
}2. Please retarget the PR base to
|
The three watcher tests wrote a fixture and called scanImportDir in the same instant, so every file was ~0s old and always skipped by the importMinFileAgeSeconds (2s) "still being written" guard in proxy/import_watcher.go:104 — 0 accounts imported and nothing moved to processed/ or failed/. Fix is test-only; the implementation guard is correct. Add backdateForWatcher() which os.Chtimes the fixture to now-(importMinFileAgeSeconds+1)s so it clears the gate deterministically, and call it from writeHelperFile and the malformed -file fixture. Fixes TestImportWatcherProcessesValidFile, TestImportWatcherMovesInvalidFileToFailed, TestImportWatcherSkipsDuplicate.
Retargets this PR onto main and resolves the conflicts in proxy/handler.go, proxy/import_credentials_test.go, web/app.js and the en/zh locales. main landed its own external_idp import hardening while this branch was open, so the two sides overlapped in apiImportCredentials. Resolved as a union rather than picking a side: main's security posture is kept and moved INTO this branch's shared importOne funnel, so every import path (the legacy /auth/credentials endpoint, /auth/import-cli-json, /auth/import-ide-cache and the data/imports watcher) now inherits it instead of only the one endpoint: - external_idp tokenEndpoint/issuerUrl are validated against the IdP host allow-list before any refresh POST, so an untrusted credential JSON can no longer point the refresh at an internal or attacker-controlled host. - Azure endpoints/scopes are derived from userId or the accessToken JWT issuer, so Kiro Account Manager exports and bare blobs import without tokenEndpoint. - Trust-on-import: an external_idp credential carrying an Azure AD JWT with a real exp is persisted verbatim, so re-importing the same JSON does not consume (rotate) the refresh token. Everything else keeps refresh-at-import. - A pasted record's account id is reused when free, else regenerated, so re-importing a backup updates instead of duplicating. - 1 MiB body cap on /auth/credentials retained from main. importCredentialRequest gains ID/UserID (decoded from id/userId/user_id) to carry the two new inputs through the normalizer. Tests: both sides' suites are kept. main's normalizeImportAuthMethod table, allow-list rejection, derivation and trust-on-import tests all pass, as do this branch's missing-token_endpoint and cli-json batch tests. The three external_idp tests that POST at an httptest server now install the no-op validator hook, since importOne enforces the allow-list on every path and http+127.0.0.1 is rejected by design. go build ./... and go vet ./... clean; 246 tests pass. The 2 remaining failures (TestClaudeToolResultMixedTextAndImage, TestOpenAIToolResultImageCarriedWhenFollowedByUser) reproduce identically on a clean origin/main worktree and are unrelated to this PR.
Retargets this branch onto the PR's real base (zsecducna/Kiro-Go main),
which had advanced 102 commits past the stale fork main.
Conflict resolutions (union, no feature dropped):
proxy/handler.go
- RequestLog: take upstream's struct (adds ApiKeyID).
- apiImportCredentials: keep the body-read + decodeImportRequest front
end (so snake_case helper JSON still imports), then upstream's
api_key short-circuit branch, then importOne for the OAuth path.
- Fold main's external_idp hardening INTO importOne rather than leaving
it inline in the handler, so all four import paths (API, cli-json,
IDE cache, directory watcher) inherit the IdP endpoint allow-list,
userId/JWT tenant derivation, trust-on-import, and id preservation.
Previously only the one handler had them.
proxy/cli_json_import.go
- importCredentialRequest gains ID, UserID and KiroApiKey (+ raw
decoding for both casings) to feed the merged logic.
- normalizeAuthMethod learns api_key.
- Region default no longer applied to api_key: those accounts are never
refreshed, so the key is probed for its real region; a us-east-1
placeholder would be validated as the answer and restore an EU key as
a permanently-403ing pool slot.
- firstNonEmpty -> firstNonBlank: upstream added its own firstNonEmpty
in bedrock_eventstream.go with different (non-trimming) semantics.
web/app.js, web/locales/{en,zh}.json
- Both sides were pure additions; kept both (528 keys each, JSON valid).
Full suite: 416 passed. The 2 remaining failures are a pre-existing
t.TempDir cleanup race ("directory not empty") that also reproduces on
pristine upstream/main, landing on a different random test each run.
|
@zsecducna Thanks for the precise diagnosis on both points — both are done and pushed. 1. Watcher tests: fixed (test-only, as you called it)Your read was exactly right: the fixture was ~0s old and always failed the Fixed via a old := time.Now().Add(-(importMinFileAgeSeconds + 1) * time.Second)so the tests won't silently start skipping again if that gate is ever retuned. All three pass: 2. Retargeted to
|
Summary
This PR builds on top of
feat/azure-tenant-ssofrom PR Quorinex#131 and fixes several follow-up issues around importing Microsoft 365 / Entra IDexternal_idpcredentials into Kiro-Go.It adds support for:
CLIProxyAPI_*.jsonfiles generated by helper toolingdata/imports/data/config.jsonThis PR is intended as a support/fix layer for Quorinex#131, not a replacement for it.
Why
PR Quorinex#131 adds the main Microsoft 365 / Entra ID hosted Enterprise SSO flow.
However, real-world usage exposed additional issues:
external_idphelper JSON could not be imported reliably because fields liketoken_endpoint,issuer_url,scopes, andprofile_arnwere not preserved through the import path.config.jsonmanually could be overwritten by the running app's in-memory config.8080while the app listened on a stale configured port like8081, makinglocalhost:8080unreachable.Import from Kiro IDE (this host)path failed inside Docker because the container runs as root and looked for the cache at/root/.aws/sso/cache/kiro-auth-token.json.$HOME-based directory mount.Changes
1. Normalize helper JSON imports
Adds
proxy/cli_json_import.go.The import path now accepts both helper-native snake_case and app-native camelCase fields, including:
For
external_idpimports, the normalizer preserves refresh-critical fields:This fixes the issue where imported Microsoft 365 credentials failed refresh because the IdP token endpoint/client ID were dropped.
2. Shared import core
Refactors credential importing around a shared
importOnepath.This ensures all import mechanisms behave consistently:
/auth/import-credentials/auth/import-cli-jsonexternal_idpcredentials now go through the same mandatory refresh/persistence path as interactive SSO credentials.3. New helper JSON import endpoint
Adds:
Supports:
{ "files": [...] }{ "accounts": [...] }This makes
CLIProxyAPI_*.jsonfiles directly importable.4. Admin UI support
Adds UI support in the Enterprise SSO modal for:
CLIProxyAPI_*.jsonfilesNew UI action:
Locale strings added for English and Chinese.
5. Zero-touch Docker import watcher
Adds an optional watcher controlled by:
Docker Compose enables it by default.
The watcher scans:
and imports matching credential files automatically.
Processed files are moved to:
Failed imports get a
.error.txtsidecar.This avoids manual
config.jsonedits and prevents races with the running app's in-memory config.6. Kiro IDE cache import
Adds:
This reads the local Kiro IDE AWS SSO cache:
The cache is camelCase and maps directly onto the existing credential decoder.
The stale
expiresAtin the IDE cache is intentionally ignored; import performs a mandatory refresh and persists the fresh upstream expiry.7. Portable Docker support for IDE cache import
Docker cannot use the host user's
~automatically because the app runs as root inside the container.This PR mounts the host AWS SSO cache directory into the container:
${KIRO_AWS_SSO_CACHE_DIR:-${HOME}/.aws/sso/cache}:/host-aws-sso-cache:roand sets:
KIRO_IDE_CACHE=/host-aws-sso-cache/kiro-auth-token.jsonThis works across:
/home/<user>/.aws/sso/cache/Users/<user>/.aws/sso/cacheIf needed, users can override:
8. Docker port mismatch fix
Docker Compose now forces the in-container app listen port to match the published mapping:
This prevents stale
data/config.jsonvalues like8081from making:unreachable.
The host port is still customizable:
9. Configurable binary port/host
Adds runtime overrides:
Precedence:
Security Notes
Docker Usage
Default:
Open:
Different host port:
Open:
Import helper JSON automatically:
mkdir -p data/imports cp CLIProxyAPI_*.json data/imports/Import from Kiro IDE cache:
If the AWS SSO cache is in a custom location:
Verification
Ad-hoc verification was run locally because the live Docker container owns the normal app port.
Verified:
go build ./...go vet ./...TestReadIdeCacheCredentialExternalIdpTestReadIdeCacheCredentialMissingFileTestReadIdeCacheCredentialNoRefreshTokenTestReadIdeCacheCredentialExternalIdpMissingEndpointTestIdeCachePathPrecedence200 OK/host-aws-sso-cache/kiro-auth-token.json