From f8c59227f6056633d70c4f00996490e422702505 Mon Sep 17 00:00:00 2001 From: decolua Date: Mon, 8 Jun 2026 09:45:58 +0700 Subject: [PATCH] fix(kiro): auto-resolve profileArn to prevent 403 on IDC login AWS OIDC IDC/Builder-ID tokens omit profileArn, so CodeWhisperer calls return 403 "User is not authorized". Resolve it natively via the ListAvailableProfiles API instead of reading Kiro IDE profile.json. - providers.js: add fetchKiroProfileArn() and resolve on poll (new logins) - tokenRefresh.js: backfill profileArn on refresh so existing IDC connections self-heal without re-login Co-authored-by: Cursor --- open-sse/services/tokenRefresh.js | 13 +++++++++++++ src/lib/oauth/providers.js | 29 ++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index 0dce4121..40643405 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.js @@ -367,6 +367,17 @@ export async function refreshCodexToken(refreshToken, log) { * Specialized refresh for Kiro (AWS CodeWhisperer) tokens * Supports both AWS SSO OIDC (Builder ID/IDC) and Social Auth (Google/GitHub) */ +// Backfill missing Kiro profileArn on refresh so existing IDC connections self-heal +async function resolveKiroProfileArnPatch(providerSpecificData, accessToken, refreshedArn) { + if (providerSpecificData?.profileArn) return {}; + let profileArn = refreshedArn?.trim?.() || null; + if (!profileArn) { + const { fetchKiroProfileArn } = await import("../../src/lib/oauth/providers.js"); + profileArn = await fetchKiroProfileArn(accessToken); + } + return profileArn ? { providerSpecificData: { profileArn } } : {}; +} + export async function refreshKiroToken(refreshToken, providerSpecificData, log, proxyOptions = null) { if (!refreshToken) return null; return dedupRefresh("kiro", refreshToken, async () => { @@ -417,6 +428,7 @@ export async function refreshKiroToken(refreshToken, providerSpecificData, log, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken || refreshToken, expiresIn: tokens.expiresIn, + ...(await resolveKiroProfileArnPatch(providerSpecificData, tokens.accessToken)), }; } @@ -453,6 +465,7 @@ export async function refreshKiroToken(refreshToken, providerSpecificData, log, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken || refreshToken, expiresIn: tokens.expiresIn, + ...(await resolveKiroProfileArnPatch(providerSpecificData, tokens.accessToken, tokens.profileArn)), }; }, log); } diff --git a/src/lib/oauth/providers.js b/src/lib/oauth/providers.js index 3d23274b..956a53d5 100644 --- a/src/lib/oauth/providers.js +++ b/src/lib/oauth/providers.js @@ -105,6 +105,27 @@ function extractEmailFromAccessToken(accessToken) { return payload.email || payload.preferred_username || payload.sub || undefined; } +// Resolve Kiro profileArn via CodeWhisperer (IDC/Builder-ID tokens omit it, causing 403) +export async function fetchKiroProfileArn(accessToken) { + if (!accessToken) return null; + try { + const response = await fetch("https://codewhisperer.us-east-1.amazonaws.com/ListAvailableProfiles", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ maxResults: 10 }), + }); + if (!response.ok) return null; + const data = await response.json(); + return data.profiles?.find((p) => p.arn?.trim())?.arn?.trim() || null; + } catch { + return null; + } +} + // Extract codex account info from id_token or access token export function extractCodexAccountInfo(idToken) { const payload = decodeJwtPayload(idToken); @@ -1413,7 +1434,13 @@ export async function pollForToken(providerName, deviceCode, codeVerifier, extra if (provider.postExchange) { extra = await provider.postExchange(result.data); } - return { success: true, tokens: provider.mapTokens(result.data, extra) }; + const tokens = provider.mapTokens(result.data, extra); + // Kiro IDC/Builder-ID tokens lack profileArn; resolve it to avoid 403 + if (providerName === "kiro" && !tokens.providerSpecificData?.profileArn) { + const profileArn = await fetchKiroProfileArn(tokens.accessToken); + if (profileArn) tokens.providerSpecificData.profileArn = profileArn; + } + return { success: true, tokens }; } else { // Check if it's still pending authorization if (result.data.error === 'authorization_pending' || result.data.error === 'slow_down') {