api9nin / src /lib /oauth /services /kiro.js
github-actions[bot]
deploy from github actions 2026-06-18
c8ae75d
Raw
History Blame Contribute Delete
11.2 kB
import { KIRO_CONFIG } from "../constants/oauth.js";
/**
* Kiro OAuth Service
* Supports multiple authentication methods:
* 1. AWS Builder ID (Device Code Flow)
* 2. AWS IAM Identity Center/IDC (Device Code Flow)
* 3. Google/GitHub Social Login (Authorization Code Flow + Manual Callback)
* 4. Import Token (Manual refresh token paste)
*/
const KIRO_AUTH_SERVICE = "https://prod.us-east-1.auth.desktop.kiro.dev";
export class KiroService {
/**
* Register OIDC client with AWS SSO
* Returns clientId and clientSecret for device code flow
*/
async registerClient(region = "us-east-1") {
const endpoint = `https://oidc.${region}.amazonaws.com/client/register`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
clientName: KIRO_CONFIG.clientName,
clientType: KIRO_CONFIG.clientType,
scopes: KIRO_CONFIG.scopes,
grantTypes: KIRO_CONFIG.grantTypes,
issuerUrl: KIRO_CONFIG.issuerUrl,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to register client: ${error}`);
}
const data = await response.json();
return {
clientId: data.clientId,
clientSecret: data.clientSecret,
clientSecretExpiresAt: data.clientSecretExpiresAt,
};
}
/**
* Start device authorization for AWS Builder ID or IDC
*/
async startDeviceAuthorization(clientId, clientSecret, startUrl, region = "us-east-1") {
const endpoint = `https://oidc.${region}.amazonaws.com/device_authorization`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
clientId,
clientSecret,
startUrl,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to start device authorization: ${error}`);
}
const data = await response.json();
return {
deviceCode: data.deviceCode,
userCode: data.userCode,
verificationUri: data.verificationUri,
verificationUriComplete: data.verificationUriComplete,
expiresIn: data.expiresIn,
interval: data.interval || 5,
};
}
/**
* Poll for token using device code (AWS Builder ID/IDC)
*/
async pollDeviceToken(clientId, clientSecret, deviceCode, region = "us-east-1") {
const endpoint = `https://oidc.${region}.amazonaws.com/token`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
clientId,
clientSecret,
deviceCode,
grantType: "urn:ietf:params:oauth:grant-type:device_code",
}),
});
const data = await response.json();
// Handle pending/slow_down/errors
if (!response.ok || data.error) {
return {
success: false,
error: data.error,
errorDescription: data.error_description,
pending: data.error === "authorization_pending" || data.error === "slow_down",
};
}
return {
success: true,
tokens: {
accessToken: data.accessToken,
refreshToken: data.refreshToken,
expiresIn: data.expiresIn,
tokenType: data.tokenType,
},
};
}
/**
* Build Google/GitHub social login URL
* Returns authorization URL for manual callback flow
* Uses kiro:// custom protocol as required by AWS Cognito whitelist
*/
buildSocialLoginUrl(provider, codeChallenge, state) {
const idp = provider === "google" ? "Google" : "Github";
// AWS Cognito only whitelists kiro:// protocol, not localhost
const redirectUri = "kiro://kiro.kiroAgent/authenticate-success";
return `${KIRO_AUTH_SERVICE}/login?idp=${idp}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256&state=${state}&prompt=select_account`;
}
/**
* Exchange authorization code for tokens (Social Login)
* Must use same redirect_uri as authorization request
*/
async exchangeSocialCode(code, codeVerifier) {
// Must match the redirect_uri used in buildSocialLoginUrl
const redirectUri = "kiro://kiro.kiroAgent/authenticate-success";
const response = await fetch(`${KIRO_AUTH_SERVICE}/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
code_verifier: codeVerifier,
redirect_uri: redirectUri,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
const data = await response.json();
return {
accessToken: data.accessToken,
refreshToken: data.refreshToken,
profileArn: data.profileArn,
expiresIn: data.expiresIn || 3600,
};
}
/**
* Refresh token using refresh token
*/
async refreshToken(refreshToken, providerSpecificData = {}) {
const { authMethod, clientId, clientSecret, region } = providerSpecificData;
// AWS SSO OIDC refresh (Builder ID or IDC)
if (clientId && clientSecret) {
const endpoint = `https://oidc.${region || "us-east-1"}.amazonaws.com/token`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
clientId,
clientSecret,
refreshToken,
grantType: "refresh_token",
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token refresh failed: ${error}`);
}
const data = await response.json();
return {
accessToken: data.accessToken,
refreshToken: data.refreshToken || refreshToken,
profileArn: data.profileArn,
expiresIn: data.expiresIn,
};
}
// Social auth refresh (Google/GitHub)
const response = await fetch(`${KIRO_AUTH_SERVICE}/refreshToken`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
refreshToken,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token refresh failed: ${error}`);
}
const data = await response.json();
return {
accessToken: data.accessToken,
refreshToken: data.refreshToken || refreshToken,
profileArn: data.profileArn,
expiresIn: data.expiresIn || 3600,
};
}
/**
* Validate and import refresh token
*/
async validateImportToken(refreshToken) {
// Validate token format
if (!refreshToken.startsWith("aorAAAAAG")) {
throw new Error("Invalid token format. Token should start with aorAAAAAG...");
}
// Try to refresh to validate
try {
const result = await this.refreshToken(refreshToken);
return {
accessToken: result.accessToken,
refreshToken: result.refreshToken || refreshToken,
profileArn: result.profileArn,
expiresIn: result.expiresIn,
authMethod: "imported",
};
} catch (error) {
throw new Error(`Token validation failed: ${error.message}`);
}
}
/**
* List available CodeWhisperer profiles for a token (or API key) and return
* the best-matching profileArn. AWS SSO OIDC logins return no profileArn, so
* it must be fetched separately — the same call works for API-key auth.
* Accepts both `arn` and `profileArn` response field names (the API-key
* JSON-1.0 surface returns `arn`).
*/
async listAvailableProfiles(accessToken, region = "us-east-1") {
const endpoint = `https://codewhisperer.${region}.amazonaws.com`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-amz-json-1.0",
"x-amz-target": "AmazonCodeWhispererService.ListAvailableProfiles",
"Authorization": `Bearer ${accessToken}`,
"Accept": "application/json",
},
body: JSON.stringify({ maxResults: 10 }),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to list profiles: ${error}`);
}
const data = await response.json();
const profiles = Array.isArray(data?.profiles) ? data.profiles : [];
const arnOf = (p) => p?.arn || p?.profileArn || null;
const match = profiles.find((p) => arnOf(p)?.split(":")[3] === region) || profiles[0];
return arnOf(match);
}
/**
* Validate an API-key credential by listing profiles with it. API keys are
* long-lived bearer tokens (no refresh), so the only way to validate one is
* to make an authenticated CodeWhisperer call. Returns a credential object
* ready to persist as a "kiro" connection with authMethod="api_key".
*/
async validateApiKey(apiKey, region = "us-east-1") {
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
throw new Error("API key is required");
}
const trimmed = apiKey.trim();
let profileArn = null;
try {
profileArn = await this.listAvailableProfiles(trimmed, region);
} catch (error) {
throw new Error(`API key validation failed: ${error.message}`);
}
return {
accessToken: trimmed,
refreshToken: null,
profileArn,
region,
authMethod: "api_key",
};
}
/**
* List available models from CodeWhisperer API
*/
async listAvailableModels(accessToken, profileArn) {
const endpoint = "https://codewhisperer.us-east-1.amazonaws.com";
const target = "AmazonCodeWhispererService.ListAvailableModels";
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-amz-json-1.0",
"x-amz-target": target,
"Authorization": `Bearer ${accessToken}`,
"Accept": "application/json",
},
body: JSON.stringify({
origin: "AI_EDITOR",
profileArn,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to list models: ${error}`);
}
const data = await response.json();
return (data.models || []).map(m => ({
id: m.modelId,
name: m.modelName || m.modelId,
description: m.description,
rateMultiplier: m.rateMultiplier,
rateUnit: m.rateUnit,
maxInputTokens: m.tokenLimits?.maxInputTokens || 0,
}));
}
/**
* Fetch user email from access token (optional, for display)
*/
extractEmailFromJWT(accessToken) {
try {
const parts = accessToken.split(".");
if (parts.length !== 3) return null;
// Decode payload (add padding if needed)
let payload = parts[1];
while (payload.length % 4) {
payload += "=";
}
const decoded = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
return decoded.email || decoded.preferred_username || decoded.sub;
} catch {
return null;
}
}
}