clawspec_core/client/
auth.rs

1use std::fmt;
2
3use http::HeaderValue;
4use reqwest::header::{AUTHORIZATION, HeaderName};
5use serde::{Deserialize, Serialize};
6use zeroize::{Zeroize, ZeroizeOnDrop};
7
8/// Errors that can occur during authentication processing.
9///
10/// This enum provides granular error information for authentication-related failures,
11/// allowing for more specific error handling and better debugging.
12#[derive(Debug, Clone, PartialEq, Eq, derive_more::Error, derive_more::Display)]
13pub enum AuthenticationError {
14    /// Bearer token contains invalid characters for HTTP headers.
15    #[display("Bearer token contains invalid characters: {message}")]
16    InvalidBearerToken {
17        /// Description of the invalid characters or format issue.
18        message: String,
19    },
20
21    /// Basic authentication username contains invalid characters.
22    #[display("Basic auth username contains invalid characters: {message}")]
23    InvalidUsername {
24        /// Description of the invalid characters or format issue.
25        message: String,
26    },
27
28    /// Basic authentication password contains invalid characters.
29    #[display("Basic auth password contains invalid characters: {message}")]
30    InvalidPassword {
31        /// Description of the invalid characters or format issue.
32        message: String,
33    },
34
35    /// API key header name is invalid.
36    #[display("Invalid API key header name '{header_name}': {message}")]
37    InvalidHeaderName {
38        /// The invalid header name that was provided.
39        header_name: String,
40        /// Description of why the header name is invalid.
41        message: String,
42    },
43
44    /// API key value contains invalid characters for HTTP headers.
45    #[display("API key contains invalid characters: {message}")]
46    InvalidApiKey {
47        /// Description of the invalid characters or format issue.
48        message: String,
49    },
50
51    /// Base64 encoding failed during Basic authentication processing.
52    #[display("Base64 encoding failed: {message}")]
53    EncodingError {
54        /// Description of the encoding failure.
55        message: String,
56    },
57}
58
59/// Secure wrapper for sensitive string data that automatically zeroes memory on drop.
60///
61/// This wrapper ensures that sensitive authentication data is securely cleared from memory
62/// when it's no longer needed, providing protection against memory inspection attacks.
63#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
64pub struct SecureString(String);
65
66impl SecureString {
67    /// Creates a new secure string from the provided value.
68    pub fn new(value: String) -> Self {
69        Self(value)
70    }
71
72    /// Returns a reference to the inner string value.
73    ///
74    /// # Security Note
75    /// The returned reference should not be stored for extended periods
76    /// to minimize exposure time of sensitive data.
77    pub fn as_str(&self) -> &str {
78        &self.0
79    }
80
81    /// Consumes the SecureString and returns the inner String.
82    ///
83    /// # Security Note
84    /// The caller becomes responsible for the secure handling of the returned String.
85    pub fn into_string(mut self) -> String {
86        // Clear the original before returning
87        std::mem::take(&mut self.0)
88    }
89
90    /// Checks if the secure string equals the given string slice.
91    ///
92    /// This method is provided for convenient testing and comparison without
93    /// exposing the internal string value.
94    pub fn equals_str(&self, other: &str) -> bool {
95        self.0 == other
96    }
97}
98
99impl fmt::Debug for SecureString {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.debug_struct("SecureString")
102            .field("value", &"[REDACTED]")
103            .finish()
104    }
105}
106
107impl fmt::Display for SecureString {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "{}", Self::mask_sensitive(&self.0))
110    }
111}
112
113impl From<String> for SecureString {
114    fn from(value: String) -> Self {
115        Self::new(value)
116    }
117}
118
119impl From<&str> for SecureString {
120    fn from(value: &str) -> Self {
121        Self::new(value.to_string())
122    }
123}
124
125impl Serialize for SecureString {
126    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
127    where
128        S: serde::Serializer,
129    {
130        self.0.serialize(serializer)
131    }
132}
133
134impl<'de> Deserialize<'de> for SecureString {
135    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136    where
137        D: serde::Deserializer<'de>,
138    {
139        String::deserialize(deserializer).map(Self::new)
140    }
141}
142
143impl SecureString {
144    /// Masks sensitive data for display/logging purposes.
145    fn mask_sensitive(value: &str) -> String {
146        if value.len() <= 8 {
147            "***".to_string()
148        } else {
149            format!("{}...{}", &value[..4], &value[value.len() - 4..])
150        }
151    }
152}
153
154/// Authentication configuration for API requests.
155///
156/// This enum supports various authentication methods commonly used in APIs.
157/// Authentication can be configured at the client level and optionally overridden
158/// for individual requests.
159///
160/// # Security Features
161///
162/// - **Memory Protection**: Sensitive data is automatically cleared from memory when dropped
163/// - **Display Masking**: Credentials are never displayed in full for logging safety
164/// - **Debug Safety**: Authentication data is redacted in debug output
165///
166/// # Examples
167///
168/// ```rust
169/// use clawspec_core::Authentication;
170///
171/// // Bearer token authentication
172/// let auth = Authentication::Bearer("my-api-token".into());
173///
174/// // Basic authentication
175/// let auth = Authentication::Basic {
176///     username: "user".to_string(),
177///     password: "pass".into(),
178/// };
179///
180/// // API key in header
181/// let auth = Authentication::ApiKey {
182///     header_name: "X-API-Key".to_string(),
183///     key: "secret-key".into(),
184/// };
185/// ```
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "snake_case")]
188pub enum Authentication {
189    /// Bearer token authentication (RFC 6750)
190    /// Adds `Authorization: Bearer <token>` header
191    Bearer(SecureString),
192
193    /// HTTP Basic authentication (RFC 7617)
194    /// Adds `Authorization: Basic <base64(username:password)>` header
195    Basic {
196        username: String,
197        password: SecureString,
198    },
199
200    /// API key authentication with custom header
201    /// Adds `<header_name>: <key>` header
202    ApiKey {
203        header_name: String,
204        key: SecureString,
205    },
206}
207
208impl Authentication {
209    /// Converts the authentication into HTTP headers.
210    ///
211    /// Returns a tuple of (HeaderName, HeaderValue) that can be added to the request.
212    ///
213    /// # Errors
214    ///
215    /// Returns `AuthenticationError` if the authentication data contains invalid characters
216    /// or cannot be properly formatted for HTTP headers.
217    pub fn to_header(&self) -> Result<(HeaderName, HeaderValue), AuthenticationError> {
218        match self {
219            Authentication::Bearer(token) => {
220                let header_value = format!("Bearer {}", token.as_str());
221                let value = HeaderValue::from_str(&header_value).map_err(|e| {
222                    AuthenticationError::InvalidBearerToken {
223                        message: e.to_string(),
224                    }
225                })?;
226                Ok((AUTHORIZATION, value))
227            }
228
229            Authentication::Basic { username, password } => {
230                // Validate username doesn't contain invalid characters
231                if username.contains(':') {
232                    return Err(AuthenticationError::InvalidUsername {
233                        message: "Username cannot contain colon (:) character".to_string(),
234                    });
235                }
236
237                use base64::Engine;
238                let credentials_str = format!("{}:{}", username, password.as_str());
239                let credentials = base64::engine::general_purpose::STANDARD.encode(credentials_str);
240
241                let header_value = format!("Basic {credentials}");
242                let value = HeaderValue::from_str(&header_value).map_err(|e| {
243                    AuthenticationError::InvalidPassword {
244                        message: e.to_string(),
245                    }
246                })?;
247                Ok((AUTHORIZATION, value))
248            }
249
250            Authentication::ApiKey { header_name, key } => {
251                let header = HeaderName::from_bytes(header_name.as_bytes()).map_err(|e| {
252                    AuthenticationError::InvalidHeaderName {
253                        header_name: header_name.clone(),
254                        message: e.to_string(),
255                    }
256                })?;
257                let value = HeaderValue::from_str(key.as_str()).map_err(|e| {
258                    AuthenticationError::InvalidApiKey {
259                        message: e.to_string(),
260                    }
261                })?;
262                Ok((header, value))
263            }
264        }
265    }
266}
267
268impl fmt::Display for Authentication {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        match self {
271            Authentication::Bearer(token) => {
272                write!(f, "Bearer {token}")
273            }
274            Authentication::Basic { username, .. } => write!(f, "Basic (username: {username})"),
275            Authentication::ApiKey { header_name, key } => {
276                write!(f, "ApiKey ({header_name}: {key})")
277            }
278        }
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_bearer_authentication() {
288        let auth = Authentication::Bearer("my-secret-token".into());
289        let (header_name, header_value) = auth.to_header().unwrap();
290
291        assert_eq!(header_name, AUTHORIZATION);
292        assert_eq!(header_value, "Bearer my-secret-token");
293    }
294
295    #[test]
296    fn test_basic_authentication() {
297        let auth = Authentication::Basic {
298            username: "user".to_string(),
299            password: "pass".into(),
300        };
301        let (header_name, header_value) = auth.to_header().unwrap();
302
303        assert_eq!(header_name, AUTHORIZATION);
304        // "user:pass" base64 encoded is "dXNlcjpwYXNz"
305        assert_eq!(header_value, "Basic dXNlcjpwYXNz");
306    }
307
308    #[test]
309    fn test_api_key_authentication() {
310        let auth = Authentication::ApiKey {
311            header_name: "X-API-Key".to_string(),
312            key: "secret-key-123".into(),
313        };
314        let (header_name, header_value) = auth.to_header().unwrap();
315
316        assert_eq!(header_name, "X-API-Key");
317        assert_eq!(header_value, "secret-key-123");
318    }
319
320    #[test]
321    fn test_display_masks_secrets() {
322        let auth = Authentication::Bearer("very-secret-token-12345".into());
323        assert_eq!(auth.to_string(), "Bearer very...2345");
324
325        let auth = Authentication::Basic {
326            username: "user".to_string(),
327            password: "password".into(),
328        };
329        assert_eq!(auth.to_string(), "Basic (username: user)");
330
331        let auth = Authentication::ApiKey {
332            header_name: "X-API-Key".to_string(),
333            key: "secret-key-12345".into(),
334        };
335        assert_eq!(auth.to_string(), "ApiKey (X-API-Key: secr...2345)");
336    }
337
338    #[test]
339    fn test_secure_string_mask_short_tokens() {
340        assert_eq!(SecureString::mask_sensitive("short"), "***");
341        assert_eq!(SecureString::mask_sensitive("12345678"), "***");
342        assert_eq!(SecureString::mask_sensitive("123456789"), "1234...6789");
343    }
344
345    #[test]
346    fn test_serialization() {
347        let auth = Authentication::Bearer("token".into());
348        let json = serde_json::to_string(&auth).unwrap();
349        assert_eq!(json, r#"{"bearer":"token"}"#);
350
351        let auth = Authentication::Basic {
352            username: "user".to_string(),
353            password: "pass".into(),
354        };
355        let json = serde_json::to_string(&auth).unwrap();
356        assert_eq!(json, r#"{"basic":{"username":"user","password":"pass"}}"#);
357
358        let auth = Authentication::ApiKey {
359            header_name: "X-API-Key".to_string(),
360            key: "secret-key".into(),
361        };
362        let json = serde_json::to_string(&auth).unwrap();
363        assert_eq!(
364            json,
365            r#"{"api_key":{"header_name":"X-API-Key","key":"secret-key"}}"#
366        );
367    }
368
369    #[test]
370    fn test_authentication_error_display() {
371        let error = AuthenticationError::InvalidBearerToken {
372            message: "contains null byte".to_string(),
373        };
374        assert_eq!(
375            error.to_string(),
376            "Bearer token contains invalid characters: contains null byte"
377        );
378
379        let error = AuthenticationError::InvalidUsername {
380            message: "contains colon".to_string(),
381        };
382        assert_eq!(
383            error.to_string(),
384            "Basic auth username contains invalid characters: contains colon"
385        );
386
387        let error = AuthenticationError::InvalidHeaderName {
388            header_name: "Invalid Header".to_string(),
389            message: "contains space".to_string(),
390        };
391        assert_eq!(
392            error.to_string(),
393            "Invalid API key header name 'Invalid Header': contains space"
394        );
395    }
396
397    #[test]
398    fn test_authentication_errors() {
399        // Test bearer token with invalid characters
400        let auth = Authentication::Bearer("\0invalid".into());
401        let result = auth.to_header();
402        assert!(result.is_err());
403        match result.unwrap_err() {
404            AuthenticationError::InvalidBearerToken { .. } => {}
405            _ => panic!("Expected InvalidBearerToken error"),
406        }
407
408        // Test basic auth with username containing colon
409        let auth = Authentication::Basic {
410            username: "user:invalid".to_string(),
411            password: "password".into(),
412        };
413        let result = auth.to_header();
414        assert!(result.is_err());
415        match result.unwrap_err() {
416            AuthenticationError::InvalidUsername { .. } => {}
417            _ => panic!("Expected InvalidUsername error"),
418        }
419
420        // Test API key with invalid header name
421        let auth = Authentication::ApiKey {
422            header_name: "Invalid Header".to_string(),
423            key: "key".into(),
424        };
425        let result = auth.to_header();
426        assert!(result.is_err());
427        match result.unwrap_err() {
428            AuthenticationError::InvalidHeaderName { .. } => {}
429            _ => panic!("Expected InvalidHeaderName error"),
430        }
431
432        // Test API key with invalid key value
433        let auth = Authentication::ApiKey {
434            header_name: "X-API-Key".to_string(),
435            key: "\0invalid".into(),
436        };
437        let result = auth.to_header();
438        assert!(result.is_err());
439        match result.unwrap_err() {
440            AuthenticationError::InvalidApiKey { .. } => {}
441            _ => panic!("Expected InvalidApiKey error"),
442        }
443    }
444
445    #[test]
446    fn test_secure_string_debug() {
447        let secure = SecureString::new("secret-password".to_string());
448        let debug_str = format!("{secure:?}");
449        assert_eq!(debug_str, "SecureString { value: \"[REDACTED]\" }");
450        assert!(!debug_str.contains("secret-password"));
451    }
452
453    #[test]
454    fn test_secure_string_display() {
455        let secure = SecureString::new("secret-password-12345".to_string());
456        let display_str = format!("{secure}");
457        assert_eq!(display_str, "secr...2345");
458        assert!(!display_str.contains("secret-password"));
459
460        let short_secure = SecureString::new("short".to_string());
461        let display_str = format!("{short_secure}");
462        assert_eq!(display_str, "***");
463    }
464
465    #[test]
466    fn test_secure_string_conversions() {
467        // Test From<String>
468        let secure: SecureString = "test".to_string().into();
469        assert_eq!(secure.as_str(), "test");
470
471        // Test From<&str>
472        let secure: SecureString = "test".into();
473        assert_eq!(secure.as_str(), "test");
474
475        // Test into_string
476        let secure = SecureString::new("test".to_string());
477        let back_to_string = secure.into_string();
478        assert_eq!(back_to_string, "test");
479    }
480}