bitbucket_cli/auth/
api_key.rs

1use anyhow::{Context, Result};
2use dialoguer::{Input, Password};
3
4use super::{AuthManager, Credential};
5
6/// API key authentication flow (fallback method)
7/// Note: Atlassian has deprecated app passwords in favor of OAuth2
8pub struct ApiKeyAuth;
9
10impl ApiKeyAuth {
11    /// Run the interactive API key authentication flow
12    pub async fn authenticate(auth_manager: &AuthManager) -> Result<Credential> {
13        println!("\nšŸ” Bitbucket API Key Authentication");
14        println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
15        println!();
16        println!("āš ļø  Note: OAuth 2.0 is the preferred authentication method.");
17        println!("   API keys are provided for automation/CI scenarios.");
18        println!();
19        println!("To create an API key (HTTP access token):");
20        println!("1. Go to Bitbucket Settings → Personal settings");
21        println!("2. Click 'HTTP access tokens' under 'Access management'");
22        println!("3. Click 'Create token'");
23        println!("4. Give it a label and select required permissions");
24        println!();
25
26        let username: String = Input::new()
27            .with_prompt("Bitbucket username")
28            .interact_text()
29            .context("Failed to read username")?;
30
31        let api_key: String = Password::new()
32            .with_prompt("API key (HTTP access token)")
33            .interact()
34            .context("Failed to read API key")?;
35
36        // Trim whitespace from token (common copy-paste issue)
37        let api_key = api_key.trim().to_string();
38
39        // Validate token format
40        if api_key.is_empty() {
41            anyhow::bail!("API key cannot be empty");
42        }
43
44        // Check for common Atlassian token prefixes
45        if !api_key.starts_with("ATATT") && !api_key.starts_with("ATCTT") {
46            println!("āš ļø  Warning: Token doesn't start with expected prefix (ATATT or ATCTT)");
47            println!("   This might not be a valid Bitbucket API token.");
48            println!(
49                "   Token starts with: {}",
50                &api_key.chars().take(5).collect::<String>()
51            );
52        }
53
54        let credential = Credential::ApiKey {
55            username: username.clone(),
56            api_key,
57        };
58
59        // Validate credentials by making a test API call
60        Self::validate_credentials(&credential).await?;
61
62        // Store credentials
63        auth_manager.store_credentials(&credential)?;
64
65        println!("\nāœ… Successfully authenticated as {}", username);
66        println!("šŸ’” Tip: Use 'bitbucket auth login --oauth' for a better experience");
67
68        Ok(credential)
69    }
70
71    /// Validate credentials against the Bitbucket API
72    async fn validate_credentials(credential: &Credential) -> Result<()> {
73        let client = reqwest::Client::new();
74
75        println!("šŸ” Validating credentials with Bitbucket API...");
76
77        let response = client
78            .get("https://api.bitbucket.org/2.0/user")
79            .header("Authorization", credential.auth_header())
80            .header("User-Agent", "bitbucket-cli/0.3.0")
81            .send()
82            .await
83            .context("Failed to connect to Bitbucket API")?;
84
85        let status = response.status();
86
87        if status.is_success() {
88            Ok(())
89        } else if status == reqwest::StatusCode::UNAUTHORIZED {
90            anyhow::bail!(
91                "Authentication failed (401 Unauthorized).\n\n\
92                Possible causes:\n\
93                - Incorrect username\n\
94                - Invalid or expired API token\n\
95                - Token doesn't have required permissions\n\n\
96                Please verify:\n\
97                1. Your Bitbucket username is correct\n\
98                2. Your API token is copied completely (should start with 'ATATT' or 'ATCTT')\n\
99                3. Token has 'Read' permission at minimum"
100            )
101        } else {
102            let body = response
103                .text()
104                .await
105                .unwrap_or_else(|_| String::from("<unable to read response>"));
106            anyhow::bail!(
107                "API error ({}):\n{}\n\n\
108                This might indicate:\n\
109                - Network connectivity issues\n\
110                - Bitbucket API is unavailable\n\
111                - Rate limiting",
112                status,
113                body
114            )
115        }
116    }
117}