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!("   Token starts with: {}", &api_key.chars().take(5).collect::<String>());
49        }
50
51        let credential = Credential::ApiKey {
52            username: username.clone(),
53            api_key,
54        };
55
56        // Validate credentials by making a test API call
57        Self::validate_credentials(&credential).await?;
58
59        // Store credentials
60        auth_manager.store_credentials(&credential)?;
61
62        println!("\nāœ… Successfully authenticated as {}", username);
63        println!("šŸ’” Tip: Use 'bitbucket auth login --oauth' for a better experience");
64
65        Ok(credential)
66    }
67
68    /// Validate credentials against the Bitbucket API
69    async fn validate_credentials(credential: &Credential) -> Result<()> {
70        let client = reqwest::Client::new();
71
72        println!("šŸ” Validating credentials with Bitbucket API...");
73
74        let response = client
75            .get("https://api.bitbucket.org/2.0/user")
76            .header("Authorization", credential.auth_header())
77            .header("User-Agent", "bitbucket-cli/0.3.0")
78            .send()
79            .await
80            .context("Failed to connect to Bitbucket API")?;
81
82        let status = response.status();
83        
84        if status.is_success() {
85            Ok(())
86        } else if status == reqwest::StatusCode::UNAUTHORIZED {
87            anyhow::bail!(
88                "Authentication failed (401 Unauthorized).\n\n\
89                Possible causes:\n\
90                - Incorrect username\n\
91                - Invalid or expired API token\n\
92                - Token doesn't have required permissions\n\n\
93                Please verify:\n\
94                1. Your Bitbucket username is correct\n\
95                2. Your API token is copied completely (should start with 'ATATT' or 'ATCTT')\n\
96                3. Token has 'Read' permission at minimum"
97            )
98        } else {
99            let body = response.text().await.unwrap_or_else(|_| String::from("<unable to read response>"));
100            anyhow::bail!(
101                "API error ({}):\n{}\n\n\
102                This might indicate:\n\
103                - Network connectivity issues\n\
104                - Bitbucket API is unavailable\n\
105                - Rate limiting",
106                status, body
107            )
108        }
109    }
110}