From d137368fb57c65e538204fb78df93fbfad5f479f Mon Sep 17 00:00:00 2001 From: engenhariaandrereis01-ai Date: Sun, 23 Aug 2026 19:08:18 -0300 Subject: [PATCH] fix(oauth): stop overwriting Kiro connections that share a profile ARN (#10815) (#11287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated on a 3-PR combined board: kiro-connection-identity 8/8 (written failing-first — 3 new cases red on the pristine release/v3.8.50 tip, green with this change), typecheck:core + dashboard-typecheck clean, all static gates within baseline. Root cause is exactly right: a CodeWhisperer profile ARN identifies the profile, not the account, and distinct Builder ID accounts via social login can share one — the ARN is now trusted only alongside a non-contradicting account-level identifier (email/clientId). Closes #10815. Thank you @engenhariaandrereis01-ai! --- .../fixes/10815-kiro-social-multi-account.md | 1 + src/lib/oauth/kiroConnectionIdentity.ts | 30 +++++++++++- tests/unit/kiro-connection-identity.test.ts | 46 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/10815-kiro-social-multi-account.md diff --git a/changelog.d/fixes/10815-kiro-social-multi-account.md b/changelog.d/fixes/10815-kiro-social-multi-account.md new file mode 100644 index 0000000000..45b2cfed59 --- /dev/null +++ b/changelog.d/fixes/10815-kiro-social-multi-account.md @@ -0,0 +1 @@ +- fix(oauth): stop treating the Kiro profile ARN as an account identity in `findKiroConnectionByIdentity()`, so a second Google/GitHub social login creates a new connection instead of overwriting the first — distinct Builder ID accounts share the same CodeWhisperer profile ARN, and the social token is not a JWT, so no e-mail was available to disambiguate them (#10815) diff --git a/src/lib/oauth/kiroConnectionIdentity.ts b/src/lib/oauth/kiroConnectionIdentity.ts index d5ff76157f..7a2a801b30 100644 --- a/src/lib/oauth/kiroConnectionIdentity.ts +++ b/src/lib/oauth/kiroConnectionIdentity.ts @@ -30,6 +30,27 @@ function providerData(connection: KiroConnectionLike): Record { : {}; } +/** True when the identity carries something that identifies the ACCOUNT (not the profile). */ +function hasAccountIdentifier(identity: KiroConnectionIdentity): boolean { + return Boolean(folded(identity.email) || trimmed(identity.clientId)); +} + +/** True when a shared field is present on both sides and disagrees — different accounts. */ +function contradictsAccount( + connection: KiroConnectionLike, + identity: KiroConnectionIdentity +): boolean { + const email = folded(identity.email); + const existingEmail = folded(connection.email); + if (email && existingEmail && email !== existingEmail) return true; + + const clientId = trimmed(identity.clientId); + const existingClientId = trimmed(providerData(connection).clientId); + if (clientId && existingClientId && clientId !== existingClientId) return true; + + return false; +} + /** Find an existing Kiro account without comparing OAuth tokens or API keys. */ export function findKiroConnectionByIdentity( connections: KiroConnectionLike[], @@ -45,7 +66,14 @@ export function findKiroConnectionByIdentity( const match = candidates.find( (connection) => trimmed(providerData(connection).profileArn) === profileArn ); - if (match) return match; + // A profile ARN identifies the CodeWhisperer PROFILE, not the account: distinct + // Builder ID accounts (Google/GitHub social login) share the same ARN. Accepting it + // as identity made a second social login overwrite the first connection (#10815). + // Only trust the ARN when the incoming identity carries an account-level identifier + // that does not contradict the stored one. + if (match && hasAccountIdentifier(identity) && !contradictsAccount(match, identity)) { + return match; + } } const clientId = trimmed(identity.clientId); diff --git a/tests/unit/kiro-connection-identity.test.ts b/tests/unit/kiro-connection-identity.test.ts index c5ac2b134f..bcda14c1db 100644 --- a/tests/unit/kiro-connection-identity.test.ts +++ b/tests/unit/kiro-connection-identity.test.ts @@ -79,3 +79,49 @@ test("findKiroConnectionByIdentity never overwrites a different authentication t null ); }); + +// #10815 — a profile ARN identifies the CodeWhisperer profile, not the account: two +// distinct social (Google/GitHub) Builder ID accounts share the same ARN, so matching +// on it alone made the second login overwrite the first connection. +const SHARED_PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:1:profile/SHARED"; + +const firstSocialAccount = { + id: "social-account-1", + authType: "oauth", + name: null, + email: null, + providerSpecificData: { + profileArn: SHARED_PROFILE_ARN, + authMethod: "imported", + provider: "Github", + }, +}; + +test("findKiroConnectionByIdentity does not match a shared profile ARN without an account identifier", () => { + const match = findKiroConnectionByIdentity([firstSocialAccount], { + authType: "oauth", + profileArn: SHARED_PROFILE_ARN, + email: null, + }); + assert.equal(match, null); +}); + +test("findKiroConnectionByIdentity treats diverging emails on a shared profile ARN as distinct accounts", () => { + const stored = { ...firstSocialAccount, id: "social-a", email: "a@example.com" }; + const match = findKiroConnectionByIdentity([stored], { + authType: "oauth", + profileArn: SHARED_PROFILE_ARN, + email: "b@example.com", + }); + assert.equal(match, null); +}); + +test("findKiroConnectionByIdentity still matches the same account on a shared profile ARN", () => { + const stored = { ...firstSocialAccount, id: "social-a", email: "a@example.com" }; + const match = findKiroConnectionByIdentity([stored], { + authType: "oauth", + profileArn: SHARED_PROFILE_ARN, + email: "a@example.com", + }); + assert.equal(match?.id, "social-a"); +});