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