Skip to main content

tako_rs_extractors/
basic.rs

1//! Basic HTTP authentication credential extraction from Authorization headers.
2//!
3//! This module provides extractors for parsing HTTP Basic authentication credentials
4//! as defined in RFC 7617. It extracts and validates the Authorization header with
5//! Basic scheme, decodes the Base64-encoded credentials, and provides structured
6//! access to username and password. The extractor handles proper error cases and
7//! provides detailed error information for authentication failures.
8//!
9//! # Examples
10//!
11//! ```rust
12//! use tako::extractors::basic::Basic;
13//! use tako::extractors::FromRequest;
14//! use tako::types::Request;
15//!
16//! async fn protected_handler(mut req: Request) -> Result<String, Box<dyn std::error::Error>> {
17//!     let basic_auth = Basic::from_request(&mut req).await?;
18//!
19//!     // Validate credentials (in production, check against database/LDAP/etc.)
20//!     if basic_auth.username == "admin" && basic_auth.password == "secret" {
21//!         Ok(format!("Welcome, {}!", basic_auth.username))
22//!     } else {
23//!         Ok("Invalid credentials".to_string())
24//!     }
25//! }
26//!
27//! // Usage in middleware or handlers
28//! async fn auth_middleware_example(basic: Basic) -> String {
29//!     format!("Authenticated user: {}", basic.username)
30//! }
31//! ```
32
33use base64::Engine;
34use base64::engine::general_purpose::STANDARD;
35use http::StatusCode;
36use http::request::Parts;
37use tako_rs_core::extractors::FromRequest;
38use tako_rs_core::extractors::FromRequestParts;
39use tako_rs_core::responder::Responder;
40use tako_rs_core::types::Request;
41
42/// Basic HTTP authentication credentials extracted from Authorization header.
43///
44/// Represents the username and password extracted from a Basic authentication
45/// Authorization header. The credentials are Base64-decoded and split on the
46/// first colon character as per RFC 7617. The raw token is also preserved
47/// for logging or advanced use cases.
48pub struct Basic {
49  /// Username extracted from the Basic auth token.
50  pub username: String,
51  /// Password extracted from the Basic auth token.
52  pub password: String,
53  /// Raw Basic auth token as received in the Authorization header.
54  pub raw: String,
55}
56
57/// Error types for Basic authentication extraction and validation.
58#[derive(Debug)]
59pub enum BasicAuthError {
60  /// Authorization header is missing from the request.
61  MissingAuthHeader,
62  /// Authorization header contains invalid UTF-8 or cannot be parsed.
63  InvalidAuthHeader,
64  /// Authorization header does not use Basic authentication scheme.
65  InvalidBasicFormat,
66  /// Base64 encoding in the Basic auth token is invalid.
67  InvalidBase64,
68  /// Decoded credentials contain invalid UTF-8 characters.
69  InvalidUtf8,
70  /// Credentials format is invalid (missing colon separator).
71  InvalidCredentialsFormat,
72}
73
74impl Responder for BasicAuthError {
75  /// Converts Basic authentication errors into appropriate HTTP responses.
76  fn into_response(self) -> tako_rs_core::types::Response {
77    let (status, message) = match self {
78      BasicAuthError::MissingAuthHeader => {
79        (StatusCode::UNAUTHORIZED, "Missing Authorization header")
80      }
81      BasicAuthError::InvalidAuthHeader => {
82        (StatusCode::UNAUTHORIZED, "Invalid Authorization header")
83      }
84      BasicAuthError::InvalidBasicFormat => (
85        StatusCode::UNAUTHORIZED,
86        "Authorization header is not Basic auth",
87      ),
88      BasicAuthError::InvalidBase64 => (
89        StatusCode::UNAUTHORIZED,
90        "Invalid Base64 encoding in Basic auth",
91      ),
92      BasicAuthError::InvalidUtf8 => (
93        StatusCode::UNAUTHORIZED,
94        "Invalid UTF-8 in Basic auth credentials",
95      ),
96      BasicAuthError::InvalidCredentialsFormat => (
97        StatusCode::UNAUTHORIZED,
98        "Invalid credentials format in Basic auth",
99      ),
100    };
101    (status, message).into_response()
102  }
103}
104
105impl Basic {
106  /// Parses Basic authentication credentials from HTTP headers.
107  fn extract_from_headers(headers: &http::HeaderMap) -> Result<Self, BasicAuthError> {
108    let auth_header = headers
109      .get("Authorization")
110      .ok_or(BasicAuthError::MissingAuthHeader)?;
111
112    let auth_str = auth_header
113      .to_str()
114      .map_err(|_| BasicAuthError::InvalidAuthHeader)?;
115
116    if !auth_str.starts_with("Basic ") {
117      return Err(BasicAuthError::InvalidBasicFormat);
118    }
119
120    let encoded = &auth_str[6..];
121    let decoded = STANDARD
122      .decode(encoded)
123      .map_err(|_| BasicAuthError::InvalidBase64)?;
124
125    let decoded_str = std::str::from_utf8(&decoded).map_err(|_| BasicAuthError::InvalidUtf8)?;
126
127    let parts: Vec<&str> = decoded_str.splitn(2, ':').collect();
128    if parts.len() != 2 {
129      return Err(BasicAuthError::InvalidCredentialsFormat);
130    }
131
132    Ok(Basic {
133      username: parts[0].to_string(),
134      password: parts[1].to_string(),
135      raw: auth_str.to_string(),
136    })
137  }
138}
139
140impl<'a> FromRequest<'a> for Basic {
141  type Error = BasicAuthError;
142
143  fn from_request(
144    req: &'a mut Request,
145  ) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
146    futures_util::future::ready(Self::extract_from_headers(req.headers()))
147  }
148}
149
150impl<'a> FromRequestParts<'a> for Basic {
151  type Error = BasicAuthError;
152
153  fn from_request_parts(
154    parts: &'a mut Parts,
155  ) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
156    futures_util::future::ready(Self::extract_from_headers(&parts.headers))
157  }
158}