feat: add IDC auth support with Kiro IDE headers

This commit is contained in:
Joao
2025-12-21 14:49:19 +00:00
parent e8de87ee90
commit 7fd98f3556
5 changed files with 1113 additions and 50 deletions

View File

@@ -40,6 +40,10 @@ type KiroTokenData struct {
ClientSecret string `json:"clientSecret,omitempty"`
// Email is the user's email address (used for file naming)
Email string `json:"email,omitempty"`
// StartURL is the IDC/Identity Center start URL (only for IDC auth method)
StartURL string `json:"startUrl,omitempty"`
// Region is the AWS region for IDC authentication (only for IDC auth method)
Region string `json:"region,omitempty"`
}
// KiroAuthBundle aggregates authentication data after OAuth flow completion

View File

@@ -0,0 +1,408 @@
// Package kiro provides Cognito Identity credential exchange for IDC authentication.
// AWS Identity Center (IDC) requires SigV4 signing with Cognito-exchanged credentials
// instead of Bearer token authentication.
package kiro
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
log "github.com/sirupsen/logrus"
)
const (
// Cognito Identity endpoints
cognitoIdentityEndpoint = "https://cognito-identity.us-east-1.amazonaws.com"
// Identity Pool ID for Q Developer / CodeWhisperer
// This is the identity pool used by kiro-cli and Amazon Q CLI
cognitoIdentityPoolID = "us-east-1:70717e99-906f-485d-8d89-c89a0b5d49c5"
// Cognito provider name for SSO OIDC
cognitoProviderName = "cognito-identity.amazonaws.com"
)
// CognitoCredentials holds temporary AWS credentials from Cognito Identity.
type CognitoCredentials struct {
AccessKeyID string `json:"access_key_id"`
SecretAccessKey string `json:"secret_access_key"`
SessionToken string `json:"session_token"`
Expiration time.Time `json:"expiration"`
}
// CognitoIdentityClient handles Cognito Identity credential exchange.
type CognitoIdentityClient struct {
httpClient *http.Client
cfg *config.Config
}
// NewCognitoIdentityClient creates a new Cognito Identity client.
func NewCognitoIdentityClient(cfg *config.Config) *CognitoIdentityClient {
client := &http.Client{Timeout: 30 * time.Second}
if cfg != nil {
client = util.SetProxy(&cfg.SDKConfig, client)
}
return &CognitoIdentityClient{
httpClient: client,
cfg: cfg,
}
}
// GetIdentityID retrieves a Cognito Identity ID using the SSO access token.
func (c *CognitoIdentityClient) GetIdentityID(ctx context.Context, accessToken, region string) (string, error) {
if region == "" {
region = "us-east-1"
}
endpoint := fmt.Sprintf("https://cognito-identity.%s.amazonaws.com", region)
// Build the GetId request
// The SSO token is passed as a login token for the identity pool
payload := map[string]interface{}{
"IdentityPoolId": cognitoIdentityPoolID,
"Logins": map[string]string{
// Use the OIDC provider URL as the key
fmt.Sprintf("oidc.%s.amazonaws.com", region): accessToken,
},
}
body, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("failed to marshal GetId request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(string(body)))
if err != nil {
return "", fmt.Errorf("failed to create GetId request: %w", err)
}
req.Header.Set("Content-Type", "application/x-amz-json-1.1")
req.Header.Set("X-Amz-Target", "AWSCognitoIdentityService.GetId")
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("GetId request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read GetId response: %w", err)
}
if resp.StatusCode != http.StatusOK {
log.Debugf("Cognito GetId failed (status %d): %s", resp.StatusCode, string(respBody))
return "", fmt.Errorf("GetId failed (status %d): %s", resp.StatusCode, string(respBody))
}
var result struct {
IdentityID string `json:"IdentityId"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("failed to parse GetId response: %w", err)
}
if result.IdentityID == "" {
return "", fmt.Errorf("empty IdentityId in GetId response")
}
log.Debugf("Cognito Identity ID: %s", result.IdentityID)
return result.IdentityID, nil
}
// GetCredentialsForIdentity exchanges an identity ID and login token for temporary AWS credentials.
func (c *CognitoIdentityClient) GetCredentialsForIdentity(ctx context.Context, identityID, accessToken, region string) (*CognitoCredentials, error) {
if region == "" {
region = "us-east-1"
}
endpoint := fmt.Sprintf("https://cognito-identity.%s.amazonaws.com", region)
payload := map[string]interface{}{
"IdentityId": identityID,
"Logins": map[string]string{
fmt.Sprintf("oidc.%s.amazonaws.com", region): accessToken,
},
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal GetCredentialsForIdentity request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(string(body)))
if err != nil {
return nil, fmt.Errorf("failed to create GetCredentialsForIdentity request: %w", err)
}
req.Header.Set("Content-Type", "application/x-amz-json-1.1")
req.Header.Set("X-Amz-Target", "AWSCognitoIdentityService.GetCredentialsForIdentity")
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("GetCredentialsForIdentity request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read GetCredentialsForIdentity response: %w", err)
}
if resp.StatusCode != http.StatusOK {
log.Debugf("Cognito GetCredentialsForIdentity failed (status %d): %s", resp.StatusCode, string(respBody))
return nil, fmt.Errorf("GetCredentialsForIdentity failed (status %d): %s", resp.StatusCode, string(respBody))
}
var result struct {
Credentials struct {
AccessKeyID string `json:"AccessKeyId"`
SecretKey string `json:"SecretKey"`
SessionToken string `json:"SessionToken"`
Expiration int64 `json:"Expiration"`
} `json:"Credentials"`
IdentityID string `json:"IdentityId"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse GetCredentialsForIdentity response: %w", err)
}
if result.Credentials.AccessKeyID == "" {
return nil, fmt.Errorf("empty AccessKeyId in GetCredentialsForIdentity response")
}
// Expiration is in seconds since epoch
expiration := time.Unix(result.Credentials.Expiration, 0)
log.Debugf("Cognito credentials obtained, expires: %s", expiration.Format(time.RFC3339))
return &CognitoCredentials{
AccessKeyID: result.Credentials.AccessKeyID,
SecretAccessKey: result.Credentials.SecretKey,
SessionToken: result.Credentials.SessionToken,
Expiration: expiration,
}, nil
}
// ExchangeSSOTokenForCredentials is a convenience method that performs the full
// Cognito Identity credential exchange flow: GetId -> GetCredentialsForIdentity
func (c *CognitoIdentityClient) ExchangeSSOTokenForCredentials(ctx context.Context, accessToken, region string) (*CognitoCredentials, error) {
log.Debugf("Exchanging SSO token for Cognito credentials (region: %s)", region)
// Step 1: Get Identity ID
identityID, err := c.GetIdentityID(ctx, accessToken, region)
if err != nil {
return nil, fmt.Errorf("failed to get identity ID: %w", err)
}
// Step 2: Get credentials for the identity
creds, err := c.GetCredentialsForIdentity(ctx, identityID, accessToken, region)
if err != nil {
return nil, fmt.Errorf("failed to get credentials for identity: %w", err)
}
return creds, nil
}
// SigV4Signer provides AWS Signature Version 4 signing for HTTP requests.
type SigV4Signer struct {
credentials *CognitoCredentials
region string
service string
}
// NewSigV4Signer creates a new SigV4 signer with the given credentials.
func NewSigV4Signer(creds *CognitoCredentials, region, service string) *SigV4Signer {
return &SigV4Signer{
credentials: creds,
region: region,
service: service,
}
}
// SignRequest signs an HTTP request using AWS Signature Version 4.
// The request body must be provided separately since it may have been read already.
func (s *SigV4Signer) SignRequest(req *http.Request, body []byte) error {
now := time.Now().UTC()
amzDate := now.Format("20060102T150405Z")
dateStamp := now.Format("20060102")
// Ensure required headers are set
if req.Header.Get("Host") == "" {
req.Header.Set("Host", req.URL.Host)
}
req.Header.Set("X-Amz-Date", amzDate)
if s.credentials.SessionToken != "" {
req.Header.Set("X-Amz-Security-Token", s.credentials.SessionToken)
}
// Create canonical request
canonicalRequest, signedHeaders := s.createCanonicalRequest(req, body)
// Create string to sign
algorithm := "AWS4-HMAC-SHA256"
credentialScope := fmt.Sprintf("%s/%s/%s/aws4_request", dateStamp, s.region, s.service)
stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s",
algorithm,
amzDate,
credentialScope,
hashSHA256([]byte(canonicalRequest)),
)
// Calculate signature
signingKey := s.getSignatureKey(dateStamp)
signature := hex.EncodeToString(hmacSHA256(signingKey, []byte(stringToSign)))
// Build Authorization header
authHeader := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
algorithm,
s.credentials.AccessKeyID,
credentialScope,
signedHeaders,
signature,
)
req.Header.Set("Authorization", authHeader)
return nil
}
// createCanonicalRequest builds the canonical request string for SigV4.
func (s *SigV4Signer) createCanonicalRequest(req *http.Request, body []byte) (string, string) {
// HTTP method
method := req.Method
// Canonical URI
uri := req.URL.Path
if uri == "" {
uri = "/"
}
// Canonical query string (sorted)
queryString := s.buildCanonicalQueryString(req)
// Canonical headers (sorted, lowercase)
canonicalHeaders, signedHeaders := s.buildCanonicalHeaders(req)
// Hashed payload
payloadHash := hashSHA256(body)
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
method,
uri,
queryString,
canonicalHeaders,
signedHeaders,
payloadHash,
)
return canonicalRequest, signedHeaders
}
// buildCanonicalQueryString builds a sorted, URI-encoded query string.
func (s *SigV4Signer) buildCanonicalQueryString(req *http.Request) string {
if req.URL.RawQuery == "" {
return ""
}
// Parse and sort query parameters
params := make([]string, 0)
for key, values := range req.URL.Query() {
for _, value := range values {
params = append(params, fmt.Sprintf("%s=%s", uriEncode(key), uriEncode(value)))
}
}
sort.Strings(params)
return strings.Join(params, "&")
}
// buildCanonicalHeaders builds sorted, lowercase canonical headers.
func (s *SigV4Signer) buildCanonicalHeaders(req *http.Request) (string, string) {
// Headers to sign (must include host and x-amz-*)
headerMap := make(map[string]string)
headerMap["host"] = req.URL.Host
for key, values := range req.Header {
lowKey := strings.ToLower(key)
// Include x-amz-* headers and content-type
if strings.HasPrefix(lowKey, "x-amz-") || lowKey == "content-type" {
headerMap[lowKey] = strings.TrimSpace(values[0])
}
}
// Sort header names
headerNames := make([]string, 0, len(headerMap))
for name := range headerMap {
headerNames = append(headerNames, name)
}
sort.Strings(headerNames)
// Build canonical headers and signed headers
var canonicalHeaders strings.Builder
for _, name := range headerNames {
canonicalHeaders.WriteString(name)
canonicalHeaders.WriteString(":")
canonicalHeaders.WriteString(headerMap[name])
canonicalHeaders.WriteString("\n")
}
signedHeaders := strings.Join(headerNames, ";")
return canonicalHeaders.String(), signedHeaders
}
// getSignatureKey derives the signing key for SigV4.
func (s *SigV4Signer) getSignatureKey(dateStamp string) []byte {
kDate := hmacSHA256([]byte("AWS4"+s.credentials.SecretAccessKey), []byte(dateStamp))
kRegion := hmacSHA256(kDate, []byte(s.region))
kService := hmacSHA256(kRegion, []byte(s.service))
kSigning := hmacSHA256(kService, []byte("aws4_request"))
return kSigning
}
// hmacSHA256 computes HMAC-SHA256.
func hmacSHA256(key, data []byte) []byte {
h := hmac.New(sha256.New, key)
h.Write(data)
return h.Sum(nil)
}
// hashSHA256 computes SHA256 hash and returns hex string.
func hashSHA256(data []byte) string {
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:])
}
// uriEncode performs URI encoding for SigV4.
func uriEncode(s string) string {
var result strings.Builder
for i := 0; i < len(s); i++ {
c := s[i]
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '~' {
result.WriteByte(c)
} else {
result.WriteString(fmt.Sprintf("%%%02X", c))
}
}
return result.String()
}
// IsExpired checks if the credentials are expired or about to expire.
func (c *CognitoCredentials) IsExpired() bool {
// Consider expired if within 5 minutes of expiration
return time.Now().Add(5 * time.Minute).After(c.Expiration)
}

