diff --git a/README.md b/README.md index 86d38eb6..533e3cbc 100644 --- a/README.md +++ b/README.md @@ -808,7 +808,7 @@ Models: ```bash Dashboard → Connect Kiro -→ AWS Builder ID or Google/GitHub +→ AWS Builder ID, AWS IAM Identity Center, Google, GitHub → Unlimited usage Models: @@ -1208,4 +1208,3 @@ MIT License - see [LICENSE](LICENSE) for details.
| + |
{colors.emoji}
@@ -114,7 +109,7 @@ export default function QuotaTable({ quotas = [], compact = false }) {
|
{/* Limit (Progress + Numbers) */}
- + |
{/* Progress bar - always show with border for visibility */}
{/* Reset Time */}
-
+ |
{countdown !== "-" || resetDisplay ? (
|
{countdown !== "-" && (
diff --git a/src/app/api/oauth/[provider]/[action]/route.js b/src/app/api/oauth/[provider]/[action]/route.js
index 3e496221..771e1fb1 100644
--- a/src/app/api/oauth/[provider]/[action]/route.js
+++ b/src/app/api/oauth/[provider]/[action]/route.js
@@ -58,15 +58,25 @@ export async function GET(request, { params }) {
}
const authData = generateAuthData(provider, null);
+ const startUrl = searchParams.get("start_url");
+ const region = searchParams.get("region");
+ const authMethod = searchParams.get("auth_method");
+ const deviceOptions = provider === "kiro"
+ ? {
+ ...(startUrl ? { startUrl } : {}),
+ ...(region ? { region } : {}),
+ ...(authMethod ? { authMethod } : {}),
+ }
+ : undefined;
// Providers that don't use PKCE for device code
const noPkceDeviceProviders = ["github", "kiro", "kimi-coding", "kilocode", "codebuddy"];
let deviceData;
if (noPkceDeviceProviders.includes(provider)) {
- deviceData = await requestDeviceCode(provider);
+ deviceData = await requestDeviceCode(provider, undefined, deviceOptions);
} else {
// Qwen and other PKCE providers
- deviceData = await requestDeviceCode(provider, authData.codeChallenge);
+ deviceData = await requestDeviceCode(provider, authData.codeChallenge, deviceOptions);
}
return NextResponse.json({
diff --git a/src/lib/oauth/providers.js b/src/lib/oauth/providers.js
index f004fa64..ffb7e2a1 100644
--- a/src/lib/oauth/providers.js
+++ b/src/lib/oauth/providers.js
@@ -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 };
}
-
diff --git a/src/shared/components/KiroAuthModal.js b/src/shared/components/KiroAuthModal.js
index 0a9133df..6f567c6d 100644
--- a/src/shared/components/KiroAuthModal.js
+++ b/src/shared/components/KiroAuthModal.js
@@ -126,10 +126,10 @@ export default function KiroAuthModal({ isOpen, onMethodSelect, onClose }) {
- {/* AWS IAM Identity Center (IDC) - HIDDEN */}
+ {/* AWS IAM Identity Center (IDC) */}
|