1use std::fmt;
2
3use http::HeaderValue;
4use reqwest::header::{AUTHORIZATION, HeaderName};
5use serde::{Deserialize, Serialize};
6use zeroize::{Zeroize, ZeroizeOnDrop};
7
8#[derive(Debug, Clone, PartialEq, Eq, derive_more::Error, derive_more::Display)]
13pub enum AuthenticationError {
14 #[display("Bearer token contains invalid characters: {message}")]
16 InvalidBearerToken {
17 message: String,
19 },
20
21 #[display("Basic auth username contains invalid characters: {message}")]
23 InvalidUsername {
24 message: String,
26 },
27
28 #[display("Basic auth password contains invalid characters: {message}")]
30 InvalidPassword {
31 message: String,
33 },
34
35 #[display("Invalid API key header name '{header_name}': {message}")]
37 InvalidHeaderName {
38 header_name: String,
40 message: String,
42 },
43
44 #[display("API key contains invalid characters: {message}")]
46 InvalidApiKey {
47 message: String,
49 },
50
51 #[display("Base64 encoding failed: {message}")]
53 EncodingError {
54 message: String,
56 },
57}
58
59#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
64pub struct SecureString(String);
65
66impl SecureString {
67 pub fn new(value: String) -> Self {
69 Self(value)
70 }
71
72 pub fn as_str(&self) -> &str {
78 &self.0
79 }
80
81 pub fn into_string(mut self) -> String {
86 std::mem::take(&mut self.0)
88 }
89
90 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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "snake_case")]
188pub enum Authentication {
189 Bearer(SecureString),
192
193 Basic {
196 username: String,
197 password: SecureString,
198 },
199
200 ApiKey {
203 header_name: String,
204 key: SecureString,
205 },
206}
207
208impl Authentication {
209 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 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 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 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 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 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 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 let secure: SecureString = "test".to_string().into();
469 assert_eq!(secure.as_str(), "test");
470
471 let secure: SecureString = "test".into();
473 assert_eq!(secure.as_str(), "test");
474
475 let secure = SecureString::new("test".to_string());
477 let back_to_string = secure.into_string();
478 assert_eq!(back_to_string, "test");
479 }
480}