Skip to main content

bitbucket_cli/cli/
auth.rs

1use anyhow::Result;
2use clap::Subcommand;
3use colored::Colorize;
4use dialoguer::{Confirm, Input};
5
6use crate::auth::{ApiKeyAuth, AuthManager, OAuthFlow};
7use crate::config::Config;
8
9#[derive(Subcommand)]
10pub enum AuthCommands {
11    /// Authenticate with Bitbucket (OAuth 2.0 preferred)
12    Login {
13        /// Use OAuth 2.0 authentication (recommended)
14        #[arg(long)]
15        oauth: bool,
16
17        /// Use API key authentication (for automation/CI)
18        #[arg(long)]
19        api_key: bool,
20
21        /// OAuth Client ID (required for OAuth)
22        #[arg(long, env = "BITBUCKET_CLIENT_ID")]
23        client_id: Option<String>,
24
25        /// OAuth Client Secret (required for OAuth)
26        #[arg(long, env = "BITBUCKET_CLIENT_SECRET")]
27        client_secret: Option<String>,
28    },
29
30    /// Remove stored credentials
31    Logout,
32
33    /// Show authentication status
34    Status,
35}
36
37impl AuthCommands {
38    pub async fn run(self) -> Result<()> {
39        match self {
40            AuthCommands::Login {
41                oauth,
42                api_key,
43                client_id,
44                client_secret,
45            } => {
46                let auth_manager = AuthManager::new()?;
47
48                // Determine authentication method
49                let use_oauth = if oauth || api_key {
50                    // User explicitly chose a method
51                    oauth
52                } else {
53                    // Interactive prompt - prefer OAuth
54                    println!("\nšŸ” Bitbucket CLI Authentication");
55                    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
56                    println!();
57                    println!("Choose authentication method:");
58                    println!();
59                    println!("  1. {} (Recommended)", "OAuth 2.0".green().bold());
60                    println!("     • More secure with token refresh");
61                    println!("     • Better user experience");
62                    println!("     • Requires OAuth app setup");
63                    println!();
64                    println!("  2. {} (Fallback)", "API Key".yellow());
65                    println!("     • For automation/CI pipelines");
66                    println!("     • Requires HTTP access token");
67                    println!("     • No automatic refresh");
68                    println!();
69
70                    Confirm::new()
71                        .with_prompt("Use OAuth 2.0?")
72                        .default(true)
73                        .interact()?
74                };
75
76                let credential = if use_oauth {
77                    // OAuth flow — resolve consumer credentials from (in priority):
78                    // 1. CLI flags
79                    // 2. Environment variables
80                    // 3. Previously stored credentials
81                    // 4. Interactive prompt (first-time only)
82                    let stored_consumer = auth_manager
83                        .get_credentials()
84                        .ok()
85                        .flatten()
86                        .and_then(|c| {
87                            c.oauth_consumer_credentials()
88                                .map(|(id, secret)| (id.to_owned(), secret.to_owned()))
89                        });
90
91                    let client_id = client_id
92                        .or_else(|| std::env::var("BITBUCKET_CLIENT_ID").ok())
93                        .or_else(|| stored_consumer.as_ref().map(|(id, _)| id.clone()))
94                        .or_else(|| {
95                            println!();
96                            println!("šŸ“‹ OAuth Consumer Setup Required");
97                            println!();
98                            println!("To use OAuth authentication, you need to create an OAuth consumer:");
99                            println!("1. Go to: https://bitbucket.org/[workspace]/workspace/settings/oauth-consumers/new");
100                            println!("2. Set callback URL to ONE of these (pick any available port):");
101                            println!("   • http://127.0.0.1:8080/callback");
102                            println!("   • http://127.0.0.1:3000/callback");
103                            println!("   • http://127.0.0.1:8888/callback");
104                            println!("   • http://127.0.0.1:9000/callback");
105                            println!("3. Select required permissions:");
106                            println!("   āœ“ Account (Read)");
107                            println!("   āœ“ Repositories (Read)");
108                            println!("   āœ“ Pull requests (Read, Write)");
109                            println!("   āœ“ Issues (Read, Write)");
110                            println!("   āœ“ Pipelines (Read, Write)");
111                           println!("4. Copy the Key (Client ID) and Secret");
112                           println!();
113
114                           Input::<String>::new()
115                                .with_prompt("OAuth Client ID (Key)")
116                                .interact_text()
117                                .ok()
118                        })
119                        .ok_or_else(|| anyhow::anyhow!("OAuth Client ID is required"))?;
120
121                    let client_secret = client_secret
122                        .or_else(|| std::env::var("BITBUCKET_CLIENT_SECRET").ok())
123                        .or_else(|| stored_consumer.map(|(_, secret)| secret))
124                        .or_else(|| {
125                            Input::<String>::new()
126                                .with_prompt("OAuth Client Secret")
127                                .interact_text()
128                                .ok()
129                        })
130                        .ok_or_else(|| anyhow::anyhow!("OAuth Client Secret is required"))?;
131
132                    let oauth = OAuthFlow::new(client_id, client_secret);
133                    oauth.authenticate(&auth_manager).await?
134                } else {
135                    // API Key flow
136                    ApiKeyAuth::authenticate(&auth_manager).await?
137                };
138
139                // Save username to config if available
140                if let Some(username) = credential.username() {
141                    let mut config = Config::load()?;
142                    config.set_username(username);
143                    config.save()?;
144                }
145
146                Ok(())
147            }
148
149            AuthCommands::Logout => {
150                let auth_manager = AuthManager::new()?;
151                auth_manager.clear_credentials()?;
152
153                let mut config = Config::load()?;
154                config.clear_auth();
155                config.save()?;
156
157                println!("{} Logged out successfully", "āœ“".green());
158                Ok(())
159            }
160
161            AuthCommands::Status => {
162                let auth_manager = AuthManager::new()?;
163                let config = Config::load()?;
164
165                if auth_manager.is_authenticated() {
166                    println!("{} Authenticated", "āœ“".green());
167
168                    // Show credential type
169                    if let Ok(Some(credential)) = auth_manager.get_credentials() {
170                        println!("  {} {}", "Method:".dimmed(), credential.type_name());
171
172                        if credential.is_oauth() && credential.needs_refresh() {
173                            println!(
174                                "  {} {}",
175                                "Status:".dimmed(),
176                                "Token needs refresh".yellow()
177                            );
178                        }
179                    }
180
181                    if let Some(username) = config.username() {
182                        println!("  {} {}", "Username:".dimmed(), username);
183                    }
184
185                    if let Some(workspace) = config.default_workspace() {
186                        println!("  {} {}", "Workspace:".dimmed(), workspace);
187                    }
188
189                    // Test the credentials
190                    match crate::api::BitbucketClient::from_stored().await {
191                        Ok(client) => match client.get::<serde_json::Value>("/user").await {
192                            Ok(user) => {
193                                if let Some(display_name) = user.get("display_name") {
194                                    println!(
195                                        "  {} {}",
196                                        "Display name:".dimmed(),
197                                        display_name.as_str().unwrap_or("Unknown")
198                                    );
199                                }
200                            }
201                            Err(e) => {
202                                println!("{} Credentials may be invalid: {}", "⚠".yellow(), e);
203                            }
204                        },
205                        Err(e) => {
206                            println!("{} Failed to create client: {}", "āœ—".red(), e);
207                        }
208                    }
209                } else {
210                    println!("{} Not authenticated", "āœ—".red());
211                    println!();
212                    println!("Run {} to authenticate", "bitbucket auth login".cyan());
213                    println!();
214                    println!("šŸ’” Tip: Use {} for the best experience", "--oauth".green());
215                }
216
217                Ok(())
218            }
219        }
220    }
221}