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