Skip to main content

reinhardt_auth/
remote_user.rs

1//! Remote User Authentication
2//!
3//! Authentication backend that trusts HTTP headers set by upstream
4//! authentication systems (e.g., Apache mod_auth, nginx auth_request).
5
6use crate::core::AuthIdentity;
7use crate::internal_user::InternalUser;
8use crate::{AuthBackend, AuthenticationError};
9use reinhardt_http::Request;
10use uuid::Uuid;
11
12/// Remote user authentication backend
13///
14/// Authenticates users based on a trusted HTTP header (typically REMOTE_USER)
15/// set by an upstream authentication layer.
16///
17/// # Security Warning
18///
19/// This backend trusts the specified header completely. Only use this when
20/// your application is behind a properly configured authentication proxy
21/// that prevents clients from spoofing this header.
22///
23/// # Examples
24///
25/// ```no_run
26/// use reinhardt_auth::{AuthBackend, AuthIdentity};
27/// use bytes::Bytes;
28/// use hyper::{HeaderMap, Method};
29/// use reinhardt_http::Request;
30///
31/// # async fn example() {
32/// // Create auth backend
33/// let auth = reinhardt_auth::RemoteUserAuth::new();
34///
35/// // Create request with REMOTE_USER header
36/// let mut headers = HeaderMap::new();
37/// headers.insert("REMOTE_USER", "alice".parse().unwrap());
38/// let request = Request::builder()
39///     .method(Method::GET)
40///     .uri("/")
41///     .headers(headers)
42///     .body(Bytes::new())
43///     .build()
44///     .unwrap();
45///
46/// let result = auth.authenticate(&request).await.unwrap();
47/// assert!(result.is_some());
48/// assert!(result.unwrap().is_authenticated());
49/// # }
50/// ```
51pub struct RemoteUserAuthentication {
52	/// Header name to check (default: "REMOTE_USER")
53	header_name: String,
54	/// Whether to force logout if header is missing
55	force_logout: bool,
56}
57
58impl RemoteUserAuthentication {
59	/// Create a new remote user authentication backend
60	///
61	/// # Examples
62	///
63	/// ```
64	/// use reinhardt_auth::RemoteUserAuth;
65	///
66	/// let auth = RemoteUserAuth::new();
67	/// ```
68	pub fn new() -> Self {
69		Self {
70			header_name: "REMOTE_USER".to_string(),
71			force_logout: true,
72		}
73	}
74
75	/// Set custom header name
76	///
77	/// # Examples
78	///
79	/// ```
80	/// use reinhardt_auth::RemoteUserAuth;
81	///
82	/// let auth = RemoteUserAuth::new()
83	///     .with_header("X-Auth-User");
84	/// ```
85	pub fn with_header(mut self, header: impl Into<String>) -> Self {
86		self.header_name = header.into();
87		self
88	}
89
90	/// Set whether to force logout when header is missing
91	pub fn force_logout(mut self, force: bool) -> Self {
92		self.force_logout = force;
93		self
94	}
95}
96
97impl Default for RemoteUserAuthentication {
98	fn default() -> Self {
99		Self::new()
100	}
101}
102
103#[async_trait::async_trait]
104impl AuthBackend for RemoteUserAuthentication {
105	async fn authenticate(
106		&self,
107		request: &Request,
108	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
109		// Get header value
110		let header_value = request
111			.headers
112			.get(&self.header_name)
113			.and_then(|v| v.to_str().ok());
114
115		match header_value {
116			Some(username) if !username.is_empty() => {
117				// Create identity from header
118				Ok(Some(Box::new(InternalUser {
119					id: Uuid::new_v5(&crate::USER_ID_NAMESPACE, username.as_bytes()),
120					username: username.to_string(),
121					email: String::new(),
122					is_active: true,
123					is_admin: false,
124					is_staff: false,
125					is_superuser: false,
126				})))
127			}
128			_ => {
129				// No header or empty header
130				Ok(None)
131			}
132		}
133	}
134
135	async fn get_user(
136		&self,
137		_user_id: &str,
138	) -> Result<Option<Box<dyn AuthIdentity>>, AuthenticationError> {
139		// For remote user auth, we can't retrieve users by ID
140		// since we only have the username from the header
141		Ok(None)
142	}
143}
144
145#[cfg(test)]
146mod tests {
147	use super::*;
148	use bytes::Bytes;
149	use hyper::{HeaderMap, Method};
150	use rstest::rstest;
151
152	#[tokio::test]
153	async fn test_remote_user_with_header() {
154		let auth = RemoteUserAuthentication::new();
155		let mut headers = HeaderMap::new();
156		headers.insert("REMOTE_USER", "testuser".parse().unwrap());
157
158		let request = Request::builder()
159			.method(Method::GET)
160			.uri("/")
161			.headers(headers)
162			.body(Bytes::new())
163			.build()
164			.unwrap();
165
166		let result = auth.authenticate(&request).await.unwrap();
167		let user = result.expect("remote user authentication should succeed");
168		let expected_id = Uuid::new_v5(&crate::USER_ID_NAMESPACE, b"testuser").to_string();
169		assert_eq!(user.id(), expected_id);
170		assert!(user.is_authenticated());
171	}
172
173	#[tokio::test]
174	async fn test_remote_user_without_header() {
175		let auth = RemoteUserAuthentication::new();
176		let request = Request::builder()
177			.method(Method::GET)
178			.uri("/")
179			.body(Bytes::new())
180			.build()
181			.unwrap();
182
183		let result = auth.authenticate(&request).await.unwrap();
184		assert!(result.is_none());
185	}
186
187	#[tokio::test]
188	async fn test_custom_header() {
189		let auth = RemoteUserAuthentication::new().with_header("X-Auth-User");
190		let mut headers = HeaderMap::new();
191		headers.insert("X-Auth-User", "alice".parse().unwrap());
192
193		let request = Request::builder()
194			.method(Method::GET)
195			.uri("/")
196			.headers(headers)
197			.body(Bytes::new())
198			.build()
199			.unwrap();
200
201		let result = auth.authenticate(&request).await.unwrap();
202		let user = result.expect("custom header authentication should succeed");
203		let expected_id = Uuid::new_v5(&crate::USER_ID_NAMESPACE, b"alice").to_string();
204		assert_eq!(user.id(), expected_id);
205		assert!(user.is_authenticated());
206	}
207
208	#[rstest]
209	#[tokio::test]
210	async fn test_same_username_produces_same_id() {
211		// Arrange
212		let auth = RemoteUserAuthentication::new();
213
214		let mut headers1 = HeaderMap::new();
215		headers1.insert("REMOTE_USER", "testuser".parse().unwrap());
216		let request1 = Request::builder()
217			.method(Method::GET)
218			.uri("/")
219			.headers(headers1)
220			.body(Bytes::new())
221			.build()
222			.unwrap();
223
224		let mut headers2 = HeaderMap::new();
225		headers2.insert("REMOTE_USER", "testuser".parse().unwrap());
226		let request2 = Request::builder()
227			.method(Method::GET)
228			.uri("/")
229			.headers(headers2)
230			.body(Bytes::new())
231			.build()
232			.unwrap();
233
234		// Act
235		let user1 = auth.authenticate(&request1).await.unwrap().unwrap();
236		let user2 = auth.authenticate(&request2).await.unwrap().unwrap();
237
238		// Assert
239		assert_eq!(
240			user1.id(),
241			user2.id(),
242			"same username must produce the same UUID"
243		);
244	}
245
246	#[rstest]
247	#[tokio::test]
248	async fn test_authenticated_user_has_default_privilege_flags() {
249		// Arrange
250		let auth = RemoteUserAuthentication::new();
251		let mut headers = HeaderMap::new();
252		headers.insert("REMOTE_USER", "testuser".parse().unwrap());
253		let request = Request::builder()
254			.method(Method::GET)
255			.uri("/")
256			.headers(headers)
257			.body(Bytes::new())
258			.build()
259			.unwrap();
260
261		// Act
262		let user = auth.authenticate(&request).await.unwrap().unwrap();
263
264		// Assert
265		assert!(user.is_authenticated());
266		assert!(!user.is_admin());
267	}
268
269	#[rstest]
270	#[tokio::test]
271	async fn test_get_user_always_returns_none() {
272		// Arrange
273		let auth = RemoteUserAuthentication::new();
274
275		// Act
276		let result = auth.get_user("any_user_id").await.unwrap();
277
278		// Assert
279		assert!(result.is_none());
280	}
281
282	#[tokio::test]
283	async fn test_empty_header() {
284		let auth = RemoteUserAuthentication::new();
285		let mut headers = HeaderMap::new();
286		headers.insert("REMOTE_USER", "".parse().unwrap());
287
288		let request = Request::builder()
289			.method(Method::GET)
290			.uri("/")
291			.headers(headers)
292			.body(Bytes::new())
293			.build()
294			.unwrap();
295
296		let result = auth.authenticate(&request).await.unwrap();
297		assert!(result.is_none());
298	}
299}