| package auth |
|
|
| import ( |
| "context" |
| "fmt" |
| "strings" |
| "time" |
|
|
| kiroauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kiro" |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" |
| ) |
|
|
| |
| |
| |
| func extractKiroIdentifier(accountName, profileArn string) string { |
| |
| if accountName != "" { |
| return kiroauth.SanitizeEmailForFilename(accountName) |
| } |
|
|
| |
| if profileArn != "" { |
| parts := strings.Split(profileArn, "/") |
| if len(parts) >= 2 { |
| |
| return kiroauth.SanitizeEmailForFilename(parts[len(parts)-1]) |
| } |
| } |
|
|
| |
| return fmt.Sprintf("%d", time.Now().UnixNano()%100000) |
| } |
|
|
| |
| type KiroAuthenticator struct{} |
|
|
| |
| func NewKiroAuthenticator() *KiroAuthenticator { |
| return &KiroAuthenticator{} |
| } |
|
|
| |
| func (a *KiroAuthenticator) Provider() string { |
| return "kiro" |
| } |
|
|
| |
| |
| func (a *KiroAuthenticator) RefreshLead() *time.Duration { |
| d := 5 * time.Minute |
| return &d |
| } |
|
|
| |
| func (a *KiroAuthenticator) createAuthRecord(tokenData *kiroauth.KiroTokenData, source string) (*coreauth.Auth, error) { |
| |
| expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt) |
| if err != nil { |
| expiresAt = time.Now().Add(1 * time.Hour) |
| } |
|
|
| |
| idPart := extractKiroIdentifier(tokenData.Email, tokenData.ProfileArn) |
|
|
| |
| label := fmt.Sprintf("kiro-%s", source) |
| if tokenData.AuthMethod == "idc" { |
| label = "kiro-idc" |
| } |
|
|
| now := time.Now() |
| fileName := fmt.Sprintf("%s-%s.json", label, idPart) |
|
|
| metadata := map[string]any{ |
| "type": "kiro", |
| "access_token": tokenData.AccessToken, |
| "refresh_token": tokenData.RefreshToken, |
| "profile_arn": tokenData.ProfileArn, |
| "expires_at": tokenData.ExpiresAt, |
| "auth_method": tokenData.AuthMethod, |
| "provider": tokenData.Provider, |
| "client_id": tokenData.ClientID, |
| "client_secret": tokenData.ClientSecret, |
| "email": tokenData.Email, |
| } |
|
|
| |
| if tokenData.StartURL != "" { |
| metadata["start_url"] = tokenData.StartURL |
| } |
| if tokenData.Region != "" { |
| metadata["region"] = tokenData.Region |
| } |
|
|
| attributes := map[string]string{ |
| "profile_arn": tokenData.ProfileArn, |
| "source": source, |
| "email": tokenData.Email, |
| } |
|
|
| |
| if tokenData.AuthMethod == "idc" { |
| attributes["source"] = "aws-idc" |
| if tokenData.StartURL != "" { |
| attributes["start_url"] = tokenData.StartURL |
| } |
| if tokenData.Region != "" { |
| attributes["region"] = tokenData.Region |
| } |
| } |
|
|
| record := &coreauth.Auth{ |
| ID: fileName, |
| Provider: "kiro", |
| FileName: fileName, |
| Label: label, |
| Status: coreauth.StatusActive, |
| CreatedAt: now, |
| UpdatedAt: now, |
| Metadata: metadata, |
| Attributes: attributes, |
| |
| NextRefreshAfter: expiresAt.Add(-5 * time.Minute), |
| } |
|
|
| if tokenData.Email != "" { |
| fmt.Printf("\n✓ Kiro authentication completed successfully! (Account: %s)\n", tokenData.Email) |
| } else { |
| fmt.Println("\n✓ Kiro authentication completed successfully!") |
| } |
|
|
| return record, nil |
| } |
|
|
| |
| |
| func (a *KiroAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { |
| if cfg == nil { |
| return nil, fmt.Errorf("kiro auth: configuration is required") |
| } |
|
|
| |
| ssoClient := kiroauth.NewSSOOIDCClient(cfg) |
| tokenData, err := ssoClient.LoginWithMethodSelection(ctx) |
| if err != nil { |
| return nil, fmt.Errorf("login failed: %w", err) |
| } |
|
|
| return a.createAuthRecord(tokenData, "aws") |
| } |
|
|
| |
| |
| func (a *KiroAuthenticator) LoginWithAuthCode(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { |
| if cfg == nil { |
| return nil, fmt.Errorf("kiro auth: configuration is required") |
| } |
|
|
| oauth := kiroauth.NewKiroOAuth(cfg) |
|
|
| |
| tokenData, err := oauth.LoginWithBuilderIDAuthCode(ctx) |
| if err != nil { |
| return nil, fmt.Errorf("login failed: %w", err) |
| } |
|
|
| |
| expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt) |
| if err != nil { |
| expiresAt = time.Now().Add(1 * time.Hour) |
| } |
|
|
| |
| idPart := extractKiroIdentifier(tokenData.Email, tokenData.ProfileArn) |
|
|
| now := time.Now() |
| fileName := fmt.Sprintf("kiro-aws-%s.json", idPart) |
|
|
| record := &coreauth.Auth{ |
| ID: fileName, |
| Provider: "kiro", |
| FileName: fileName, |
| Label: "kiro-aws", |
| Status: coreauth.StatusActive, |
| CreatedAt: now, |
| UpdatedAt: now, |
| Metadata: map[string]any{ |
| "type": "kiro", |
| "access_token": tokenData.AccessToken, |
| "refresh_token": tokenData.RefreshToken, |
| "profile_arn": tokenData.ProfileArn, |
| "expires_at": tokenData.ExpiresAt, |
| "auth_method": tokenData.AuthMethod, |
| "provider": tokenData.Provider, |
| "client_id": tokenData.ClientID, |
| "client_secret": tokenData.ClientSecret, |
| "email": tokenData.Email, |
| }, |
| Attributes: map[string]string{ |
| "profile_arn": tokenData.ProfileArn, |
| "source": "aws-builder-id-authcode", |
| "email": tokenData.Email, |
| }, |
| |
| NextRefreshAfter: expiresAt.Add(-5 * time.Minute), |
| } |
|
|
| if tokenData.Email != "" { |
| fmt.Printf("\n✓ Kiro authentication completed successfully! (Account: %s)\n", tokenData.Email) |
| } else { |
| fmt.Println("\n✓ Kiro authentication completed successfully!") |
| } |
|
|
| return record, nil |
| } |
|
|
| |
| |
| func (a *KiroAuthenticator) LoginWithGoogle(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { |
| if cfg == nil { |
| return nil, fmt.Errorf("kiro auth: configuration is required") |
| } |
|
|
| oauth := kiroauth.NewKiroOAuth(cfg) |
|
|
| |
| tokenData, err := oauth.LoginWithGoogle(ctx) |
| if err != nil { |
| return nil, fmt.Errorf("google login failed: %w", err) |
| } |
|
|
| |
| expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt) |
| if err != nil { |
| expiresAt = time.Now().Add(1 * time.Hour) |
| } |
|
|
| |
| idPart := extractKiroIdentifier(tokenData.Email, tokenData.ProfileArn) |
|
|
| now := time.Now() |
| fileName := fmt.Sprintf("kiro-google-%s.json", idPart) |
|
|
| record := &coreauth.Auth{ |
| ID: fileName, |
| Provider: "kiro", |
| FileName: fileName, |
| Label: "kiro-google", |
| Status: coreauth.StatusActive, |
| CreatedAt: now, |
| UpdatedAt: now, |
| Metadata: map[string]any{ |
| "type": "kiro", |
| "access_token": tokenData.AccessToken, |
| "refresh_token": tokenData.RefreshToken, |
| "profile_arn": tokenData.ProfileArn, |
| "expires_at": tokenData.ExpiresAt, |
| "auth_method": tokenData.AuthMethod, |
| "provider": tokenData.Provider, |
| "email": tokenData.Email, |
| }, |
| Attributes: map[string]string{ |
| "profile_arn": tokenData.ProfileArn, |
| "source": "google-oauth", |
| "email": tokenData.Email, |
| }, |
| |
| NextRefreshAfter: expiresAt.Add(-5 * time.Minute), |
| } |
|
|
| if tokenData.Email != "" { |
| fmt.Printf("\n✓ Kiro Google authentication completed successfully! (Account: %s)\n", tokenData.Email) |
| } else { |
| fmt.Println("\n✓ Kiro Google authentication completed successfully!") |
| } |
|
|
| return record, nil |
| } |
|
|
| |
| |
| func (a *KiroAuthenticator) LoginWithGitHub(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { |
| if cfg == nil { |
| return nil, fmt.Errorf("kiro auth: configuration is required") |
| } |
|
|
| oauth := kiroauth.NewKiroOAuth(cfg) |
|
|
| |
| tokenData, err := oauth.LoginWithGitHub(ctx) |
| if err != nil { |
| return nil, fmt.Errorf("github login failed: %w", err) |
| } |
|
|
| |
| expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt) |
| if err != nil { |
| expiresAt = time.Now().Add(1 * time.Hour) |
| } |
|
|
| |
| idPart := extractKiroIdentifier(tokenData.Email, tokenData.ProfileArn) |
|
|
| now := time.Now() |
| fileName := fmt.Sprintf("kiro-github-%s.json", idPart) |
|
|
| record := &coreauth.Auth{ |
| ID: fileName, |
| Provider: "kiro", |
| FileName: fileName, |
| Label: "kiro-github", |
| Status: coreauth.StatusActive, |
| CreatedAt: now, |
| UpdatedAt: now, |
| Metadata: map[string]any{ |
| "type": "kiro", |
| "access_token": tokenData.AccessToken, |
| "refresh_token": tokenData.RefreshToken, |
| "profile_arn": tokenData.ProfileArn, |
| "expires_at": tokenData.ExpiresAt, |
| "auth_method": tokenData.AuthMethod, |
| "provider": tokenData.Provider, |
| "email": tokenData.Email, |
| }, |
| Attributes: map[string]string{ |
| "profile_arn": tokenData.ProfileArn, |
| "source": "github-oauth", |
| "email": tokenData.Email, |
| }, |
| |
| NextRefreshAfter: expiresAt.Add(-5 * time.Minute), |
| } |
|
|
| if tokenData.Email != "" { |
| fmt.Printf("\n✓ Kiro GitHub authentication completed successfully! (Account: %s)\n", tokenData.Email) |
| } else { |
| fmt.Println("\n✓ Kiro GitHub authentication completed successfully!") |
| } |
|
|
| return record, nil |
| } |
|
|
| |
| func (a *KiroAuthenticator) ImportFromKiroIDE(ctx context.Context, cfg *config.Config) (*coreauth.Auth, error) { |
| tokenData, err := kiroauth.LoadKiroIDEToken() |
| if err != nil { |
| return nil, fmt.Errorf("failed to load Kiro IDE token: %w", err) |
| } |
|
|
| |
| expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt) |
| if err != nil { |
| expiresAt = time.Now().Add(1 * time.Hour) |
| } |
|
|
| |
| if tokenData.Email == "" { |
| tokenData.Email = kiroauth.ExtractEmailFromJWT(tokenData.AccessToken) |
| } |
|
|
| |
| idPart := extractKiroIdentifier(tokenData.Email, tokenData.ProfileArn) |
| |
| provider := kiroauth.SanitizeEmailForFilename(strings.ToLower(strings.TrimSpace(tokenData.Provider))) |
| if provider == "" { |
| provider = "imported" |
| } |
|
|
| now := time.Now() |
| fileName := fmt.Sprintf("kiro-%s-%s.json", provider, idPart) |
|
|
| record := &coreauth.Auth{ |
| ID: fileName, |
| Provider: "kiro", |
| FileName: fileName, |
| Label: fmt.Sprintf("kiro-%s", provider), |
| Status: coreauth.StatusActive, |
| CreatedAt: now, |
| UpdatedAt: now, |
| Metadata: map[string]any{ |
| "type": "kiro", |
| "access_token": tokenData.AccessToken, |
| "refresh_token": tokenData.RefreshToken, |
| "profile_arn": tokenData.ProfileArn, |
| "expires_at": tokenData.ExpiresAt, |
| "auth_method": tokenData.AuthMethod, |
| "provider": tokenData.Provider, |
| "email": tokenData.Email, |
| }, |
| Attributes: map[string]string{ |
| "profile_arn": tokenData.ProfileArn, |
| "source": "kiro-ide-import", |
| "email": tokenData.Email, |
| }, |
| |
| NextRefreshAfter: expiresAt.Add(-5 * time.Minute), |
| } |
|
|
| |
| if tokenData.Email != "" { |
| fmt.Printf("\n✓ Imported Kiro token from IDE (Provider: %s, Account: %s)\n", tokenData.Provider, tokenData.Email) |
| } else { |
| fmt.Printf("\n✓ Imported Kiro token from IDE (Provider: %s)\n", tokenData.Provider) |
| } |
|
|
| return record, nil |
| } |
|
|
| |
| func (a *KiroAuthenticator) Refresh(ctx context.Context, cfg *config.Config, auth *coreauth.Auth) (*coreauth.Auth, error) { |
| if auth == nil || auth.Metadata == nil { |
| return nil, fmt.Errorf("invalid auth record") |
| } |
|
|
| refreshToken, ok := auth.Metadata["refresh_token"].(string) |
| if !ok || refreshToken == "" { |
| return nil, fmt.Errorf("refresh token not found") |
| } |
|
|
| clientID, _ := auth.Metadata["client_id"].(string) |
| clientSecret, _ := auth.Metadata["client_secret"].(string) |
| authMethod, _ := auth.Metadata["auth_method"].(string) |
| startURL, _ := auth.Metadata["start_url"].(string) |
| region, _ := auth.Metadata["region"].(string) |
|
|
| var tokenData *kiroauth.KiroTokenData |
| var err error |
|
|
| ssoClient := kiroauth.NewSSOOIDCClient(cfg) |
|
|
| |
| switch { |
| case clientID != "" && clientSecret != "" && authMethod == "idc" && region != "": |
| |
| tokenData, err = ssoClient.RefreshTokenWithRegion(ctx, clientID, clientSecret, refreshToken, region, startURL) |
| case clientID != "" && clientSecret != "" && authMethod == "builder-id": |
| |
| tokenData, err = ssoClient.RefreshToken(ctx, clientID, clientSecret, refreshToken) |
| default: |
| |
| oauth := kiroauth.NewKiroOAuth(cfg) |
| tokenData, err = oauth.RefreshToken(ctx, refreshToken) |
| } |
|
|
| if err != nil { |
| return nil, fmt.Errorf("token refresh failed: %w", err) |
| } |
|
|
| |
| expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt) |
| if err != nil { |
| expiresAt = time.Now().Add(1 * time.Hour) |
| } |
|
|
| |
| updated := auth.Clone() |
| now := time.Now() |
| updated.UpdatedAt = now |
| updated.LastRefreshedAt = now |
| updated.Metadata["access_token"] = tokenData.AccessToken |
| updated.Metadata["refresh_token"] = tokenData.RefreshToken |
| updated.Metadata["expires_at"] = tokenData.ExpiresAt |
| updated.Metadata["last_refresh"] = now.Format(time.RFC3339) |
| |
| updated.NextRefreshAfter = expiresAt.Add(-5 * time.Minute) |
|
|
| return updated, nil |
| } |
|
|