1use std::fmt;
2
3use http::HeaderValue;
4use reqwest::header::{AUTHORIZATION, HeaderName};
5use serde::{Deserialize, Serialize};
6use zeroize::{Zeroize, ZeroizeOnDrop};
7
8#[cfg(feature = "oauth2")]
9use super::oauth2::SharedOAuth2Config;
10
11#[derive(Debug, Clone, PartialEq, Eq, derive_more::Error, derive_more::Display)]
16pub enum AuthenticationError {
17 #[display("Bearer token contains invalid characters: {message}")]
19 InvalidBearerToken {
20 message: String,
22 },
23
24 #[display("Basic auth username contains invalid characters: {message}")]
26 InvalidUsername {
27 message: String,
29 },
30
31 #[display("Basic auth password contains invalid characters: {message}")]
33 InvalidPassword {
34 message: String,
36 },
37
38 #[display("Invalid API key header name '{header_name}': {message}")]
40 InvalidHeaderName {
41 header_name: String,
43 message: String,
45 },
46
47 #[display("API key contains invalid characters: {message}")]
49 InvalidApiKey {
50 message: String,
52 },
53
54 #[display("Base64 encoding failed: {message}")]
56 EncodingError {
57 message: String,
59 },
60
61 #[cfg(feature = "oauth2")]
63 #[display("OAuth2 token has not been acquired yet")]
64 OAuth2TokenNotAcquired,
65
66 #[cfg(feature = "oauth2")]
68 #[display("OAuth2 error: {message}")]
69 OAuth2Error {
70 message: String,
72 },
73}
74
75#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
80pub struct SecureString(String);
81
82impl SecureString {
83 pub fn new(value: String) -> Self {
85 Self(value)
86 }
87
88 pub fn as_str(&self) -> &str {
94 &self.0
95 }
96
97 pub fn into_string(mut self) -> String {
102 std::mem::take(&mut self.0)
104 }
105
106 pub fn equals_str(&self, other: &str) -> bool {
111 self.0 == other
112 }
113}
114
115impl fmt::Debug for SecureString {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 f.debug_struct("SecureString")
118 .field("value", &"[REDACTED]")
119 .finish()
120 }
121}
122
123impl fmt::Display for SecureString {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 write!(f, "{}", Self::mask_sensitive(&self.0))
126 }
127}
128
129impl From<String> for SecureString {
130 fn from(value: String) -> Self {
131 Self::new(value)
132 }
133}
134
135impl From<&str> for SecureString {
136 fn from(value: &str) -> Self {
137 Self::new(value.to_string())
138 }
139}
140
141impl Serialize for SecureString {
142 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
143 where
144 S: serde::Serializer,
145 {
146 self.0.serialize(serializer)
147 }
148}
149
150impl<'de> Deserialize<'de> for SecureString {
151 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
152 where
153 D: serde::Deserializer<'de>,
154 {
155 String::deserialize(deserializer).map(Self::new)
156 }
157}
158
159impl SecureString {
160 fn mask_sensitive(value: &str) -> String {
162 if value.len() <= 8 {
163 "***".to_string()
164 } else {
165 format!("{}...{}", &value[..4], &value[value.len() - 4..])
166 }
167 }
168}
169
170#[derive(Clone)]
205#[cfg_attr(
206 not(feature = "oauth2"),
207 derive(Serialize, Deserialize),
208 serde(rename_all = "snake_case")
209)]
210pub enum Authentication {
211 Bearer(SecureString),
214
215 Basic {
218 username: String,
220 password: SecureString,
222 },
223
224 ApiKey {
227 header_name: String,
229 key: SecureString,
231 },
232
233 #[cfg(feature = "oauth2")]
238 OAuth2(SharedOAuth2Config),
239}
240
241impl Authentication {
242 pub fn to_header(&self) -> Result<(HeaderName, HeaderValue), AuthenticationError> {
251 match self {
252 Authentication::Bearer(token) => {
253 let header_value = format!("Bearer {}", token.as_str());
254 let value = HeaderValue::from_str(&header_value).map_err(|e| {
255 AuthenticationError::InvalidBearerToken {
256 message: e.to_string(),
257 }
258 })?;
259 Ok((AUTHORIZATION, value))
260 }
261
262 Authentication::Basic { username, password } => {
263 if username.contains(':') {
265 return Err(AuthenticationError::InvalidUsername {
266 message: "Username cannot contain colon (:) character".to_string(),
267 });
268 }
269
270 use base64::Engine;
271 let credentials_str = format!("{}:{}", username, password.as_str());
272 let credentials = base64::engine::general_purpose::STANDARD.encode(credentials_str);
273
274 let header_value = format!("Basic {credentials}");
275 let value = HeaderValue::from_str(&header_value).map_err(|e| {
276 AuthenticationError::InvalidPassword {
277 message: e.to_string(),
278 }
279 })?;
280 Ok((AUTHORIZATION, value))
281 }
282
283 Authentication::ApiKey { header_name, key } => {
284 let header = HeaderName::from_bytes(header_name.as_bytes()).map_err(|e| {
285 AuthenticationError::InvalidHeaderName {
286 header_name: header_name.clone(),
287 message: e.to_string(),
288 }
289 })?;
290 let value = HeaderValue::from_str(key.as_str()).map_err(|e| {
291 AuthenticationError::InvalidApiKey {
292 message: e.to_string(),
293 }
294 })?;
295 Ok((header, value))
296 }
297
298 #[cfg(feature = "oauth2")]
299 Authentication::OAuth2(_) => {
300 Err(AuthenticationError::OAuth2TokenNotAcquired)
303 }
304 }
305 }
306}
307
308impl fmt::Debug for Authentication {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 match self {
311 Self::Bearer(_) => f.debug_tuple("Bearer").field(&"[REDACTED]").finish(),
312 Self::Basic { username, .. } => f
313 .debug_struct("Basic")
314 .field("username", username)
315 .field("password", &"[REDACTED]")
316 .finish(),
317 Self::ApiKey { header_name, .. } => f
318 .debug_struct("ApiKey")
319 .field("header_name", header_name)
320 .field("key", &"[REDACTED]")
321 .finish(),
322 #[cfg(feature = "oauth2")]
323 Self::OAuth2(config) => f.debug_tuple("OAuth2").field(config).finish(),
324 }
325 }
326}
327
328impl fmt::Display for Authentication {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 match self {
331 Self::Bearer(token) => {
332 write!(f, "Bearer {token}")
333 }
334 Self::Basic { username, .. } => write!(f, "Basic (username: {username})"),
335 Self::ApiKey { header_name, key } => {
336 write!(f, "ApiKey ({header_name}: {key})")
337 }
338 #[cfg(feature = "oauth2")]
339 Self::OAuth2(config) => {
340 write!(f, "OAuth2 (client_id: {})", config.0.client_id)
341 }
342 }
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn test_bearer_authentication() {
352 let auth = Authentication::Bearer("my-secret-token".into());
353 let (header_name, header_value) = auth.to_header().unwrap();
354
355 assert_eq!(header_name, AUTHORIZATION);
356 assert_eq!(header_value, "Bearer my-secret-token");
357 }
358
359 #[test]
360 fn test_basic_authentication() {
361 let auth = Authentication::Basic {
362 username: "user".to_string(),
363 password: "pass".into(),
364 };
365 let (header_name, header_value) = auth.to_header().unwrap();
366
367 assert_eq!(header_name, AUTHORIZATION);
368 assert_eq!(header_value, "Basic dXNlcjpwYXNz");
370 }
371
372 #[test]
373 fn test_api_key_authentication() {
374 let auth = Authentication::ApiKey {
375 header_name: "X-API-Key".to_string(),
376 key: "secret-key-123".into(),
377 };
378 let (header_name, header_value) = auth.to_header().unwrap();
379
380 assert_eq!(header_name, "X-API-Key");
381 assert_eq!(header_value, "secret-key-123");
382 }
383
384 #[test]
385 fn test_display_masks_secrets() {
386 let auth = Authentication::Bearer("very-secret-token-12345".into());
387 assert_eq!(auth.to_string(), "Bearer very...2345");
388
389 let auth = Authentication::Basic {
390 username: "user".to_string(),
391 password: "password".into(),
392 };
393 assert_eq!(auth.to_string(), "Basic (username: user)");
394
395 let auth = Authentication::ApiKey {
396 header_name: "X-API-Key".to_string(),
397 key: "secret-key-12345".into(),
398 };
399 assert_eq!(auth.to_string(), "ApiKey (X-API-Key: secr...2345)");
400 }
401
402 #[test]
403 fn test_secure_string_mask_short_tokens() {
404 assert_eq!(SecureString::mask_sensitive("short"), "***");
405 assert_eq!(SecureString::mask_sensitive("12345678"), "***");
406 assert_eq!(SecureString::mask_sensitive("123456789"), "1234...6789");
407 }
408
409 #[cfg(not(feature = "oauth2"))]
412 #[test]
413 fn test_serialization() {
414 let auth = Authentication::Bearer("token".into());
415 let json = serde_json::to_string(&auth).expect("serialize bearer");
416 assert_eq!(json, r#"{"bearer":"token"}"#);
417
418 let auth = Authentication::Basic {
419 username: "user".to_string(),
420 password: "pass".into(),
421 };
422 let json = serde_json::to_string(&auth).expect("serialize basic");
423 assert_eq!(json, r#"{"basic":{"username":"user","password":"pass"}}"#);
424
425 let auth = Authentication::ApiKey {
426 header_name: "X-API-Key".to_string(),
427 key: "secret-key".into(),
428 };
429 let json = serde_json::to_string(&auth).expect("serialize apikey");
430 assert_eq!(
431 json,
432 r#"{"api_key":{"header_name":"X-API-Key","key":"secret-key"}}"#
433 );
434 }
435
436 #[test]
437 fn test_authentication_error_display() {
438 let error = AuthenticationError::InvalidBearerToken {
439 message: "contains null byte".to_string(),
440 };
441 assert_eq!(
442 error.to_string(),
443 "Bearer token contains invalid characters: contains null byte"
444 );
445
446 let error = AuthenticationError::InvalidUsername {
447 message: "contains colon".to_string(),
448 };
449 assert_eq!(
450 error.to_string(),
451 "Basic auth username contains invalid characters: contains colon"
452 );
453
454 let error = AuthenticationError::InvalidHeaderName {
455 header_name: "Invalid Header".to_string(),
456 message: "contains space".to_string(),
457 };
458 assert_eq!(
459 error.to_string(),
460 "Invalid API key header name 'Invalid Header': contains space"
461 );
462 }
463
464 #[test]
465 fn test_authentication_errors() {
466 let auth = Authentication::Bearer("\0invalid".into());
468 let result = auth.to_header();
469 assert!(result.is_err());
470 match result.unwrap_err() {
471 AuthenticationError::InvalidBearerToken { .. } => {}
472 _ => panic!("Expected InvalidBearerToken error"),
473 }
474
475 let auth = Authentication::Basic {
477 username: "user:invalid".to_string(),
478 password: "password".into(),
479 };
480 let result = auth.to_header();
481 assert!(result.is_err());
482 match result.unwrap_err() {
483 AuthenticationError::InvalidUsername { .. } => {}
484 _ => panic!("Expected InvalidUsername error"),
485 }
486
487 let auth = Authentication::ApiKey {
489 header_name: "Invalid Header".to_string(),
490 key: "key".into(),
491 };
492 let result = auth.to_header();
493 assert!(result.is_err());
494 match result.unwrap_err() {
495 AuthenticationError::InvalidHeaderName { .. } => {}
496 _ => panic!("Expected InvalidHeaderName error"),
497 }
498
499 let auth = Authentication::ApiKey {
501 header_name: "X-API-Key".to_string(),
502 key: "\0invalid".into(),
503 };
504 let result = auth.to_header();
505 assert!(result.is_err());
506 match result.unwrap_err() {
507 AuthenticationError::InvalidApiKey { .. } => {}
508 _ => panic!("Expected InvalidApiKey error"),
509 }
510 }
511
512 #[test]
513 fn test_secure_string_debug() {
514 let secure = SecureString::new("secret-password".to_string());
515 let debug_str = format!("{secure:?}");
516 assert_eq!(debug_str, "SecureString { value: \"[REDACTED]\" }");
517 assert!(!debug_str.contains("secret-password"));
518 }
519
520 #[test]
521 fn test_secure_string_display() {
522 let secure = SecureString::new("secret-password-12345".to_string());
523 let display_str = format!("{secure}");
524 assert_eq!(display_str, "secr...2345");
525 assert!(!display_str.contains("secret-password"));
526
527 let short_secure = SecureString::new("short".to_string());
528 let display_str = format!("{short_secure}");
529 assert_eq!(display_str, "***");
530 }
531
532 #[test]
533 fn test_secure_string_conversions() {
534 let secure: SecureString = "test".to_string().into();
536 assert_eq!(secure.as_str(), "test");
537
538 let secure: SecureString = "test".into();
540 assert_eq!(secure.as_str(), "test");
541
542 let secure = SecureString::new("test".to_string());
544 let back_to_string = secure.into_string();
545 assert_eq!(back_to_string, "test");
546 }
547}