Skip to main content

reinhardt_http/
auth_state.rs

1//! Authentication state stored in request extensions.
2//!
3//! This module provides [`AuthState`], a helper struct that stores
4//! authentication information in request extensions.
5//!
6//! `AuthState` uses a private validation marker to prevent external construction
7//! via struct literal syntax. Only the provided constructors
8//! ([`AuthState::authenticated`], [`AuthState::anonymous`], [`AuthState::from_extensions`])
9//! can create valid instances, preventing type collision attacks where
10//! malicious code could insert a spoofed auth state into request extensions.
11
12use crate::Extensions;
13use crate::extensions::{IsActive, IsAdmin, IsAuthenticated};
14
15/// Private marker to validate that an `AuthState` was created through
16/// official constructors, not through external struct literal construction.
17#[derive(Clone, Debug, PartialEq, Eq)]
18struct AuthStateMarker;
19
20/// Helper struct to store authentication state in request extensions.
21///
22/// This struct is used by authentication middleware to communicate
23/// the authenticated user's information to downstream handlers.
24///
25/// The struct contains a private field to prevent external construction
26/// via struct literal syntax. Use the provided constructors instead.
27///
28/// # Security Note
29///
30/// If this state is serialized and sent to client-side code (e.g., in
31/// a WASM SPA), the permission checks (`is_authenticated()`,
32/// `is_admin()`, `is_active()`) should only be used for **UI display
33/// purposes** (showing/hiding elements). An attacker can modify
34/// client-side state, so all authorization decisions must be enforced
35/// server-side through authentication middleware and permission
36/// classes (see `reinhardt-auth`).
37///
38/// # Example
39///
40/// ```rust,no_run
41/// # use reinhardt_http::AuthState;
42/// # struct Request { extensions: Extensions }
43/// # struct Extensions;
44/// # impl Extensions {
45/// #     fn insert<T>(&mut self, _value: T) {}
46/// #     fn get<T>(&self) -> Option<T> { None }
47/// # }
48/// # let mut request = Request { extensions: Extensions };
49/// // In middleware (after authentication)
50/// request.extensions.insert(AuthState::authenticated("123", false, true));
51///
52/// // In handler (via CurrentUser or directly)
53/// let auth_state: Option<AuthState> = request.extensions.get();
54/// ```
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct AuthState {
57	/// The authenticated user's ID as a string.
58	///
59	/// This is typically a UUID or database primary key serialized to string.
60	user_id: String,
61
62	/// Whether the user is authenticated.
63	is_authenticated: bool,
64
65	/// Whether the user has admin/superuser privileges.
66	is_admin: bool,
67
68	/// Whether the user's account is active.
69	is_active: bool,
70
71	/// Private validation marker to prevent external construction.
72	_marker: AuthStateMarker,
73}
74
75impl AuthState {
76	/// Creates a new authenticated state.
77	///
78	/// # Arguments
79	///
80	/// * `user_id` - The authenticated user's ID
81	/// * `is_admin` - Whether the user has admin privileges
82	/// * `is_active` - Whether the user's account is active
83	pub fn authenticated(user_id: impl Into<String>, is_admin: bool, is_active: bool) -> Self {
84		Self {
85			user_id: user_id.into(),
86			is_authenticated: true,
87			is_admin,
88			is_active,
89			_marker: AuthStateMarker,
90		}
91	}
92
93	/// Creates an anonymous (unauthenticated) state.
94	pub fn anonymous() -> Self {
95		Self {
96			user_id: String::new(),
97			is_authenticated: false,
98			is_admin: false,
99			is_active: false,
100			_marker: AuthStateMarker,
101		}
102	}
103
104	/// Create auth state from request extensions.
105	///
106	/// This method first attempts to retrieve an `AuthState` object that was
107	/// inserted directly into extensions (e.g., by custom middleware). If no
108	/// `AuthState` object is found, it falls back to reconstructing one from
109	/// individual newtype-wrapped entries (`IsAuthenticated`, `IsAdmin`,
110	/// `IsActive`) stored in extensions by legacy middleware.
111	///
112	/// # Returns
113	///
114	/// Returns `Some(AuthState)` if an `AuthState` object is found or if both
115	/// user_id and `IsAuthenticated` individual entries exist, `None` otherwise.
116	pub fn from_extensions(extensions: &Extensions) -> Option<Self> {
117		// Primary: try to get AuthState object directly
118		if let Some(state) = extensions.get::<AuthState>() {
119			return Some(state);
120		}
121		// Fallback: reconstruct from individual extension entries (backward compatibility)
122		let user_id = extensions.get::<String>()?;
123		let is_authenticated = extensions
124			.get::<IsAuthenticated>()
125			.map(|v| v.0)
126			.unwrap_or(false);
127		let is_admin = extensions.get::<IsAdmin>().map(|v| v.0).unwrap_or(false);
128		let is_active = extensions.get::<IsActive>().map(|v| v.0).unwrap_or(false);
129		Some(Self {
130			user_id,
131			is_authenticated,
132			is_admin,
133			is_active,
134			_marker: AuthStateMarker,
135		})
136	}
137
138	/// Get the authenticated user's ID.
139	pub fn user_id(&self) -> &str {
140		&self.user_id
141	}
142
143	/// Check if the user is authenticated.
144	pub fn is_authenticated(&self) -> bool {
145		self.is_authenticated
146	}
147
148	/// Check if the user has admin privileges.
149	pub fn is_admin(&self) -> bool {
150		self.is_admin
151	}
152
153	/// Check if the user's account is active.
154	pub fn is_active(&self) -> bool {
155		self.is_active
156	}
157
158	/// Check if user is anonymous (not authenticated).
159	pub fn is_anonymous(&self) -> bool {
160		!self.is_authenticated
161	}
162}
163
164#[cfg(test)]
165mod tests {
166	use super::*;
167	use rstest::rstest;
168
169	#[test]
170	fn test_authenticated() {
171		let state = AuthState::authenticated("user-123", true, true);
172
173		assert_eq!(state.user_id(), "user-123");
174		assert!(state.is_authenticated());
175		assert!(state.is_admin());
176		assert!(state.is_active());
177	}
178
179	#[test]
180	fn test_anonymous() {
181		let state = AuthState::anonymous();
182
183		assert!(state.user_id().is_empty());
184		assert!(!state.is_authenticated());
185		assert!(!state.is_admin());
186		assert!(!state.is_active());
187	}
188
189	#[rstest]
190	fn test_from_extensions_with_authstate_object() {
191		// Arrange
192		let extensions = Extensions::new();
193		let state = AuthState::authenticated("user-456", true, true);
194		extensions.insert(state.clone());
195
196		// Act
197		let result = AuthState::from_extensions(&extensions);
198
199		// Assert
200		assert_eq!(result, Some(state));
201		let retrieved = result.unwrap();
202		assert_eq!(retrieved.user_id(), "user-456");
203		assert!(retrieved.is_authenticated());
204		assert!(retrieved.is_admin());
205		assert!(retrieved.is_active());
206	}
207
208	#[rstest]
209	fn test_from_extensions_with_individual_values() {
210		// Arrange
211		let extensions = Extensions::new();
212		extensions.insert("user-789".to_string());
213		extensions.insert(IsAuthenticated(true));
214
215		// Act
216		let result = AuthState::from_extensions(&extensions);
217
218		// Assert
219		assert!(result.is_some());
220		let retrieved = result.unwrap();
221		assert_eq!(retrieved.user_id(), "user-789");
222		assert!(retrieved.is_authenticated());
223		assert!(!retrieved.is_admin());
224		assert!(!retrieved.is_active());
225	}
226
227	#[rstest]
228	fn test_from_extensions_empty() {
229		// Arrange
230		let extensions = Extensions::new();
231
232		// Act
233		let result = AuthState::from_extensions(&extensions);
234
235		// Assert
236		assert_eq!(result, None);
237	}
238
239	#[rstest]
240	fn test_from_extensions_preserves_admin_and_active() {
241		// Arrange
242		let extensions = Extensions::new();
243		let state = AuthState::authenticated("admin-user", true, true);
244		extensions.insert(state);
245
246		// Act
247		let result = AuthState::from_extensions(&extensions);
248
249		// Assert
250		let retrieved = result.unwrap();
251		assert_eq!(retrieved.user_id(), "admin-user");
252		assert!(retrieved.is_authenticated());
253		assert!(retrieved.is_admin());
254		assert!(retrieved.is_active());
255	}
256}