View File

@@ -2,6 +2,7 @@
package kiro
import (
"bufio"
"context"
"crypto/rand"
"crypto/sha256"
@@ -12,6 +13,7 @@ import (
"io"
"net"
"net/http"
"os"
"strings"
"time"
@@ -24,10 +26,13 @@ import (
const (
// AWS SSO OIDC endpoints
ssoOIDCEndpoint = "https://oidc.us-east-1.amazonaws.com"
// Kiro's start URL for Builder ID
builderIDStartURL = "https://view.awsapps.com/start"
// Default region for IDC
defaultIDCRegion = "us-east-1"
// Polling interval
pollInterval = 5 * time.Second
@@ -83,6 +88,429 @@ type CreateTokenResponse struct {
RefreshToken string `json:"refreshToken"`
}
// getOIDCEndpoint returns the OIDC endpoint for the given region.
func getOIDCEndpoint(region string) string {
if region == "" {
region = defaultIDCRegion
}
return fmt.Sprintf("https://oidc.%s.amazonaws.com", region)
}
// promptInput prompts the user for input with an optional default value.
func promptInput(prompt, defaultValue string) string {
reader := bufio.NewReader(os.Stdin)
if defaultValue != "" {
fmt.Printf("%s [%s]: ", prompt, defaultValue)
} else {
fmt.Printf("%s: ", prompt)
}
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if input == "" {
return defaultValue
}
return input
}
// promptSelect prompts the user to select from options using arrow keys or number input.
func promptSelect(prompt string, options []string) int {
fmt.Println(prompt)
for i, opt := range options {
fmt.Printf(" %d) %s\n", i+1, opt)
}
fmt.Print("Enter selection (1-", len(options), "): ")
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
// Parse the selection
var selection int
if _, err := fmt.Sscanf(input, "%d", &selection); err != nil || selection < 1 || selection > len(options) {
return 0 // Default to first option
}
return selection - 1
}
// RegisterClientWithRegion registers a new OIDC client with AWS using a specific region.
func (c *SSOOIDCClient) RegisterClientWithRegion(ctx context.Context, region string) (*RegisterClientResponse, error) {
endpoint := getOIDCEndpoint(region)
payload := map[string]interface{}{
"clientName": "Kiro IDE",
"clientType": "public",
"scopes": []string{"codewhisperer:completions", "codewhisperer:analysis", "codewhisperer:conversations", "codewhisperer:transformations", "codewhisperer:taskassist"},
"grantTypes": []string{"urn:ietf:params:oauth:grant-type:device_code", "refresh_token"},
}
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint+"/client/register", strings.NewReader(string(body)))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", kiroUserAgent)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
log.Debugf("register client failed (status %d): %s", resp.StatusCode, string(respBody))
return nil, fmt.Errorf("register client failed (status %d)", resp.StatusCode)
}
var result RegisterClientResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, err
}
return &result, nil
}
// StartDeviceAuthorizationWithIDC starts the device authorization flow for IDC.
func (c *SSOOIDCClient) StartDeviceAuthorizationWithIDC(ctx context.Context, clientID, clientSecret, startURL, region string) (*StartDeviceAuthResponse, error) {
endpoint := getOIDCEndpoint(region)
payload := map[string]string{
"clientId": clientID,
"clientSecret": clientSecret,
"startUrl": startURL,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint+"/device_authorization", strings.NewReader(string(body)))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", kiroUserAgent)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
log.Debugf("start device auth failed (status %d): %s", resp.StatusCode, string(respBody))
return nil, fmt.Errorf("start device auth failed (status %d)", resp.StatusCode)
}
var result StartDeviceAuthResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, err
}
return &result, nil
}
// CreateTokenWithRegion polls for the access token after user authorization using a specific region.
func (c *SSOOIDCClient) CreateTokenWithRegion(ctx context.Context, clientID, clientSecret, deviceCode, region string) (*CreateTokenResponse, error) {
endpoint := getOIDCEndpoint(region)
payload := map[string]string{
"clientId": clientID,
"clientSecret": clientSecret,
"deviceCode": deviceCode,
"grantType": "urn:ietf:params:oauth:grant-type:device_code",
}
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint+"/token", strings.NewReader(string(body)))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", kiroUserAgent)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Check for pending authorization
if resp.StatusCode == http.StatusBadRequest {
var errResp struct {
Error string `json:"error"`
}
if json.Unmarshal(respBody, &errResp) == nil {
if errResp.Error == "authorization_pending" {
return nil, fmt.Errorf("authorization_pending")
}
if errResp.Error == "slow_down" {
return nil, fmt.Errorf("slow_down")
}
}
log.Debugf("create token failed: %s", string(respBody))
return nil, fmt.Errorf("create token failed")
}
if resp.StatusCode != http.StatusOK {
log.Debugf("create token failed (status %d): %s", resp.StatusCode, string(respBody))
return nil, fmt.Errorf("create token failed (status %d)", resp.StatusCode)
}
var result CreateTokenResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, err
}
return &result, nil
}
// RefreshTokenWithRegion refreshes an access token using the refresh token with a specific region.
func (c *SSOOIDCClient) RefreshTokenWithRegion(ctx context.Context, clientID, clientSecret, refreshToken, region, startURL string) (*KiroTokenData, error) {
endpoint := getOIDCEndpoint(region)
payload := map[string]string{
"clientId": clientID,
"clientSecret": clientSecret,
"refreshToken": refreshToken,
"grantType": "refresh_token",
}
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint+"/token", strings.NewReader(string(body)))
if err != nil {
return nil, err
}
// Set headers matching kiro2api's IDC token refresh
// These headers are required for successful IDC token refresh
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Host", fmt.Sprintf("oidc.%s.amazonaws.com", region))
req.Header.Set("Connection", "keep-alive")
req.Header.Set("x-amz-user-agent", "aws-sdk-js/3.738.0 ua/2.1 os/other lang/js md/browser#unknown_unknown api/sso-oidc#3.738.0 m/E KiroIDE")
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "*")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("User-Agent", "node")
req.Header.Set("Accept-Encoding", "br, gzip, deflate")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
log.Debugf("token refresh failed (status %d): %s", resp.StatusCode, string(respBody))
return nil, fmt.Errorf("token refresh failed (status %d)", resp.StatusCode)
}
var result CreateTokenResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, err
}
expiresAt := time.Now().Add(time.Duration(result.ExpiresIn) * time.Second)
return &KiroTokenData{
AccessToken: result.AccessToken,
RefreshToken: result.RefreshToken,
ExpiresAt: expiresAt.Format(time.RFC3339),
AuthMethod: "idc",
Provider: "AWS",
ClientID: clientID,
ClientSecret: clientSecret,
StartURL: startURL,
Region: region,
}, nil
}
// LoginWithIDC performs the full device code flow for AWS Identity Center (IDC).
func (c *SSOOIDCClient) LoginWithIDC(ctx context.Context, startURL, region string) (*KiroTokenData, error) {
fmt.Println("\n╔══════════════════════════════════════════════════════════╗")
fmt.Println("║ Kiro Authentication (AWS Identity Center) ║")
fmt.Println("╚══════════════════════════════════════════════════════════╝")
// Step 1: Register client with the specified region
fmt.Println("\nRegistering client...")
regResp, err := c.RegisterClientWithRegion(ctx, region)
if err != nil {
return nil, fmt.Errorf("failed to register client: %w", err)
}
log.Debugf("Client registered: %s", regResp.ClientID)
// Step 2: Start device authorization with IDC start URL
fmt.Println("Starting device authorization...")
authResp, err := c.StartDeviceAuthorizationWithIDC(ctx, regResp.ClientID, regResp.ClientSecret, startURL, region)
if err != nil {
return nil, fmt.Errorf("failed to start device auth: %w", err)
}
// Step 3: Show user the verification URL
fmt.Printf("\n")
fmt.Println("════════════════════════════════════════════════════════════")
fmt.Printf(" Confirm the following code in the browser:\n")
fmt.Printf(" Code: %s\n", authResp.UserCode)
fmt.Println("════════════════════════════════════════════════════════════")
fmt.Printf("\n Open this URL: %s\n\n", authResp.VerificationURIComplete)
// Set incognito mode based on config
if c.cfg != nil {
browser.SetIncognitoMode(c.cfg.IncognitoBrowser)
if !c.cfg.IncognitoBrowser {
log.Info("kiro: using normal browser mode (--no-incognito). Note: You may not be able to select a different account.")
} else {
log.Debug("kiro: using incognito mode for multi-account support")
}
} else {
browser.SetIncognitoMode(true)
log.Debug("kiro: using incognito mode for multi-account support (default)")
}
// Open browser
if err := browser.OpenURL(authResp.VerificationURIComplete); err != nil {
log.Warnf("Could not open browser automatically: %v", err)
fmt.Println(" Please open the URL manually in your browser.")
} else {
fmt.Println(" (Browser opened automatically)")
}
// Step 4: Poll for token
fmt.Println("Waiting for authorization...")
interval := pollInterval
if authResp.Interval > 0 {
interval = time.Duration(authResp.Interval) * time.Second
}
deadline := time.Now().Add(time.Duration(authResp.ExpiresIn) * time.Second)
for time.Now().Before(deadline) {
select {
case <-ctx.Done():
browser.CloseBrowser()
return nil, ctx.Err()
case <-time.After(interval):
tokenResp, err := c.CreateTokenWithRegion(ctx, regResp.ClientID, regResp.ClientSecret, authResp.DeviceCode, region)
if err != nil {
errStr := err.Error()
if strings.Contains(errStr, "authorization_pending") {
fmt.Print(".")
continue
}
if strings.Contains(errStr, "slow_down") {
interval += 5 * time.Second
continue
}
browser.CloseBrowser()
return nil, fmt.Errorf("token creation failed: %w", err)
}
fmt.Println("\n\n✓ Authorization successful!")
// Close the browser window
if err := browser.CloseBrowser(); err != nil {
log.Debugf("Failed to close browser: %v", err)
}
// Step 5: Get profile ARN from CodeWhisperer API
fmt.Println("Fetching profile information...")
profileArn := c.fetchProfileArn(ctx, tokenResp.AccessToken)
// Fetch user email
email := FetchUserEmailWithFallback(ctx, c.cfg, tokenResp.AccessToken)
if email != "" {
fmt.Printf(" Logged in as: %s\n", email)
}
expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
return &KiroTokenData{
AccessToken: tokenResp.AccessToken,
RefreshToken: tokenResp.RefreshToken,
ProfileArn: profileArn,
ExpiresAt: expiresAt.Format(time.RFC3339),
AuthMethod: "idc",
Provider: "AWS",
ClientID: regResp.ClientID,
ClientSecret: regResp.ClientSecret,
Email: email,
StartURL: startURL,
Region: region,
}, nil
}
}
// Close browser on timeout
if err := browser.CloseBrowser(); err != nil {
log.Debugf("Failed to close browser on timeout: %v", err)
}
return nil, fmt.Errorf("authorization timed out")
}
// LoginWithMethodSelection prompts the user to select between Builder ID and IDC, then performs the login.
func (c *SSOOIDCClient) LoginWithMethodSelection(ctx context.Context) (*KiroTokenData, error) {
fmt.Println("\n╔══════════════════════════════════════════════════════════╗")
fmt.Println("║ Kiro Authentication (AWS) ║")
fmt.Println("╚══════════════════════════════════════════════════════════╝")
// Prompt for login method
options := []string{
"Use with Builder ID (personal AWS account)",
"Use with IDC Account (organization SSO)",
}
selection := promptSelect("\n? Select login method:", options)
if selection == 0 {
// Builder ID flow - use existing implementation
return c.LoginWithBuilderID(ctx)
}
// IDC flow - prompt for start URL and region
fmt.Println()
startURL := promptInput("? Enter Start URL", "")
if startURL == "" {
return nil, fmt.Errorf("start URL is required for IDC login")
}
region := promptInput("? Enter Region", defaultIDCRegion)
return c.LoginWithIDC(ctx, startURL, region)
}
// RegisterClient registers a new OIDC client with AWS.
func (c *SSOOIDCClient) RegisterClient(ctx context.Context) (*RegisterClientResponse, error) {
payload := map[string]interface{}{