Skip to main content

reinhardt_auth/
basic.rs

1//! HTTP Basic Authentication
2//!
3//! Passwords are hashed with Argon2id on storage and verified using
4//! constant-time comparison provided by the `argon2` crate.
5
6use crate::core::hasher::PasswordHasher;
7use crate::rest_authentication::RestAuthentication;
8use crate::{AuthenticationBackend, AuthenticationError, SimpleUser, User};
9use base64::{Engine, engine::general_purpose::STANDARD};
10use reinhardt_http::Request;
11use std::collections::HashMap;
12use uuid::Uuid;
13
14/// Argon2-based password hasher used internally by `BasicAuthentication`.
15///
16/// This is intentionally a thin wrapper so the module stays self-contained
17/// without requiring the `argon2-hasher` feature flag.
18struct InternalArgon2Hasher;
19
20impl PasswordHasher for InternalArgon2Hasher {
21	fn hash(&self, password: &str) -> Result<String, reinhardt_core::exception::Error> {
22		use argon2::Argon2;
23		use password_hash::{PasswordHasher as _, SaltString, rand_core::OsRng};
24
25		let salt = SaltString::generate(&mut OsRng);
26		let argon2 = Argon2::default();
27
28		argon2
29			.hash_password(password.as_bytes(), &salt)
30			.map(|hash| hash.to_string())
31			.map_err(|e| reinhardt_core::exception::Error::Authentication(e.to_string()))
32	}
33
34	fn verify(&self, password: &str, hash: &str) -> Result<bool, reinhardt_core::exception::Error> {
35		use argon2::Argon2;
36		use password_hash::{PasswordHash, PasswordVerifier};
37
38		let parsed_hash = PasswordHash::new(hash)
39			.map_err(|e| reinhardt_core::exception::Error::Authentication(e.to_string()))?;
40
41		// Argon2 verify_password uses constant-time comparison internally
42		Ok(Argon2::default()
43			.verify_password(password.as_bytes(), &parsed_hash)
44			.is_ok())
45	}
46}
47
48/// Basic Authentication backend
49///
50/// Passwords are hashed with Argon2id before storage.
51/// Verification uses the constant-time comparison built into Argon2.
52pub struct BasicAuthentication {
53	/// username -> argon2 password hash
54	users: HashMap<String, String>,
55	hasher: InternalArgon2Hasher,
56}
57
58impl BasicAuthentication {
59	/// Creates a new BasicAuthentication backend with no users.
60	///
61	/// # Examples
62	///
63	/// ```
64	/// use reinhardt_auth::{HttpBasicAuth, AuthenticationBackend};
65	/// use bytes::Bytes;
66	/// use hyper::{HeaderMap, Method, Uri, Version};
67	/// use reinhardt_http::Request;
68	///
69	/// # async fn example() {
70	/// let auth = HttpBasicAuth::new();
71	///
72	// Create a request without authentication header
73	/// let request = Request::builder()
74	///     .method(Method::GET)
75	///     .uri("/")
76	///     .body(Bytes::new())
77	///     .build()
78	///     .unwrap();
79	///
80	// Since no users are registered, authentication should return None
81	/// let result = auth.authenticate(&request).await.unwrap();
82	/// assert!(result.is_none());
83	/// # }
84	/// # tokio::runtime::Runtime::new().unwrap().block_on(example());
85	/// ```
86	pub fn new() -> Self {
87		Self {
88			users: HashMap::new(),
89			hasher: InternalArgon2Hasher,
90		}
91	}
92
93	/// Adds a user with the given username and password.
94	///
95	/// The password is hashed with Argon2id before storage.
96	///
97	/// # Panics
98	///
99	/// Panics if password hashing fails (should not happen in practice).
100	///
101	/// # Examples
102	///
103	/// ```
104	/// use reinhardt_auth::{HttpBasicAuth, AuthenticationBackend};
105	/// use bytes::Bytes;
106	/// use hyper::{HeaderMap, Method, Uri, Version};
107	/// use reinhardt_http::Request;
108	///
109	/// # async fn example() {
110	/// let mut auth = HttpBasicAuth::new();
111	/// auth.add_user("alice", "secret123");
112	/// auth.add_user("bob", "password456");
113	///
114	/// // Create a request with valid Basic auth credentials
115	/// // "alice:secret123" in base64 is "YWxpY2U6c2VjcmV0MTIz"
116	/// let mut headers = HeaderMap::new();
117	/// headers.insert("Authorization", "Basic YWxpY2U6c2VjcmV0MTIz".parse().unwrap());
118	/// let request = Request::builder()
119	///     .method(Method::GET)
120	///     .uri("/")
121	///     .headers(headers)
122	///     .body(Bytes::new())
123	///     .build()
124	///     .unwrap();
125	///
126	/// // Authentication should succeed
127	/// let result = auth.authenticate(&request).await.unwrap();
128	/// assert!(result.is_some());
129	/// assert_eq!(result.unwrap().get_username(), "alice");
130	/// # }
131	/// # tokio::runtime::Runtime::new().unwrap().block_on(example());
132	/// ```
133	pub fn add_user(&mut self, username: impl Into<String>, password: impl Into<String>) {
134		let hash = self
135			.hasher
136			.hash(&password.into())
137			.expect("Argon2 hashing should not fail");
138		self.users.insert(username.into(), hash);
139	}
140
141	/// Parse Authorization header
142	fn parse_auth_header(&self, header: &str) -> Option<(String, String)> {
143		if !header.starts_with("Basic ") {
144			return None;
145		}
146
147		let encoded = header.strip_prefix("Basic ")?;
148		let decoded = STANDARD.decode(encoded).ok()?;
149		let decoded_str = String::from_utf8(decoded).ok()?;
150
151		let parts: Vec<&str> = decoded_str.splitn(2, ':').collect();
152		if parts.len() != 2 {
153			return None;
154		}
155
156		Some((parts[0].to_string(), parts[1].to_string()))
157	}
158}
159
160impl Default for BasicAuthentication {
161	fn default() -> Self {
162		Self::new()
163	}
164}
165
166#[async_trait::async_trait]
167impl AuthenticationBackend for BasicAuthentication {
168	async fn authenticate(
169		&self,
170		request: &Request,
171	) -> Result<Option<Box<dyn User>>, AuthenticationError> {
172		let auth_header = request
173			.headers
174			.get("Authorization")
175			.and_then(|h| h.to_str().ok());
176
177		if let Some(header) = auth_header
178			&& let Some((username, password)) = self.parse_auth_header(header)
179		{
180			if let Some(stored_hash) = self.users.get(&username) {
181				// Argon2 verify uses constant-time comparison internally
182				let is_valid = self.hasher.verify(&password, stored_hash).unwrap_or(false);
183				if is_valid {
184					return Ok(Some(Box::new(SimpleUser {
185						id: Uuid::new_v4(),
186						username: username.clone(),
187						email: format!("{}@example.com", username),
188						is_active: true,
189						is_admin: false,
190						is_staff: false,
191						is_superuser: false,
192					})));
193				}
194			}
195			return Err(AuthenticationError::InvalidCredentials);
196		}
197
198		Ok(None)
199	}
200
201	async fn get_user(&self, user_id: &str) -> Result<Option<Box<dyn User>>, AuthenticationError> {
202		if self.users.contains_key(user_id) {
203			Ok(Some(Box::new(SimpleUser {
204				id: Uuid::new_v4(),
205				username: user_id.to_string(),
206				email: format!("{}@example.com", user_id),
207				is_active: true,
208				is_admin: false,
209				is_staff: false,
210				is_superuser: false,
211			})))
212		} else {
213			Ok(None)
214		}
215	}
216}
217
218// Implement REST API Authentication trait by forwarding to AuthenticationBackend
219#[async_trait::async_trait]
220impl RestAuthentication for BasicAuthentication {
221	async fn authenticate(
222		&self,
223		request: &Request,
224	) -> Result<Option<Box<dyn User>>, AuthenticationError> {
225		// Forward to AuthenticationBackend implementation
226		AuthenticationBackend::authenticate(self, request).await
227	}
228}
229
230#[cfg(test)]
231mod tests {
232	use super::*;
233	use bytes::Bytes;
234	use hyper::{HeaderMap, Method};
235	use rstest::rstest;
236
237	fn create_request_with_auth(auth: &str) -> Request {
238		let mut headers = HeaderMap::new();
239		headers.insert("Authorization", auth.parse().unwrap());
240		Request::builder()
241			.method(Method::GET)
242			.uri("/")
243			.headers(headers)
244			.body(Bytes::new())
245			.build()
246			.unwrap()
247	}
248
249	#[rstest]
250	#[tokio::test]
251	async fn test_basic_auth_success() {
252		// Arrange
253		let mut backend = BasicAuthentication::new();
254		backend.add_user("testuser", "testpass");
255
256		// Base64 encode "testuser:testpass"
257		let auth = "Basic dGVzdHVzZXI6dGVzdHBhc3M=";
258		let request = create_request_with_auth(auth);
259
260		// Act
261		let result = AuthenticationBackend::authenticate(&backend, &request)
262			.await
263			.unwrap();
264
265		// Assert
266		assert!(result.is_some());
267		assert_eq!(result.unwrap().get_username(), "testuser");
268	}
269
270	#[rstest]
271	#[tokio::test]
272	async fn test_basic_auth_invalid_password() {
273		// Arrange
274		let mut backend = BasicAuthentication::new();
275		backend.add_user("testuser", "correctpass");
276
277		// Base64 encode "testuser:wrongpass"
278		let auth = "Basic dGVzdHVzZXI6d3JvbmdwYXNz";
279		let request = create_request_with_auth(auth);
280
281		// Act
282		let result = AuthenticationBackend::authenticate(&backend, &request).await;
283
284		// Assert
285		assert!(result.is_err());
286	}
287
288	#[rstest]
289	#[tokio::test]
290	async fn test_basic_auth_no_header() {
291		// Arrange
292		let backend = BasicAuthentication::new();
293		let request = Request::builder()
294			.method(Method::GET)
295			.uri("/")
296			.body(Bytes::new())
297			.build()
298			.unwrap();
299
300		// Act
301		let result = AuthenticationBackend::authenticate(&backend, &request)
302			.await
303			.unwrap();
304
305		// Assert
306		assert!(result.is_none());
307	}
308
309	#[rstest]
310	fn test_parse_auth_header() {
311		// Arrange
312		let backend = BasicAuthentication::new();
313
314		// Act
315		let (user, pass) = backend.parse_auth_header("Basic dGVzdDpwYXNz").unwrap();
316
317		// Assert
318		assert_eq!(user, "test");
319		assert_eq!(pass, "pass");
320	}
321
322	#[rstest]
323	#[tokio::test]
324	async fn test_get_user() {
325		// Arrange
326		let mut backend = BasicAuthentication::new();
327		backend.add_user("testuser", "testpass");
328
329		// Act
330		let user = backend.get_user("testuser").await.unwrap();
331		let no_user = backend.get_user("nonexistent").await.unwrap();
332
333		// Assert
334		assert!(user.is_some());
335		assert_eq!(user.unwrap().get_username(), "testuser");
336		assert!(no_user.is_none());
337	}
338
339	#[rstest]
340	fn test_password_is_hashed_on_storage() {
341		// Arrange
342		let mut backend = BasicAuthentication::new();
343
344		// Act
345		backend.add_user("testuser", "plaintext_password");
346
347		// Assert
348		let stored = backend.users.get("testuser").unwrap();
349		// Argon2 hashes start with "$argon2"
350		assert!(
351			stored.starts_with("$argon2"),
352			"Password should be stored as Argon2 hash, got: {}",
353			stored
354		);
355		assert_ne!(stored, "plaintext_password");
356	}
357
358	#[rstest]
359	fn test_argon2_verification_works() {
360		// Arrange
361		let hasher = InternalArgon2Hasher;
362		let password = "test_password_123";
363
364		// Act
365		let hash = hasher.hash(password).unwrap();
366		let valid = hasher.verify(password, &hash).unwrap();
367		let invalid = hasher.verify("wrong_password", &hash).unwrap();
368
369		// Assert
370		assert!(valid);
371		assert!(!invalid);
372	}
373}