Skip to main content

reinhardt_auth/
rest_authentication.rs

1//! REST API Authentication
2//!
3//! Provides REST API-compatible authentication wrappers and combinators.
4
5use crate::core::AuthIdentity;
6use crate::internal_user::InternalUser;
7use crate::sessions::{Session, backends::SessionBackend};
8use crate::{AuthBackend, AuthenticationError};
9use reinhardt_http::Request;
10use sha2::{Digest, Sha256};
11use std::sync::Arc;
12use subtle::ConstantTimeEq;
13
14/// REST API authentication trait wrapper
15///
16/// Provides a REST API-compatible interface for authentication.
17#[async_trait::async_trait]
18pub trait RestAuthentication: Send + Sync {
19	/// Authenticate a request and return an identity if successful
20	async fn authenticate(
21		&self,
22		request: &Request,
23	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError>;
24}
25
26/// Basic authentication configuration
27#[derive(Debug, Clone)]
28pub struct BasicAuthConfig {
29	/// Realm for WWW-Authenticate header
30	pub realm: String,
31}
32
33impl Default for BasicAuthConfig {
34	fn default() -> Self {
35		Self {
36			realm: "api".to_string(),
37		}
38	}
39}
40
41/// Session authentication configuration
42#[derive(Debug, Clone)]
43pub struct SessionAuthConfig {
44	/// Session cookie name
45	pub cookie_name: String,
46	/// Whether to enforce CSRF protection
47	pub enforce_csrf: bool,
48}
49
50impl Default for SessionAuthConfig {
51	fn default() -> Self {
52		Self {
53			cookie_name: "sessionid".to_string(),
54			enforce_csrf: true,
55		}
56	}
57}
58
59/// Token authentication configuration
60#[derive(Debug, Clone)]
61pub struct TokenAuthConfig {
62	/// Token header name (default: "Authorization")
63	pub header_name: String,
64	/// Token prefix (default: "Token")
65	pub prefix: String,
66}
67
68impl Default for TokenAuthConfig {
69	fn default() -> Self {
70		Self {
71			header_name: "Authorization".to_string(),
72			prefix: "Token".to_string(),
73		}
74	}
75}
76
77/// Composite authentication backend
78///
79/// Tries multiple authentication methods in sequence, similar to Django REST Framework.
80///
81/// # Examples
82///
83/// ```
84/// use reinhardt_auth::{CompositeAuthentication, SessionAuthentication, TokenAuthentication};
85/// use reinhardt_auth::sessions::backends::InMemorySessionBackend;
86///
87/// let session_backend = InMemorySessionBackend::new();
88/// let auth = CompositeAuthentication::new()
89///     .with_backend(SessionAuthentication::new(session_backend))
90///     .with_backend(TokenAuthentication::new());
91/// ```
92pub struct CompositeAuthentication {
93	backends: Vec<Arc<dyn AuthBackend>>,
94}
95
96impl CompositeAuthentication {
97	/// Create a new composite authentication backend
98	///
99	/// # Examples
100	///
101	/// ```
102	/// use reinhardt_auth::CompositeAuthentication;
103	///
104	/// let auth = CompositeAuthentication::new();
105	/// ```
106	pub fn new() -> Self {
107		Self {
108			backends: Vec::new(),
109		}
110	}
111
112	/// Add an authentication backend (chainable)
113	///
114	/// Backends are tried in the order they are added.
115	/// The backend will be wrapped in an Arc internally.
116	///
117	/// # Examples
118	///
119	/// ```
120	/// use reinhardt_auth::{CompositeAuthentication, TokenAuthentication};
121	///
122	/// let auth = CompositeAuthentication::new()
123	///     .with_backend(TokenAuthentication::new());
124	/// ```
125	pub fn with_backend<B: AuthBackend + 'static>(mut self, backend: B) -> Self {
126		self.backends.push(Arc::new(backend));
127		self
128	}
129
130	/// Add multiple backends at once (chainable)
131	pub fn with_backends(mut self, backends: Vec<Arc<dyn AuthBackend>>) -> Self {
132		self.backends.extend(backends);
133		self
134	}
135}
136
137impl Default for CompositeAuthentication {
138	fn default() -> Self {
139		Self::new()
140	}
141}
142
143#[async_trait::async_trait]
144impl RestAuthentication for CompositeAuthentication {
145	/// Fallback authentication pattern: backends are tried sequentially.
146	///
147	/// - `Ok(Some(user))`: authentication succeeded, return immediately
148	/// - `Ok(None)`: this backend does not handle this authentication type, try next
149	/// - `Err(e)`: backend error (e.g., database failure), log and try next
150	///
151	/// Errors are only propagated when ALL backends return `Err`, meaning no backend
152	/// could attempt authentication. If any backend returns `Ok(None)`, it indicates
153	/// that at least one backend processed the request normally, so errors from
154	/// other backends are considered irrelevant to this request type.
155	async fn authenticate(
156		&self,
157		request: &Request,
158	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
159		// Try each backend in order, collecting errors
160		let mut errors: Vec<AuthenticationError> = Vec::new();
161
162		for backend in &self.backends {
163			match backend.authenticate(request).await {
164				Ok(Some(user)) => return Ok(Some(user)),
165				Ok(None) => continue,
166				Err(e) => {
167					tracing::warn!("Authentication backend error occurred");
168					tracing::debug!(error = %e, "Authentication backend error details");
169					errors.push(e);
170				}
171			}
172		}
173
174		// If all backends failed with errors and none returned Ok(None),
175		// propagate the first error to inform the caller
176		if !errors.is_empty() && self.backends.len() == errors.len() {
177			return Err(errors.into_iter().next().expect("errors is non-empty"));
178		}
179
180		Ok(None)
181	}
182}
183
184#[async_trait::async_trait]
185impl AuthBackend for CompositeAuthentication {
186	async fn authenticate(
187		&self,
188		request: &Request,
189	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
190		<Self as RestAuthentication>::authenticate(self, request).await
191	}
192
193	async fn get_user(
194		&self,
195		user_id: &str,
196	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
197		// Try each backend in order until one succeeds, collecting errors
198		let mut errors: Vec<AuthenticationError> = Vec::new();
199
200		for backend in &self.backends {
201			match backend.get_user(user_id).await {
202				Ok(Some(user)) => return Ok(Some(user)),
203				Ok(None) => continue,
204				Err(e) => {
205					tracing::warn!("get_user backend error occurred");
206					tracing::debug!(error = %e, "get_user backend error details");
207					errors.push(e);
208				}
209			}
210		}
211
212		// If all backends failed with errors and none returned Ok(None),
213		// propagate the first error to inform the caller
214		if !errors.is_empty() && self.backends.len() == errors.len() {
215			return Err(errors.into_iter().next().expect("errors is non-empty"));
216		}
217
218		Ok(None)
219	}
220}
221
222/// Token authentication using custom tokens
223pub struct TokenAuthentication {
224	/// Token store (token -> user_id) — retained for `get_user` lookups by user_id
225	tokens: std::collections::HashMap<String, String>,
226	/// SHA-256 digest index for O(1) token lookup
227	/// Key: SHA-256(token), Value: (original_token, user_id)
228	token_index: std::collections::HashMap<[u8; 32], (String, String)>,
229	/// Configuration
230	config: TokenAuthConfig,
231}
232
233impl TokenAuthentication {
234	/// Create a new token authentication backend
235	///
236	/// # Examples
237	///
238	/// ```
239	/// use reinhardt_auth::TokenAuthentication;
240	///
241	/// let auth = TokenAuthentication::new();
242	/// ```
243	pub fn new() -> Self {
244		Self {
245			tokens: std::collections::HashMap::new(),
246			token_index: std::collections::HashMap::new(),
247			config: TokenAuthConfig::default(),
248		}
249	}
250
251	/// Create with custom configuration
252	pub fn with_config(config: TokenAuthConfig) -> Self {
253		Self {
254			tokens: std::collections::HashMap::new(),
255			token_index: std::collections::HashMap::new(),
256			config,
257		}
258	}
259
260	/// Add a token for a user
261	///
262	/// Tokens are stored with their SHA-256 digest for O(1) lookup.
263	pub fn add_token(&mut self, token: impl Into<String>, user_id: impl Into<String>) {
264		let token = token.into();
265		let user_id = user_id.into();
266		let digest: [u8; 32] = Sha256::digest(token.as_bytes()).into();
267		self.token_index
268			.insert(digest, (token.clone(), user_id.clone()));
269		self.tokens.insert(token, user_id);
270	}
271
272	/// Find a token using SHA-256 digest for O(1) lookup with constant-time
273	/// verification to prevent timing attacks.
274	///
275	/// 1. Compute SHA-256(candidate) and look up in the digest index (O(1))
276	/// 2. Verify the match with constant-time comparison on the original token
277	fn find_token_constant_time(&self, candidate: &str) -> Option<&String> {
278		let digest: [u8; 32] = Sha256::digest(candidate.as_bytes()).into();
279		if let Some((stored_token, user_id)) = self.token_index.get(&digest) {
280			let candidate_bytes = candidate.as_bytes();
281			let stored_bytes = stored_token.as_bytes();
282			if candidate_bytes.len() == stored_bytes.len()
283				&& bool::from(candidate_bytes.ct_eq(stored_bytes))
284			{
285				Some(user_id)
286			} else {
287				None
288			}
289		} else {
290			None
291		}
292	}
293}
294
295impl Default for TokenAuthentication {
296	fn default() -> Self {
297		Self::new()
298	}
299}
300
301#[async_trait::async_trait]
302impl RestAuthentication for TokenAuthentication {
303	async fn authenticate(
304		&self,
305		request: &Request,
306	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
307		let auth_header = request
308			.headers
309			.get(&self.config.header_name)
310			.and_then(|h| h.to_str().ok());
311
312		if let Some(header) = auth_header {
313			let prefix = format!("{} ", self.config.prefix);
314			if let Some(token) = header.strip_prefix(&prefix)
315				&& let Some(user_id) = self.find_token_constant_time(token)
316			{
317				// Try to parse user_id as UUID, or generate a new one if it fails
318				let id = uuid::Uuid::parse_str(user_id).unwrap_or_else(|_| {
319					uuid::Uuid::new_v5(&crate::USER_ID_NAMESPACE, user_id.as_bytes())
320				});
321				return Ok(Some(Box::new(InternalUser {
322					id,
323					username: user_id.clone(),
324					email: String::new(),
325					is_active: true,
326					is_admin: false,
327					is_staff: false,
328					is_superuser: false,
329				})));
330			}
331		}
332
333		Ok(None)
334	}
335}
336
337#[async_trait::async_trait]
338impl AuthBackend for TokenAuthentication {
339	async fn authenticate(
340		&self,
341		request: &Request,
342	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
343		<Self as RestAuthentication>::authenticate(self, request).await
344	}
345
346	async fn get_user(
347		&self,
348		user_id: &str,
349	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
350		if self.tokens.values().any(|id| id == user_id) {
351			// Try to parse user_id as UUID, or generate a new one if it fails
352			let id = uuid::Uuid::parse_str(user_id).unwrap_or_else(|_| {
353				uuid::Uuid::new_v5(&crate::USER_ID_NAMESPACE, user_id.as_bytes())
354			});
355			Ok(Some(Box::new(InternalUser {
356				id,
357				username: user_id.to_string(),
358				email: String::new(),
359				is_active: true,
360				is_admin: false,
361				is_staff: false,
362				is_superuser: false,
363			})))
364		} else {
365			Ok(None)
366		}
367	}
368}
369
370/// Remote user authentication (from upstream proxy)
371pub struct RemoteUserAuthentication {
372	/// Header name to check
373	header_name: String,
374}
375
376impl RemoteUserAuthentication {
377	/// Create a new remote user authentication backend
378	pub fn new() -> Self {
379		Self {
380			header_name: "REMOTE_USER".to_string(),
381		}
382	}
383
384	/// Set custom header name
385	pub fn with_header(mut self, header: impl Into<String>) -> Self {
386		self.header_name = header.into();
387		self
388	}
389}
390
391impl Default for RemoteUserAuthentication {
392	fn default() -> Self {
393		Self::new()
394	}
395}
396
397#[async_trait::async_trait]
398impl RestAuthentication for RemoteUserAuthentication {
399	async fn authenticate(
400		&self,
401		request: &Request,
402	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
403		let header_value = request
404			.headers
405			.get(&self.header_name)
406			.and_then(|v| v.to_str().ok());
407
408		if let Some(username) = header_value
409			&& !username.is_empty()
410		{
411			return Ok(Some(Box::new(InternalUser {
412				id: uuid::Uuid::new_v5(&crate::USER_ID_NAMESPACE, username.as_bytes()),
413				username: username.to_string(),
414				email: String::new(),
415				is_active: true,
416				is_admin: false,
417				is_staff: false,
418				is_superuser: false,
419			})));
420		}
421
422		Ok(None)
423	}
424}
425
426#[async_trait::async_trait]
427impl AuthBackend for RemoteUserAuthentication {
428	async fn authenticate(
429		&self,
430		request: &Request,
431	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
432		<Self as RestAuthentication>::authenticate(self, request).await
433	}
434
435	async fn get_user(
436		&self,
437		_user_id: &str,
438	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
439		Ok(None)
440	}
441}
442
443/// Session-based authentication
444#[derive(Clone)]
445pub struct SessionAuthentication<B: SessionBackend> {
446	/// Configuration
447	config: SessionAuthConfig,
448	/// Session backend for loading session data
449	session_backend: B,
450}
451
452impl<B: SessionBackend> SessionAuthentication<B> {
453	/// Create a new session authentication backend
454	pub fn new(session_backend: B) -> Self {
455		Self {
456			config: SessionAuthConfig::default(),
457			session_backend,
458		}
459	}
460
461	/// Create with custom configuration
462	pub fn with_config(config: SessionAuthConfig, session_backend: B) -> Self {
463		Self {
464			config,
465			session_backend,
466		}
467	}
468}
469
470impl<B: SessionBackend + Default> Default for SessionAuthentication<B> {
471	fn default() -> Self {
472		Self::new(B::default())
473	}
474}
475
476#[async_trait::async_trait]
477impl<B: SessionBackend> RestAuthentication for SessionAuthentication<B> {
478	async fn authenticate(
479		&self,
480		request: &Request,
481	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
482		// Check for session cookie
483		let cookie_header = request.headers.get("Cookie").and_then(|h| h.to_str().ok());
484
485		if let Some(cookies) = cookie_header {
486			for cookie in cookies.split(';') {
487				let parts: Vec<&str> = cookie.trim().splitn(2, '=').collect();
488				if parts.len() == 2 && parts[0] == self.config.cookie_name {
489					let session_key = parts[1];
490
491					// Load session from backend
492					let mut session =
493						Session::from_key(self.session_backend.clone(), session_key.to_string())
494							.await
495							.map_err(|_| AuthenticationError::SessionExpired)?;
496
497					// Get user ID from session
498					let user_id: String = match session.get("_auth_user_id") {
499						Ok(Some(id)) => id,
500						Ok(None) => return Ok(None), // No user in session
501						Err(_) => return Err(AuthenticationError::SessionExpired),
502					};
503
504					// Get additional user fields from session
505					let username: String = session
506						.get("_auth_user_name")
507						.ok()
508						.flatten()
509						.unwrap_or_else(|| user_id.clone());
510					let email: String = session
511						.get("_auth_user_email")
512						.ok()
513						.flatten()
514						.unwrap_or_default();
515					let is_active: bool = session
516						.get("_auth_user_is_active")
517						.ok()
518						.flatten()
519						.unwrap_or(true);
520					let is_admin: bool = session
521						.get("_auth_user_is_admin")
522						.ok()
523						.flatten()
524						.unwrap_or(false);
525					let is_staff: bool = session
526						.get("_auth_user_is_staff")
527						.ok()
528						.flatten()
529						.unwrap_or(false);
530					let is_superuser: bool = session
531						.get("_auth_user_is_superuser")
532						.ok()
533						.flatten()
534						.unwrap_or(false);
535
536					// Create user from session data
537					let user = InternalUser {
538						id: uuid::Uuid::parse_str(&user_id)
539							.map_err(|_| AuthenticationError::InvalidCredentials)?,
540						username,
541						email,
542						is_active,
543						is_admin,
544						is_staff,
545						is_superuser,
546					};
547
548					return Ok(Some(Box::new(user)));
549				}
550			}
551		}
552
553		Ok(None)
554	}
555}
556
557#[async_trait::async_trait]
558impl<B: SessionBackend> AuthBackend for SessionAuthentication<B> {
559	async fn authenticate(
560		&self,
561		request: &Request,
562	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
563		<Self as RestAuthentication>::authenticate(self, request).await
564	}
565
566	async fn get_user(
567		&self,
568		_user_id: &str,
569	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
570		// Session-based user retrieval by ID is not supported without a
571		// concrete user model. Applications should provide their own
572		// AuthBackend implementation that queries the database with their
573		// custom user type.
574		Ok(None)
575	}
576}
577
578#[cfg(test)]
579mod tests {
580	use super::*;
581	use crate::AuthenticationError;
582	#[cfg(feature = "jwt")]
583	use crate::basic::BasicAuthentication;
584	use bytes::Bytes;
585	use hyper::{HeaderMap, Method};
586	use rstest::rstest;
587	use std::sync::Mutex;
588
589	#[tokio::test]
590	#[cfg(feature = "jwt")]
591	async fn test_composite_authentication() {
592		let mut basic = BasicAuthentication::new();
593		basic.add_user("user1", "pass1");
594
595		let composite = CompositeAuthentication::new().with_backend(basic);
596
597		// Test with basic auth
598		let mut headers = HeaderMap::new();
599		headers.insert(
600			"Authorization",
601			"Basic dXNlcjE6cGFzczE=".parse().unwrap(), // user1:pass1
602		);
603
604		let request = Request::builder()
605			.method(Method::GET)
606			.uri("/")
607			.headers(headers)
608			.body(Bytes::new())
609			.build()
610			.unwrap();
611
612		let result = RestAuthentication::authenticate(&composite, &request)
613			.await
614			.unwrap();
615		assert!(result.is_some());
616		assert!(result.unwrap().is_authenticated());
617	}
618
619	#[tokio::test]
620	async fn test_token_authentication() {
621		let mut auth = TokenAuthentication::new();
622		auth.add_token("secret_token", "alice");
623
624		let mut headers = HeaderMap::new();
625		headers.insert("Authorization", "Token secret_token".parse().unwrap());
626
627		let request = Request::builder()
628			.method(Method::GET)
629			.uri("/")
630			.headers(headers)
631			.body(Bytes::new())
632			.build()
633			.unwrap();
634
635		let result = RestAuthentication::authenticate(&auth, &request)
636			.await
637			.unwrap();
638		let user = result.expect("token authentication should succeed");
639		assert!(user.is_authenticated());
640		let expected_id = uuid::Uuid::new_v5(&crate::USER_ID_NAMESPACE, b"alice").to_string();
641		assert_eq!(user.id(), expected_id);
642	}
643
644	#[tokio::test]
645	async fn test_remote_user_authentication() {
646		let auth = RemoteUserAuthentication::new();
647
648		let mut headers = HeaderMap::new();
649		headers.insert("REMOTE_USER", "bob".parse().unwrap());
650
651		let request = Request::builder()
652			.method(Method::GET)
653			.uri("/")
654			.headers(headers)
655			.body(Bytes::new())
656			.build()
657			.unwrap();
658
659		let result = RestAuthentication::authenticate(&auth, &request)
660			.await
661			.unwrap();
662		let user = result.expect("remote user authentication should succeed");
663		assert!(user.is_authenticated());
664		let expected_id = uuid::Uuid::new_v5(&crate::USER_ID_NAMESPACE, b"bob").to_string();
665		assert_eq!(user.id(), expected_id);
666	}
667
668	#[tokio::test]
669	async fn test_session_authentication() {
670		use crate::sessions::InMemorySessionBackend;
671		use crate::sessions::Session;
672
673		let session_backend = InMemorySessionBackend::new();
674
675		// Create a session with user data
676		let mut session = Session::new(session_backend.clone());
677		session
678			.set("_auth_user_id", "550e8400-e29b-41d4-a716-446655440000")
679			.unwrap();
680		session.set("_auth_user_name", "testuser").unwrap();
681		session.set("_auth_user_email", "test@example.com").unwrap();
682		session.set("_auth_user_is_active", true).unwrap();
683		session.save().await.unwrap();
684
685		// Get the generated session key
686		let session_key = session.get_or_create_key().to_string();
687
688		let auth = SessionAuthentication::new(session_backend);
689
690		let mut headers = HeaderMap::new();
691		let cookie_value = format!("sessionid={}", session_key);
692		headers.insert("Cookie", cookie_value.parse().unwrap());
693
694		let request = Request::builder()
695			.method(Method::GET)
696			.uri("/")
697			.headers(headers)
698			.body(Bytes::new())
699			.build()
700			.unwrap();
701
702		let result = RestAuthentication::authenticate(&auth, &request)
703			.await
704			.unwrap();
705		assert!(result.is_some());
706
707		// Verify the authenticated user
708		let user = result.unwrap();
709		assert_eq!(user.id(), "550e8400-e29b-41d4-a716-446655440000");
710		assert!(user.is_authenticated());
711	}
712
713	#[rstest]
714	#[tokio::test]
715	async fn test_token_auth_same_username_produces_same_id() {
716		// Arrange
717		let mut auth = TokenAuthentication::new();
718		auth.add_token("token1", "alice");
719		auth.add_token("token2", "alice");
720
721		let mut headers1 = HeaderMap::new();
722		headers1.insert("Authorization", "Token token1".parse().unwrap());
723		let request1 = Request::builder()
724			.method(Method::GET)
725			.uri("/")
726			.headers(headers1)
727			.body(Bytes::new())
728			.build()
729			.unwrap();
730
731		let mut headers2 = HeaderMap::new();
732		headers2.insert("Authorization", "Token token2".parse().unwrap());
733		let request2 = Request::builder()
734			.method(Method::GET)
735			.uri("/")
736			.headers(headers2)
737			.body(Bytes::new())
738			.build()
739			.unwrap();
740
741		// Act
742		let user1 = RestAuthentication::authenticate(&auth, &request1)
743			.await
744			.unwrap()
745			.unwrap();
746		let user2 = RestAuthentication::authenticate(&auth, &request2)
747			.await
748			.unwrap()
749			.unwrap();
750
751		// Assert
752		assert_eq!(
753			user1.id(),
754			user2.id(),
755			"same username must produce the same UUID"
756		);
757	}
758
759	#[rstest]
760	#[tokio::test]
761	async fn test_token_auth_user_has_default_privilege_flags() {
762		// Arrange
763		let mut auth = TokenAuthentication::new();
764		auth.add_token("secret_token", "alice");
765
766		let mut headers = HeaderMap::new();
767		headers.insert("Authorization", "Token secret_token".parse().unwrap());
768		let request = Request::builder()
769			.method(Method::GET)
770			.uri("/")
771			.headers(headers)
772			.body(Bytes::new())
773			.build()
774			.unwrap();
775
776		// Act
777		let user = RestAuthentication::authenticate(&auth, &request)
778			.await
779			.unwrap()
780			.unwrap();
781
782		// Assert
783		assert!(user.is_authenticated());
784		assert!(!user.is_admin());
785	}
786
787	#[rstest]
788	#[tokio::test]
789	async fn test_remote_user_auth_same_username_produces_same_id() {
790		// Arrange
791		let auth = RemoteUserAuthentication::new();
792
793		let mut headers1 = HeaderMap::new();
794		headers1.insert("REMOTE_USER", "bob".parse().unwrap());
795		let request1 = Request::builder()
796			.method(Method::GET)
797			.uri("/")
798			.headers(headers1)
799			.body(Bytes::new())
800			.build()
801			.unwrap();
802
803		let mut headers2 = HeaderMap::new();
804		headers2.insert("REMOTE_USER", "bob".parse().unwrap());
805		let request2 = Request::builder()
806			.method(Method::GET)
807			.uri("/")
808			.headers(headers2)
809			.body(Bytes::new())
810			.build()
811			.unwrap();
812
813		// Act
814		let user1 = RestAuthentication::authenticate(&auth, &request1)
815			.await
816			.unwrap()
817			.unwrap();
818		let user2 = RestAuthentication::authenticate(&auth, &request2)
819			.await
820			.unwrap()
821			.unwrap();
822
823		// Assert
824		assert_eq!(
825			user1.id(),
826			user2.id(),
827			"same username must produce the same UUID"
828		);
829	}
830
831	#[rstest]
832	#[tokio::test]
833	async fn test_token_auth_get_user_same_id_produces_same_uuid() {
834		// Arrange
835		let mut auth = TokenAuthentication::new();
836		auth.add_token("secret_token", "alice");
837
838		// Act
839		let user1 = auth.get_user("alice").await.unwrap().unwrap();
840		let user2 = auth.get_user("alice").await.unwrap().unwrap();
841
842		// Assert
843		assert_eq!(
844			user1.id(),
845			user2.id(),
846			"same user_id must produce the same UUID via get_user"
847		);
848	}
849
850	#[rstest]
851	#[tokio::test]
852	async fn test_token_auth_unknown_token_returns_none() {
853		// Arrange
854		let mut auth = TokenAuthentication::new();
855		auth.add_token("known_token", "alice");
856
857		let mut headers = HeaderMap::new();
858		headers.insert("Authorization", "Token unknown_token".parse().unwrap());
859		let request = Request::builder()
860			.method(Method::GET)
861			.uri("/")
862			.headers(headers)
863			.body(Bytes::new())
864			.build()
865			.unwrap();
866
867		// Act
868		let result = RestAuthentication::authenticate(&auth, &request)
869			.await
870			.unwrap();
871
872		// Assert
873		assert!(result.is_none());
874	}
875
876	#[rstest]
877	#[tokio::test]
878	async fn test_token_auth_get_user_unknown_returns_none() {
879		// Arrange
880		let mut auth = TokenAuthentication::new();
881		auth.add_token("secret_token", "alice");
882
883		// Act
884		let result = auth.get_user("nonexistent_user").await.unwrap();
885
886		// Assert
887		assert!(result.is_none());
888	}
889
890	#[tokio::test]
891	async fn test_custom_token_config() {
892		let config = TokenAuthConfig {
893			header_name: "X-API-Key".to_string(),
894			prefix: "Bearer".to_string(),
895		};
896
897		let mut auth = TokenAuthentication::with_config(config);
898		auth.add_token("my_token", "charlie");
899
900		let mut headers = HeaderMap::new();
901		headers.insert("X-API-Key", "Bearer my_token".parse().unwrap());
902
903		let request = Request::builder()
904			.method(Method::GET)
905			.uri("/")
906			.headers(headers)
907			.body(Bytes::new())
908			.build()
909			.unwrap();
910
911		let result = RestAuthentication::authenticate(&auth, &request)
912			.await
913			.unwrap();
914		let user = result.expect("custom token authentication should succeed");
915		assert!(user.is_authenticated());
916		let expected_id = uuid::Uuid::new_v5(&crate::USER_ID_NAMESPACE, b"charlie").to_string();
917		assert_eq!(user.id(), expected_id);
918	}
919
920	struct MockAuthBackend {
921		auth_result: Mutex<Option<Result<Option<Box<dyn AuthIdentity>>, AuthenticationError>>>,
922		get_user_result: Mutex<Option<Result<Option<Box<dyn AuthIdentity>>, AuthenticationError>>>,
923	}
924
925	impl MockAuthBackend {
926		fn new(
927			auth_result: Result<Option<Box<dyn AuthIdentity>>, AuthenticationError>,
928			get_user_result: Result<Option<Box<dyn AuthIdentity>>, AuthenticationError>,
929		) -> Self {
930			Self {
931				auth_result: Mutex::new(Some(auth_result)),
932				get_user_result: Mutex::new(Some(get_user_result)),
933			}
934		}
935	}
936
937	#[async_trait::async_trait]
938	impl AuthBackend for MockAuthBackend {
939		async fn authenticate(
940			&self,
941			_request: &Request,
942		) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
943			self.auth_result.lock().unwrap().take().unwrap_or(Ok(None))
944		}
945
946		async fn get_user(
947			&self,
948			_user_id: &str,
949		) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
950			self.get_user_result
951				.lock()
952				.unwrap()
953				.take()
954				.unwrap_or(Ok(None))
955		}
956	}
957
958	#[rstest]
959	#[tokio::test]
960	async fn test_composite_auth_all_backends_error() {
961		// Arrange
962		let composite = CompositeAuthentication::new()
963			.with_backend(MockAuthBackend::new(
964				Err(AuthenticationError::DatabaseError("db1 down".to_string())),
965				Err(AuthenticationError::DatabaseError("db1 down".to_string())),
966			))
967			.with_backend(MockAuthBackend::new(
968				Err(AuthenticationError::DatabaseError("db2 down".to_string())),
969				Err(AuthenticationError::DatabaseError("db2 down".to_string())),
970			));
971
972		let request = Request::builder()
973			.method(Method::GET)
974			.uri("/")
975			.body(Bytes::new())
976			.build()
977			.unwrap();
978
979		// Act
980		let result = RestAuthentication::authenticate(&composite, &request).await;
981
982		// Assert - all backends errored, so error is propagated
983		assert!(result.is_err());
984	}
985
986	#[rstest]
987	#[tokio::test]
988	async fn test_composite_auth_one_error_one_none() {
989		// Arrange - one backend errors, another returns Ok(None)
990		// This tests the intentional fallback behavior: Ok(None) means
991		// "this backend doesn't handle this auth type", so errors from
992		// other backends are irrelevant.
993		let composite = CompositeAuthentication::new()
994			.with_backend(MockAuthBackend::new(
995				Err(AuthenticationError::DatabaseError("db down".to_string())),
996				Err(AuthenticationError::DatabaseError("db down".to_string())),
997			))
998			.with_backend(MockAuthBackend::new(Ok(None), Ok(None)));
999
1000		let request = Request::builder()
1001			.method(Method::GET)
1002			.uri("/")
1003			.body(Bytes::new())
1004			.build()
1005			.unwrap();
1006
1007		// Act
1008		let result = RestAuthentication::authenticate(&composite, &request).await;
1009
1010		// Assert - one backend returned Ok(None), so errors are not propagated
1011		assert!(result.is_ok());
1012		assert!(result.unwrap().is_none());
1013	}
1014
1015	#[rstest]
1016	#[tokio::test]
1017	async fn test_composite_get_user_all_error() {
1018		// Arrange
1019		let composite = CompositeAuthentication::new()
1020			.with_backend(MockAuthBackend::new(
1021				Ok(None),
1022				Err(AuthenticationError::DatabaseError("db1 down".to_string())),
1023			))
1024			.with_backend(MockAuthBackend::new(
1025				Ok(None),
1026				Err(AuthenticationError::DatabaseError("db2 down".to_string())),
1027			));
1028
1029		// Act
1030		let result = AuthBackend::get_user(&composite, "some_user").await;
1031
1032		// Assert - all backends errored on get_user, so error is propagated
1033		assert!(result.is_err());
1034	}
1035}