* feat(kiro): wire aws identity center device flow into provider oauth (#587)

* feat(kiro): wire aws identity center device flow into provider oauth

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Dang Dinh Quan
2026-04-15 11:44:46 +07:00
committed by GitHub
parent c3a2bd01b7
commit b1288c5064
7 changed files with 243 additions and 164 deletions

View File

@@ -25,6 +25,28 @@ import {
CODEBUDDY_CONFIG,
} from "./constants/oauth";
const BASE64_BLOCK_SIZE = 4;
/**
* Decode JWT access token and extract a stable account identifier for display/upsert.
* @param {string} accessToken
* @returns {string|undefined}
*/
function extractEmailFromAccessToken(accessToken) {
try {
if (!accessToken || typeof accessToken !== "string") return undefined;
const parts = accessToken.split(".");
if (parts.length !== 3) return undefined;
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const missingPadding = (BASE64_BLOCK_SIZE - (base64.length % BASE64_BLOCK_SIZE)) % BASE64_BLOCK_SIZE;
const padded = base64 + "=".repeat(missingPadding);
const payload = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
return payload.email || payload.preferred_username || payload.sub || undefined;
} catch {
return undefined;
}
}
// Provider configurations
const PROVIDERS = {
claude: {
@@ -652,9 +674,17 @@ const PROVIDERS = {
config: KIRO_CONFIG,
flowType: "device_code",
// Kiro uses AWS SSO OIDC - requires client registration first
requestDeviceCode: async (config) => {
requestDeviceCode: async (config, codeChallenge, options = {}) => {
const trimmedRegion = typeof options.region === "string" ? options.region.trim() : "";
const region = trimmedRegion || "us-east-1";
const trimmedStartUrl = typeof options.startUrl === "string" ? options.startUrl.trim() : "";
const startUrl = trimmedStartUrl || config.startUrl;
const authMethod = options.authMethod === "idc" ? "idc" : "builder-id";
const registerClientUrl = `https://oidc.${region}.amazonaws.com/client/register`;
const deviceAuthUrl = `https://oidc.${region}.amazonaws.com/device_authorization`;
// Step 1: Register client with AWS SSO OIDC
const registerRes = await fetch(config.registerClientUrl, {
const registerRes = await fetch(registerClientUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -677,7 +707,7 @@ const PROVIDERS = {
const clientInfo = await registerRes.json();
// Step 2: Request device authorization
const deviceRes = await fetch(config.deviceAuthUrl, {
const deviceRes = await fetch(deviceAuthUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -686,7 +716,7 @@ const PROVIDERS = {
body: JSON.stringify({
clientId: clientInfo.clientId,
clientSecret: clientInfo.clientSecret,
startUrl: config.startUrl,
startUrl,
}),
});
@@ -708,10 +738,15 @@ const PROVIDERS = {
// Store client credentials for token exchange
_clientId: clientInfo.clientId,
_clientSecret: clientInfo.clientSecret,
_region: region,
_authMethod: authMethod,
_startUrl: startUrl,
};
},
pollToken: async (config, deviceCode, codeVerifier, extraData) => {
const response = await fetch(config.tokenUrl, {
const region = extraData?._region || "us-east-1";
const tokenUrl = `https://oidc.${region}.amazonaws.com/token`;
const response = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -745,6 +780,9 @@ const PROVIDERS = {
// Store client credentials for refresh
_clientId: extraData?._clientId,
_clientSecret: extraData?._clientSecret,
_region: extraData?._region,
_authMethod: extraData?._authMethod,
_startUrl: extraData?._startUrl,
},
};
}
@@ -758,14 +796,19 @@ const PROVIDERS = {
};
},
mapTokens: (tokens) => {
const email = extractEmailFromAccessToken(tokens.access_token);
const mapped = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
email,
providerSpecificData: {
profileArn: tokens?.profile_arn || null,
clientId: tokens._clientId,
clientSecret: tokens._clientSecret,
region: tokens._region || "us-east-1",
authMethod: tokens._authMethod || "builder-id",
startUrl: tokens._startUrl || KIRO_CONFIG.startUrl,
},
};
return mapped;
@@ -1158,12 +1201,12 @@ export async function exchangeTokens(providerName, code, redirectUri, codeVerifi
/**
* Request device code (for device_code flow)
*/
export async function requestDeviceCode(providerName, codeChallenge) {
export async function requestDeviceCode(providerName, codeChallenge, options) {
const provider = getProvider(providerName);
if (provider.flowType !== "device_code") {
throw new Error(`Provider ${providerName} does not support device code flow`);
}
return await provider.requestDeviceCode(provider.config, codeChallenge);
return await provider.requestDeviceCode(provider.config, codeChallenge, options || {});
}
/**
@@ -1213,4 +1256,3 @@ export async function pollForToken(providerName, deviceCode, codeVerifier, extra
return { success: false, error: result.data.error, errorDescription: result.data.error_description };
}