Skip to main content

calimero_client/
auth.rs

1//! Authentication implementations for Calimero client
2//!
3//! This module provides various authentication implementations including
4//! CLI-based authentication and other authentication methods.
5
6// Standard library
7use std::io::{self, Write};
8
9// External crates
10use async_trait::async_trait;
11use eyre::Result;
12use url::Url;
13use webbrowser;
14
15// Local crate
16use crate::storage::JwtToken;
17use crate::traits::ClientAuthenticator;
18
19/// CLI-specific implementation of ClientAuthenticator
20///
21/// This authenticator is designed for command-line interfaces and provides
22/// browser-based authentication flows suitable for interactive use.
23pub struct CliAuthenticator {
24    /// Output handler for user interaction
25    output: Box<dyn OutputHandler + Send + Sync>,
26}
27
28impl std::fmt::Debug for CliAuthenticator {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("CliAuthenticator")
31            .field("output", &"<dyn OutputHandler>")
32            .finish()
33    }
34}
35
36impl Clone for CliAuthenticator {
37    fn clone(&self) -> Self {
38        // Create a new authenticator with console output
39        Self::new()
40    }
41}
42
43/// Trait for handling output during authentication
44pub trait OutputHandler: Send + Sync {
45    /// Display a message to the user
46    fn display_message(&self, message: &str);
47
48    /// Display an error message
49    fn display_error(&self, error: &str);
50
51    /// Display success message
52    fn display_success(&self, message: &str);
53
54    /// Open a URL in the default browser
55    fn open_browser(&self, url: &Url) -> Result<()>;
56
57    /// Wait for user input
58    fn wait_for_input(&self, prompt: &str) -> Result<String>;
59}
60
61/// Simple console output handler
62#[derive(Debug, Clone, Copy)]
63pub struct ConsoleOutputHandler;
64
65impl OutputHandler for ConsoleOutputHandler {
66    fn display_message(&self, message: &str) {
67        println!("{}", message);
68    }
69
70    fn display_error(&self, error: &str) {
71        eprintln!("Error: {}", error);
72    }
73
74    fn display_success(&self, message: &str) {
75        println!("✓ {}", message);
76    }
77
78    fn open_browser(&self, url: &Url) -> Result<()> {
79        webbrowser::open(url.as_str())?;
80        Ok(())
81    }
82
83    fn wait_for_input(&self, prompt: &str) -> Result<String> {
84        print!("{}", prompt);
85        io::stdout().flush()?;
86
87        let mut input = String::new();
88        let _ = io::stdin().read_line(&mut input)?;
89
90        Ok(input.trim().to_owned())
91    }
92}
93
94impl CliAuthenticator {
95    /// Create a new CLI authenticator with console output
96    pub fn new() -> Self {
97        Self {
98            output: Box::new(ConsoleOutputHandler),
99        }
100    }
101
102    /// Create a new CLI authenticator with custom output handler
103    pub fn with_output(output: Box<dyn OutputHandler + Send + Sync>) -> Self {
104        Self { output }
105    }
106
107    /// Get the output handler
108    pub fn output(&self) -> &dyn OutputHandler {
109        self.output.as_ref()
110    }
111}
112
113#[async_trait]
114impl ClientAuthenticator for CliAuthenticator {
115    async fn authenticate(&self, api_url: &Url) -> Result<JwtToken> {
116        self.output.display_message("Starting authentication...");
117
118        // For now, this is a placeholder implementation
119        // In a real implementation, this would:
120        // 1. Check if authentication is required
121        // 2. Open browser for OAuth flow
122        // 3. Handle the callback
123        // 4. Return the tokens
124
125        self.output
126            .display_message(&format!("Please authenticate at: {}", api_url));
127
128        // Simulate authentication process
129        let access_token = self.output.wait_for_input("Enter access token: ")?;
130
131        if access_token.is_empty() {
132            return Err(eyre::eyre!("Access token cannot be empty"));
133        }
134
135        let refresh_token = self
136            .output
137            .wait_for_input("Enter refresh token (optional): ")?;
138
139        let token = if refresh_token.is_empty() {
140            JwtToken::new(access_token)
141        } else {
142            JwtToken::with_refresh(access_token, refresh_token)
143        };
144
145        self.output.display_success("Authentication successful!");
146        Ok(token)
147    }
148
149    async fn refresh_tokens(&self, _refresh_token: &str) -> Result<JwtToken> {
150        self.output
151            .display_message("Refreshing authentication tokens...");
152
153        // For now, this is a placeholder implementation
154        // In a real implementation, this would:
155        // 1. Send refresh token to the API
156        // 2. Receive new access token
157        // 3. Return the new tokens
158
159        self.output
160            .display_message("Please provide new access token:");
161        let access_token = self.output.wait_for_input("Enter new access token: ")?;
162
163        if access_token.is_empty() {
164            return Err(eyre::eyre!("Access token cannot be empty"));
165        }
166
167        let token = JwtToken::new(access_token);
168        self.output.display_success("Token refresh successful!");
169        Ok(token)
170    }
171
172    async fn handle_auth_failure(&self, api_url: &Url) -> Result<JwtToken> {
173        self.output
174            .display_error("Authentication failed. Please try again.");
175
176        // Try to open the authentication URL in the browser
177        if let Err(e) = self.output.open_browser(api_url) {
178            self.output
179                .display_error(&format!("Failed to open browser: {}", e));
180            self.output
181                .display_message(&format!("Please manually visit: {}", api_url));
182        }
183
184        // Wait for user to complete authentication
185        self.output
186            .display_message("Please complete authentication in your browser, then press Enter.");
187        drop(self.output.wait_for_input("Press Enter when done: ")?);
188
189        // Try to authenticate again
190        self.authenticate(api_url).await
191    }
192
193    async fn check_auth_required(&self, _api_url: &Url) -> Result<bool> {
194        // For now, assume all APIs require authentication
195        // In a real implementation, this would check the API health endpoint
196        Ok(true)
197    }
198
199    fn get_auth_method(&self) -> &'static str {
200        "CLI Browser-based OAuth"
201    }
202
203    fn supports_refresh(&self) -> bool {
204        true
205    }
206}
207
208/// Headless authenticator for non-interactive environments
209#[derive(Debug, Clone)]
210pub struct HeadlessAuthenticator {
211    /// Pre-configured tokens
212    tokens: Option<JwtToken>,
213}
214
215impl HeadlessAuthenticator {
216    /// Create a new headless authenticator
217    pub fn new() -> Self {
218        Self { tokens: None }
219    }
220
221    /// Create a new headless authenticator with pre-configured tokens
222    pub fn with_tokens(tokens: JwtToken) -> Self {
223        Self {
224            tokens: Some(tokens),
225        }
226    }
227
228    /// Set tokens for the authenticator
229    pub fn set_tokens(&mut self, tokens: JwtToken) {
230        self.tokens = Some(tokens);
231    }
232}
233
234#[async_trait]
235impl ClientAuthenticator for HeadlessAuthenticator {
236    async fn authenticate(&self, _api_url: &Url) -> Result<JwtToken> {
237        if let Some(tokens) = &self.tokens {
238            Ok(tokens.clone())
239        } else {
240            Err(eyre::eyre!(
241                "No tokens configured for headless authenticator"
242            ))
243        }
244    }
245
246    async fn refresh_tokens(&self, _refresh_token: &str) -> Result<JwtToken> {
247        Err(eyre::eyre!("Token refresh not supported in headless mode"))
248    }
249
250    async fn handle_auth_failure(&self, _api_url: &Url) -> Result<JwtToken> {
251        Err(eyre::eyre!(
252            "Cannot handle authentication failure in headless mode"
253        ))
254    }
255
256    async fn check_auth_required(&self, _api_url: &Url) -> Result<bool> {
257        Ok(true)
258    }
259
260    fn get_auth_method(&self) -> &'static str {
261        "Headless Pre-configured Tokens"
262    }
263
264    fn supports_refresh(&self) -> bool {
265        false
266    }
267}
268
269/// API key authenticator for simple API key authentication
270#[derive(Debug, Clone)]
271pub struct ApiKeyAuthenticator {
272    /// API key for authentication
273    api_key: String,
274}
275
276impl ApiKeyAuthenticator {
277    /// Create a new API key authenticator
278    pub fn new(api_key: String) -> Self {
279        Self { api_key }
280    }
281
282    /// Get the API key
283    pub fn api_key(&self) -> &str {
284        &self.api_key
285    }
286}
287
288#[async_trait]
289impl ClientAuthenticator for ApiKeyAuthenticator {
290    async fn authenticate(&self, _api_url: &Url) -> Result<JwtToken> {
291        // For API key auth, we create a simple token
292        let token = JwtToken::new(self.api_key.clone())
293            .with_metadata("auth_type".to_owned(), serde_json::json!("api_key"));
294        Ok(token)
295    }
296
297    async fn refresh_tokens(&self, _refresh_token: &str) -> Result<JwtToken> {
298        // API keys don't refresh, just return the same
299        Err(eyre::eyre!("API keys do not support token refresh"))
300    }
301
302    async fn handle_auth_failure(&self, _api_url: &Url) -> Result<JwtToken> {
303        Err(eyre::eyre!("API key authentication failed"))
304    }
305
306    async fn check_auth_required(&self, _api_url: &Url) -> Result<bool> {
307        Ok(true)
308    }
309
310    fn get_auth_method(&self) -> &'static str {
311        "API Key"
312    }
313
314    fn supports_refresh(&self) -> bool {
315        false
316    }
317}
318
319/// Trait for meroctl output handling during authentication
320pub trait MeroctlOutputHandler: Send + Sync {
321    /// Display a message to the user
322    fn display_message(&self, message: &str);
323
324    /// Display an error message
325    fn display_error(&self, error: &str);
326
327    /// Display success message
328    fn display_success(&self, message: &str);
329
330    /// Open a URL in the default browser
331    fn open_browser(&self, url: &Url) -> Result<()>;
332
333    /// Wait for user input
334    fn wait_for_input(&self, prompt: &str) -> Result<String>;
335}