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::{AuthenticationBackend, AuthenticationError, SimpleUser, User};
7use reinhardt_http::Request;
8use uuid::Uuid;
9
10/// Remote user authentication backend
11///
12/// Authenticates users based on a trusted HTTP header (typically REMOTE_USER)
13/// set by an upstream authentication layer.
14///
15/// # Security Warning
16///
17/// This backend trusts the specified header completely. Only use this when
18/// your application is behind a properly configured authentication proxy
19/// that prevents clients from spoofing this header.
20///
21/// # Examples
22///
23/// ```no_run
24/// use reinhardt_auth::{AuthenticationBackend, SimpleUser};
25/// use bytes::Bytes;
26/// use hyper::{HeaderMap, Method};
27/// use reinhardt_http::Request;
28///
29/// # async fn example() {
30/// // Create auth backend
31/// let auth = reinhardt_auth::RemoteUserAuth::new();
32///
33/// // Create request with REMOTE_USER header
34/// let mut headers = HeaderMap::new();
35/// headers.insert("REMOTE_USER", "alice".parse().unwrap());
36/// let request = Request::builder()
37///     .method(Method::GET)
38///     .uri("/")
39///     .headers(headers)
40///     .body(Bytes::new())
41///     .build()
42///     .unwrap();
43///
44/// let result = auth.authenticate(&request).await.unwrap();
45/// assert!(result.is_some());
46/// assert_eq!(result.unwrap().get_username(), "alice");
47/// # }
48/// ```
49pub struct RemoteUserAuthentication {
50	/// Header name to check (default: "REMOTE_USER")
51	header_name: String,
52	/// Whether to force logout if header is missing
53	force_logout: bool,
54}
55
56impl RemoteUserAuthentication {
57	/// Create a new remote user authentication backend
58	///
59	/// # Examples
60	///
61	/// ```
62	/// use reinhardt_auth::RemoteUserAuth;
63	///
64	/// let auth = RemoteUserAuth::new();
65	/// ```
66	pub fn new() -> Self {
67		Self {
68			header_name: "REMOTE_USER".to_string(),
69			force_logout: true,
70		}
71	}
72
73	/// Set custom header name
74	///
75	/// # Examples
76	///
77	/// ```
78	/// use reinhardt_auth::RemoteUserAuth;
79	///
80	/// let auth = RemoteUserAuth::new()
81	///     .with_header("X-Auth-User");
82	/// ```
83	pub fn with_header(mut self, header: impl Into<String>) -> Self {
84		self.header_name = header.into();
85		self
86	}
87
88	/// Set whether to force logout when header is missing
89	pub fn force_logout(mut self, force: bool) -> Self {
90		self.force_logout = force;
91		self
92	}
93}
94
95impl Default for RemoteUserAuthentication {
96	fn default() -> Self {
97		Self::new()
98	}
99}
100
101#[async_trait::async_trait]
102impl AuthenticationBackend for RemoteUserAuthentication {
103	async fn authenticate(
104		&self,
105		request: &Request,
106	) -> Result<Option<Box<dyn User>>, AuthenticationError> {
107		// Get header value
108		let header_value = request
109			.headers
110			.get(&self.header_name)
111			.and_then(|v| v.to_str().ok());
112
113		match header_value {
114			Some(username) if !username.is_empty() => {
115				// Create user from header
116				Ok(Some(Box::new(SimpleUser {
117					id: Uuid::new_v4(),
118					username: username.to_string(),
119					email: format!("{}@example.com", username),
120					is_active: true,
121					is_admin: false,
122					is_staff: false,
123					is_superuser: false,
124				})))
125			}
126			_ => {
127				// No header or empty header
128				Ok(None)
129			}
130		}
131	}
132
133	async fn get_user(&self, _user_id: &str) -> Result<Option<Box<dyn User>>, AuthenticationError> {
134		// For remote user auth, we can't retrieve users by ID
135		// since we only have the username from the header
136		Ok(None)
137	}
138}
139
140#[cfg(test)]
141mod tests {
142	use super::*;
143	use bytes::Bytes;
144	use hyper::{HeaderMap, Method};
145
146	#[tokio::test]
147	async fn test_remote_user_with_header() {
148		let auth = RemoteUserAuthentication::new();
149		let mut headers = HeaderMap::new();
150		headers.insert("REMOTE_USER", "testuser".parse().unwrap());
151
152		let request = Request::builder()
153			.method(Method::GET)
154			.uri("/")
155			.headers(headers)
156			.body(Bytes::new())
157			.build()
158			.unwrap();
159
160		let result = auth.authenticate(&request).await.unwrap();
161		assert!(result.is_some());
162		assert_eq!(result.unwrap().get_username(), "testuser");
163	}
164
165	#[tokio::test]
166	async fn test_remote_user_without_header() {
167		let auth = RemoteUserAuthentication::new();
168		let request = Request::builder()
169			.method(Method::GET)
170			.uri("/")
171			.body(Bytes::new())
172			.build()
173			.unwrap();
174
175		let result = auth.authenticate(&request).await.unwrap();
176		assert!(result.is_none());
177	}
178
179	#[tokio::test]
180	async fn test_custom_header() {
181		let auth = RemoteUserAuthentication::new().with_header("X-Auth-User");
182		let mut headers = HeaderMap::new();
183		headers.insert("X-Auth-User", "alice".parse().unwrap());
184
185		let request = Request::builder()
186			.method(Method::GET)
187			.uri("/")
188			.headers(headers)
189			.body(Bytes::new())
190			.build()
191			.unwrap();
192
193		let result = auth.authenticate(&request).await.unwrap();
194		assert!(result.is_some());
195		assert_eq!(result.unwrap().get_username(), "alice");
196	}
197
198	#[tokio::test]
199	async fn test_empty_header() {
200		let auth = RemoteUserAuthentication::new();
201		let mut headers = HeaderMap::new();
202		headers.insert("REMOTE_USER", "".parse().unwrap());
203
204		let request = Request::builder()
205			.method(Method::GET)
206			.uri("/")
207			.headers(headers)
208			.body(Bytes::new())
209			.build()
210			.unwrap();
211
212		let result = auth.authenticate(&request).await.unwrap();
213		assert!(result.is_none());
214	}
215}